一、背景
你好!非常高兴你能看到这篇文章,在开始前我将大致介绍两点,关于适合谁看和能了解到什么。
适合谁看
类型人群 | 推荐指数 |
完全不了解Node,但有兴趣看看 | 适合,有点门槛,会尽量照顾到 |
写过Node Server,但没有用过了解过这个API | 很适合,交流学习的机会 |
用过了解过此API,但不知道他的前世今生 | 非常适合,你看标题啊 |
非常了解此API甚至提过相关PR | 非常适合! |
Takeaway
如果你有耐心看完本篇文章,你将了解到什么:
- 什么是 AsyncLocalStorage ?一般什么时候使用它?如何使用它?
- 没有 AsyncLocalStorage 这个 API 之前的时代是怎么解决异步存储的?大概的原理是什么?
- 了解广义上的 Async Local Storage 是如何一步一步发展过来的?(即合订本)
- AsyncLocalStorage 与最新的阿里巴巴主导的 TC39 提案 AsyncContext 之间是什么关系?
- 其他语言中类似的方法是怎么用的?
- Node 是如何实现的 AsyncHook?
二、开门见山:什么是 AsyncLocalStorage
一个案例引入
当一个 Request 通过层层关卡打到我们的 Server,一般会产生多个系统的日志,包括但不限于:
- 访问日志
- 异常日志
- SQL日志
- 第三方服务日志等
而当发生了线上问题的时候,需要进行溯源排查。
一般的做法是在请求之处,生成一个 unique traceId,此 id 在所有日志中携带就可以追踪该请求的所有链路,这就是所谓的全链路日志追踪。
好的,那么在 Node Server 中如何让一个请求的上下游调用都带上这个 traceId 呢,我们下面给几个方案。
(先不管这个 id 是 server 自己生成的还是 client 带上来的)
方案1:全局变量
简单,先拍脑袋想一个方法,全局变量globalTraceId!
因为closure闭包的特性,会帮我们close住全局变量的引用。
所以可以在任何异步调用中,读取这个变量所指的内存里的值,然后传给日志服务即可:(以 raw Node.js 为例)
// Raw Node.js HTTP server const http = require('http'); let globalTraceId // 全局变量 // 0. 处理请求的方法 function handleRequest(req, res) { // 生成唯一 traceId,每次请求进入,复写 globalTraceId 的值 globalTraceId = generateTraceId() // 检查用户cookie是否有效 cookieValidator().then((res) => { // 校验成功,返回给用户他需要的内容 res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('Congrats! Your damn cookie is the best one!'); res.end(); }).catch((err) => { // 把 traceId 连同 error 上报给异常监控系统 reportError(err, globalTraceId) // 写状态码500和错误信息等 // ... }); } // 1. 创建 server const server = http.createServer((req, res) => { handleRequest(req, res) }); // 2. 让 server 和 port:3000 建立 socket 链接,持续接收端口信息 server.listen(3000, () => { console.log('Server listening on port 3000'); });
但是在 Node.js 是单线程(主线程是单线程),globalTraceId这样全局变量,在第一个请求异步校验 cookie 的过程中,因为 main stack 已空,所以从backlog里面调入第二个请求进入主线程。
而globalTraceId会被第二个请求复写,导致第一个请求在错误上报的时候不能拿到正确的 id
方案2:直接透传参数
那上面全局变量的方法确实不对,那你会想,那我把生成的 traceId 作为参数一步步透传,是不是也可以达到每个请求同一个 traceId 的效果,好像好多库和框架就是这么做的。
是的,我们来看下直接透传参数是怎么做的。
const http = require('http'); function handleRequest(req, res) { const traceId = req.headers['x-trace-id'] || generateTraceId(); // 把 traceId 写入 req 这个 object,将参数一路带下去 req.traceId = traceId; // 同上 cookieValidator().then((result) => { // 校验成功,返回给用户他需要的内容 // ... }).catch((err) => { // 上报 traceId reportError(err, req.traceId) // 写状态码500和错误信息等 // ... }); } function cookieValidator() { return new Promise((resolve, reject) => { setTimeout(() => { // do someting // ... }, 1000); }); } // 此后省略监听等操作 // ...
能够看出来,把 traceId 通过 req 这个 object 一路传下去。能传下去的原因是 node 异步调用的时候,会创建一个新的 context(上下文),把当前调用栈、local variable、referenced global variable 存下来,一直到请求返回再在存下来的 context 中继续执行。
所以所谓的直接透传参数,就是通过 local variable 被存到了 async function call context 里面而完成了traceId在一次请求里面一路传递。
常见的 Node 库如何处理 traceId 的?
细心的你已经发现,我们最常用 express 或者 koa 这样的库的时候,不就是这样干的嘛。那我们来举几个常用的库的例子
Express.js
Express 是最早流行的Node server库,到现在依然很流行,实现了路由、中间件等各种功能。
下面看下 Express 是如何传递 TraceId 的
// Via express const express = require('express'); const { v4: uuidv4 } = require('uuid'); const { reportError } = require('./error-logging'); const app = express(); // 中间件 app.use((req, res, next) => { const traceId = uuidv4(); // generate a new UUID for the trace ID req.traceId = traceId; // attach the trace ID to the request object next(); }); // 设置路由 app.get('/', async (req, res, next) => { const traceId = req.traceId; try { // call an asynchronous function and pass along the trace ID const result = await someAsyncFunction(traceId); // do something with the result res.send(result); } catch (error) { // log the error and trace ID to the error logging system reportError(error, { traceId }); next(error); } }); // 监听端口 // ...
Koa.js
Koa 也是社区非常流行的库
Koa 早期使用 yield语法,后期支持了await语法。我们熟悉的 egg.js是基于 Koa 封装的Node Server框架。现在淘宝的midwayjs最早也是基于egg.js做的。
好的,辈分关系梳理完了,我们来看下在 Koa 中是如何透传参数的。
const Koa = require('koa'); const { v4: uuidv4 } = require('uuid'); const { reportError } = require('./error-logging'); const app = new Koa(); // 中间件A app.use(async (ctx, next) => { const traceId = uuidv4(); // generate a new UUID for the trace ID ctx.state.traceId = traceId; // store the trace ID in the Koa context object try { await next(); } catch (error) { // log the error and trace ID to the error logging system reportError(error, { traceId }); throw error; } }); // 中间件B,通过 ctx 透传 traceId app.use(async (ctx) => { const traceId = ctx.state.traceId; // call an asynchronous function and pass along the trace ID const result = await someAsyncFunction(traceId); // do something with the result ctx.body = result; }); // 监听端口 // ...
从上面的代码几乎和 express 一样,也是通过把 tracId 存到一路透传的 ctx 变量里面实现参数的透传。
Nest.js
Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。
总结来说 Nest 的特点是,完美支持ts、拥抱装饰器和注解,同时通过依赖注入(DI)和模块化思想等,使代码结构工整,易于阅读。更多介绍可以看官网。
在官方文档上,Nest 是推荐如何使用 Async Local Storage 的,可以见这篇文章,代码如下:https://docs.nestjs.com/recipes/async-local-storage#nestjs-cls
// 使用 nestjs-cls这个库 // npm i nestjs-cls // 模块初始化的时候,申明 Cls Module @Module({ imports: [ // Register the ClsModule, ClsModule.forRoot({ middleware: { // automatically mount the // ClsMiddleware for all routes mount: true, // and use the setup method to // provide default store values. setup: (cls, req) => { // 通过CLS存储 traceId cls.set('traceId', req.headers['x-trace-id'] || generateTraceId()); }, }, }), ], providers: [CatService], controllers: [CatController], }) export class AppModule {} // 在 Service 中注册 Cls,并且直接调用 @Injectable() export class CatService { constructor( // We can inject the provided ClsService instance, private readonly cls: ClsService, private readonly catRepository: CatRepository, ) {} getCatForUser() { // and use the "get" method to retrieve any stored value. const userId = this.cls.get('traceId'); // 获得 traceId return this.catRepository.getForUser(userId); } }
上面的代码我们可以看到,Nest 和上面的库肉眼上的不同,是采用了依赖注入的方式进行注册,同时大量使用装饰器的方法。
如果对依赖注入有兴趣可以看这篇文章,完成了IOC的一个简单的实现。
https://zhuanlan.zhihu.com/p/311184005
OK,那么 nestjs-cls这个库做了什么?我们来看这个包的描述
The nestjs-cls package provides several DX improvements over using plain AsyncLocalStorage (CLS is an abbreviation of the term continuation-local storage).
DX 是 Developer Experience 即开发者体验。所以这个库是用于提升开发者体验的基于原生AsyncLocalStorage的包,所以,下面终于介绍到了我们今天的主角AsyncLocalStorage!
方案3:今天的角,AsyncLocalStorage
AsyncLocalStorage 是 Nodejs 的 API(大约在2019年推出,2020年进入稳定版本)
简单来说,就是 Node.js 觉得大家的方法都不够优雅,哥直接在C++的层面给你们做掉这个事,然后提供个API给大伙用,怎么样?嗯大概就是这样。
前面已经说了,这是官方的API,自然有官方的文档来描述
官方文档: This class creates stores that stay coherent through asynchronous operations. While you can create your own implementation on top of the node:async_hooks module, AsyncLocalStorage should be preferred as it is a performant and memory safe implementation that involves significant optimizations that are non-obvious to implement. The following example uses AsyncLocalStorage to build a simple logger that assigns IDs to incoming HTTP requests and includes them in messages logged within each request. 文档地址: https://nodejs.org/api/async_context.html#class-asynclocalstorage
中文解释简单来说就是:AsyncLocalStorage是基于node:async_hooks实现的,并且(比之其他方法)是性能好、内存安全的方法去存储用于log的信息。
我们来看个例子 AsyncLocalStorage是怎么使用的
// How to use AsyncLocalStorage in Node.js import http from 'node:http'; import { AsyncLocalStorage } from 'node:async_hooks'; const asyncLocalStorage = new AsyncLocalStorage(); function logWithId(msg) { const traceId = asyncLocalStorage.getStore(); console.log(`${traceId}:`, msg); } let traceId = 0; http.createServer((req, res) => { // 关键的API调用 asyncLocalStorage.run(traceId++, () => { logWithId('start'); // Imagine any chain of async operations here setImmediate(() => { logWithId('finish'); res.end(); }); }); }).listen(8080); http.get('http://localhost:8080'); http.get('http://localhost:8080'); // Prints: // 0: start // 1: start // 0: finish // 1: finish
下面是这段代码的解释:
上面展示了2个请求被打到port:8080,异步调用被放在了asyncLocalStorage.run这个API里面。setImmediate异步调用的时候,通过logWithId取出该请求在进入时被赋予的id。可以看到,即使第二个请求的id(第一个请求进来的时候idSeq=0,第二个请求进来时idSeq=1)已经被log出来了,但是 line 8 的 同一个 asyncLocalStorage却能getStore出对应每个请求的不同id。
所以此方法,比通过函数内部变量的方式存储id优雅得多(另外,通过asyncLocalStorage这样隐式传递参数有点像函数式编程中的 State Monad 帮助开发者隐式管理参数传递)
小结
讲到这,我们Takeaway中的1已经被解决了
- 什么是 AsyncLocalStorage ?什么时候使用它?如何使用它?
如果认真看完,相信你对它已经有了个感性的认识。那么接下来,我们来看标题的后一句话,AsyncLocalStorage的前世今生
三、历史合订:在 Node.js 14 之前的 Async Local Storage
忘记历史就意味着背叛(狗头),所以我们来看看历史的合订本。
既然大家都有参数异步传递的需求,所以等19年AsyncLocalStorage被推出之前一定有社区方案被大家使用,所以我们来看下在 Node.js 14发布之前,社区是如何处理的。
这里我按照时间线来梳理此事,那要从2013年的春天说起
2013年:CLS横空出世(1.1k Star)
2013年4月30号,node-continuation-local-storage 这个仓库提交了第一个 commit,而这个仓库,社区更多地称它为CLS(Continuation-Local Storage,之后简称为CLS)。我截取了这个10岁的仓库README中的第一段给大家。
Continuation-local storage works like thread-local storage in threaded programming, but is based on chains of Node-style callbacks instead of threads. The standard Node convention of functions calling functions is very similar to something called "continuation-passing style" in functional programming, and the name comes from the way this module allows you to set and get values that are scoped to the lifetime of these chains of function calls.
这段话可以提取出几个信息:
- CLS 像多线程编程中的独立线程的 storage(TLS: thread local storage)一样工作,只是原理是基于 callback function 而不是线程
- 取名中有 Continuation 代表 C,是因为类似于函数编程中的 "continuation-passing style" 概念,旨在链式函数调用过程中维护一个持久的数据
- 你set和get的值,是在这些异步的function的整个生命周期的调用链内的
如何使用
下面是 github 上该仓库的示例
const express = require('express'); const cls = require('continuation-local-storage'); // require('cls-hooked') 也行,后面会提到 const app = express(); // Create a new namespace for the traceId const traceNamespace = cls.createNamespace('trace'); // Middleware to set the traceId for each request app.use((req, res, next) => { traceNamespace.run(() => { // Generate a new traceId if one doesn't exist traceNamespace.set('traceId', generateTraceId()); next(); }); }); // Route to get the traceId for the current request app.get('/traceId', async (req, res) => { try { const cookie = await cookieValidator() // 校验是否成功等 // ... } catch(e) { // 上报 traceId const traceId = traceNamespace.get('traceId'); reportError(err, traceId) } res.send(`Trace ID: ${traceId}`); });
每次执行 namespace.run(callback) 都会生成一个上下文。语法上,通过 run 方法,包住一个回调函数,在这个回调内可以访问到我们的 Continuation-Local Storage。这个xxx.run(callbakc, ...)的语法之后我们会多次看到。
实现原理
CLS 通过 process.addAsyncListener 这个 API 监听异步事件。在创建异步事件的时候将当前上下文传入,执行异步回调时,传入上下文,异步事件执行结束销毁上下文。而process.addAsyncListener是 Node v0.11 版本的 API,目前仓库引用的是 polyfill 的方法。
从下面截图中可以看下,这是段 polyfill 的代码是2013年9月2号被提交的,是相当古早的事情了。
// load polyfill if native support is unavailable if (!process.addAsyncListener) require('async-listener');
通过 async call 的事件,可以写出一个方法来存储我们在每个异步调用中的需要存储的变量。所以,这里还是用的一个局部变量来存储当前异步调用的上下文;同时在全局变量里面,维护了一个类似于栈的结构,通过此数据结构完成了nest的功能,即嵌套调用,run里面在嵌入一个run。不得不说,栈的结构非常适合做调用场景,因为 main call stack 就是一个栈 : )
我们来看下具体的代码实现:https://github.com/othiym23/node-continuation-local-storage/blob/master/context.js
// createNamespace 就是调用内部的 create 方法 function create(name) { assert.ok(name, "namespace must be given a name!"); var namespace = new Namespace(name); // 新建 space namespace.id = process.addAsyncListener({ create : function () { return namespace.active; }, before : function (context, storage) { if (storage) namespace.enter(storage); }, after : function (context, storage) { if (storage) namespace.exit(storage); }, error : function (storage) { if (storage) namespace.exit(storage); } }); process.namespaces[name] = namespace; return namespace; }
在create这个方法中,我们会新建一个 Namespace 来管理所有的方法,此 name 会在原生API上监听各种事件,同时触发我们的 store 变化。其中namespace.enter(storage)表示将此时的 ctx 入栈,在async call before的时候调用,即完成异步时间后、开始执行回调函数之前。而在async call after时,则是调用出栈方法 namespace.exit(storage)。
这个过程中,传入的参数 storage,就是我们在 store 中存入的 traceId
// cls的实现 // 这是 store 全局变量的 class function Namespace(name) { this.name = name; // changed in 2.7: no default context this.active = null; this._set = []; this.id = null; } // run方法 Namespace.prototype.run = function (fn) { var context = this.createContext(); this.enter(context); try { fn(context); return context; } catch (exception) { if (exception) { exception[ERROR_SYMBOL] = context; } throw exception; } finally { this.exit(context); } }; // 当前的 active 入栈,把新的 ctx 当做 this.active Namespace.prototype.enter = function (context) { assert.ok(context, "context must be provided for entering"); this._set.push(this.active); this.active = context; };
上面的 this._set就是刚才说的被维护的栈的结构。每一次 run 的调用,会创建一个 context 作为 this.active,同时把当前的老的 context(this.active)给 push 进入 this._set 这个栈,等待后续被pop后调用。
后来介绍的cls-hooked逻辑和他差不多,但是实现更容易理解,把他把每个异步调用的上下文存到了一个全局变量new map(),然后通过全局唯一的为异步调用生成的asyncId 作为 key 来区分。不过为了嵌套能力,栈的结构依旧保留。
虽然这个仓库已经在"历史的垃圾堆"里了,但是里面 API 的设计和数据存储结构的还是值得一看,因为之后的实现也沿用的类似的设计。
那我们接下来,看下一个API的发布。
2017年:async_hooks
async_hooks不是一个三方库,而是一个Node build-in的module,供用户调用。
The async_hooks API was released in Node.js 8.x in 2017
如何使用
通过 hook 可以往 async call 的各个阶段注册方法,类似于我们熟悉的React生命周期。同时,每次异步初始化,都会生成一个独一无二的asyncId,所以基于此可以实现我们的异步监听
const asyncHooks = require('async-hooks') const asyncHook = asyncHooks.createHook({ init: (asyncId, type, triggerAsyncId, resource) => {}, before: asyncId => {}, after: asyncId => {}, destroy: asyncId => {}, promiseResolve: asyncId => {}, }) asyncHook.enable(); // init() is called during object construction. The resource may not have // completed construction when this callback runs. Therefore, all fields of the // resource referenced by "asyncId" may not have been populated. function init(asyncId, type, triggerAsyncId, resource) { } // before() is called just before the resource's callback is called. It can be // called 0-N times for handles (such as TCPWrap), and will be called exactly 1 // time for requests (such as FSReqCallback). function before(asyncId) { } // after() is called just after the resource's callback has finished. function after(asyncId) { } // destroy() is called when the resource is destroyed. function destroy(asyncId) { }
实现原理
具体的讨论,可以看最后的延展章节,这里就不过多介绍了
2017年:cls-hooked
在2017年,async_hooks发布后,Jeff Lewis 这位老兄马不停蹄地将老仓库fork出来,重新用最新的 async_hooks 重写了 CLS。由于重写后 API 没有任何变化,就不再列举使用方法了。
下面我们来看看他的 README
This is a fork of CLS using AsyncWrap OR async_hooks instead of async-listener . When running Nodejs version < 8, this module uses AsyncWrap which is an unsupported Nodejs API, so please consider the risk before using it. When running Nodejs version >= 8.2.1, this module uses the newer async_hooks API which is considered Experimental by Nodejs.
从他的 README 可以看到,Node版本小于8的,使用了 AsyncWrap,而Node版本大于8.2.1的则用async_hooks重写了。
值得注意的是,他用 Experimental来描述此API,自然而然我们到 Nodejs 的官网可以看到,不再被推荐使用。原因是可用性、安全性、以及性能表现都有问题。
当你想使用 Async Context Tracking 的能力,取而代之的应该是 AsyncLocalStorage。
其实因为是 ALS 做了适当的优化并且语法更简洁
Stability: 1 - Experimental. Please migrate away from this API, if you can. We do not recommend using the createHook, AsyncHook, and executionAsyncResource APIs as they have usability issues, safety risks, and performance implications. Async context tracking use cases are better served by the stable AsyncLocalStorage API.
Node文档中,API的稳定性(就是告诉你敢不敢用这个 API)有分级
即Stability index被分为了4档,分别是:
- Stability: 0 - Deprecated
- Stability: 1 - Experimental
- Stability: 2 - Stable
- Stability: 3 - Legacy
我们的 async_hooks 被归到了 Experimental,在未来的任何版本中都可能出现非向后兼容的变化或删除。不建议在生产环境中使用该功能。所以,任何线上环境都只使用 Statbility:2 - Stable 就好。
2019年:AsyncLocalStorage(ALS)千呼万唤始出来
AsyncLocalStorage was first introduced in Node.js version 12.0.0, released on April 23, 2019.
因为AsyncLocalStorage (ALS) 的需求强烈,所以在经过一系列的实验后,ALS终于在 Node v13.10.0 完整支持了,随后 API 迁移到了长期支持版本 Node v12.17.0
因为之前已经介绍过API的使用,也夸了不少了,下面我从其他角度来阐述下
性能问题
AsyncLocalStorage 直接基于 async_hooks 进行封装。而 async_hooks 对于性能有一定的影响,在高并发的场景,大家对此 API 保持了谨慎的态度。
图片来源:https://github.com/bmeurer/async-hooks-performance-impact
同时,Node 的 issue 里面也有大量对此的讨论,比如这个《AsyncLocalStorage kills 97% of performance in an async environment #34493》:https://github.com/nodejs/node/issues/34493
本来Node的性能就是他的短板(或者说这事因为他的特质所导致的),现在用上ALS后性能又变差了不少,自然会让人在生产环境对他敬而远之,所以怎么解决呢?
后续更新
这个时候老大哥 V8 出手了,我借给你一个 V8 的 API 用吧,可以直接监听我的 Promise lifecycle
这就是 v8::Context PromiseHook API。这个 Hook 被加入 V8 后,很快被 Stephen Belanger 加入到了 async_hooks
这是引入 PromiseHook 的 PR 地址:PR: async_hooks: use new v8::Context PromiseHook API #36394:https://github.com/nodejs/node/pull/36394
然后,从21年5月这个评论里面就能看出,https://github.com/nodejs/node/issues/34493#issuecomment-845094849,在V8的加持下,在 Node v16.2.0 的版本里,ALS的性能"大幅"提升。
小结
我们完成了2和3,同时拓展了些类似的场景和用法
- 没有 AsyncLocalStorage 这个 API 之前的时代是怎么解决异步存储的?大概的原理是什么?
- 了解广义上的 Async Local Storage 是如何一步一步发展过来的?(即合订本)
通过此章,我们其实可以按照时间轴画一张图出来
好的,这幅图已经阐述了大致的发展的历史。
而最后一个之前从未提及的东西,也引出了这篇文章被写出的原因AsyncContext。为什么这么说,最总结的时候说吧。
四、异枝同根:ALS 与最新 TC39 提案 AsyncContext 的关系
由阿里巴巴 TC39 代表主导的 Async Context 提案 刚在 2023年 2 月初的 TC39 会议中成为了 TC39 Stage 1 提案。提案的目标是定义在 JavaScript 的异步任务中传递数据的方案。
既然看到这里,大家也能很快明白新的 TC39 提案AsyncContext是在做什么。对比两者的API,可以看到AsyncContext结合了AsyncLocalStorage和AsyncResource,并用了更通用的名字context来指代后两种方法的结合。
TC39提案: AsyncContext
class AsyncContext<T> { // 快照当前执行上下文中所有 AsyncContext 实例的值,并返回一个函数。 // 当这个函数执行时,会将 AsyncContext 状态快照恢复为执行上下文的全局状态。 static wrap<R>(fn: (...args: any[]) => R): (...args: any[]) => R; // 立刻执行 fn,并在 fn 执行期间将 value 设置为当前 // AsyncContext 实例的值。这个值会在 fn 过程中发起的异步操作中被 // 快照(相当于 wrap)。 run<R>(value: T, fn: () => R): R; // 获取当前 AsyncContext 实例的值。 get(): T; }
Node API: AsyncLocalStorage
class AsyncLocalStorage<T> { constructor(); // 立刻执行 callback,并在 callback 执行期间设置异步局部变量值。 run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R; // 获取异步局部变量当前值 getStore(): T; } class AsyncResource { // 快照当前的执行上下文异步局部变量全局状态。 constructor(); // 立刻执行 fn,并在 fn 执行期间将快照恢复为当前执行上下文异步局部变量全局状态。 runInAsyncScope<R>(fn: (...args: any[]) => R, thisArg, ...args: any[]): R; }
我在这里回答下这章的标题,他们的关系是什么?
答案是
- AsyncLocalStorage是Node的API;不是标准,只是一个 runtime 的 API
- AsyncContext是EMACScript标准(如果通过);通过后将成为规范,具体实现由各种 runtime 配合 JS Engine 来支持
看来这比雷锋和雷锋塔的关系更近些。
另外,为了让 ECMA 标准能同时能兼容Node的API(因为标准和目前的API不一样,到时候又得改),所以吞吞老师的提案让 AsyncContext 的语法和 AsyncLocalStorage 非常接近。
五、它山之石:其他语言中管理多线程上下文的方法
大家可以了解下同行都是怎么做的,大家看看门道,我也看个热闹。
Java
使用 ThreadLocal 来处理线程内的变量
public class TraceIdFilter implements Filter { private static final String TRACE_ID_HEADER = "X-Trace-Id"; private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String traceId = httpRequest.getHeader(TRACE_ID_HEADER); if (traceId == null || traceId.isEmpty()) { traceId = generateTraceId(); } TRACE_ID.set(traceId); try { chain.doFilter(request, response); } finally { TRACE_ID.remove(); } } public static String getTraceId() { return TRACE_ID.get(); } private String generateTraceId() { return UUID.randomUUID().toString(); } // Other methods for initializing and destroying the filter... }
C++
使用 thread_local 来处理本地线程变量
#include <iostream> #include <thread> thread_local int my_thread_local; void my_thread_function() { my_thread_local = std::hash<std::thread::id>()(std::this_thread::get_id()); std::cout << "My thread-local value is " << my_thread_local << std::endl; } int main() { std::thread t1(my_thread_function); std::thread t2(my_thread_function); t1.join(); t2.join(); return 0; }
Python
同上
import threading my_thread_local = threading.local() def my_thread_function(): my_thread_local.value = threading.get_ident() print(f"My thread-local value is {my_thread_local.value}") t1 = threading.Thread(target=my_thread_function) t2 = threading.Thread(target=my_thread_function) t1.start() t2.start() t1.join() t2.join()
......