异步钩子
术语
异步的资源表示具有关联回调的对象。 此回调可能会被多次调用,比如 net.createServer()
中的 'connection'
事件、或者像 fs.open()
一样只调用一次。 资源也可以在调用回调之前关闭。 AsyncHook
没有明确区分这些不同的情况,而是将它们表示为抽象的概念,即资源。
如果使用 Worker
,每个线程都有一个独立的 async_hooks
接口,每个线程都会使用一组新的 async ID。
API概述
1 | import async_hooks from 'node:async_hooks'; |
async_hooks.createHook(callbacks)
- callbacks (Object)
- init
- before
- after
- destroy
- promiseResolve
回调
init()
/before()
/after()
/destroy()
在资源的生命周期内为相应的异步事件调用,所有回调皆是可选的。
1 | import { createHook } from 'node:async_hooks'; |
async_hooks.createHook(callbacks).enable()
返回 Asynchook
实例的引用
启用给定 AsyncHook
实例的回调。 如果没有提供回调,则启用是无操作的。
默认禁用 AsyncHook
实例。 如果 AsyncHook
实例应该在创建后立即启用,则可以使用以下模式。
1 | import { createHook } from 'node:async_hooks'; |
async_hooks.createHook(callbacks).disable()
从要执行的 AsyncHook
回调全局池中禁用给定 AsyncHook
实例的回调。 一旦一个钩子被禁用,则它在启用之前不会被再次调用。
为了 API 一致性,disable()
也返回 AsyncHook
实例。
Hooks Callbocks
init(asyncId, type, triggerAsyncId, resource)
asyncId
number
异步资源的唯一 ID。type
string
异步资源的类型。triggerAsyncId
number
在其执行上下文中创建此异步资源的异步资源的唯一 ID。resource
Object
对表示异步操作的资源的引用,需要在销毁期间释放。
type
type是字符串,标识调用init的资源类型,一般对应着资源的构造函数签名。
由 Node.js 本身创建的 type
资源可以在任何 Node.js 版本中更改。 有效值包括 PROMISE
TLSWRAP
、TCPWRAP
、TCPSERVERWRAP
、GETADDRINFOREQWRAP
、FSREQCALLBACK
、Microtask
和 Timeout
。
triggerAsyncId
triggerAsyncId
is the asyncId
of the resource that caused (or “triggered”) the new resource to initialize and that caused init
to call. This is different from async_hooks.executionAsyncId()
that only shows when a resource was created, while triggerAsyncId
shows why a resource was created.
下面是 triggerAsyncId
的简单演示:
1 | import { createHook, executionAsyncId } from 'node:async_hooks'; |
当使用 nc localhost 8080
访问服务器时的输出:
1 | TCPSERVERWRAP(5): trigger: 1 execution: 1 |
The TCPSERVERWRAP
is the server which receives the connections.
The TCPWRAP
is the new connection from the client. When a new connection is made, the TCPWrap
instance is immediately constructed. This happens outside of any JavaScript stack. (An executionAsyncId()
of 0
means that it is being executed from C++ with no JavaScript stack above it.) With only that information, it would be impossible to link resources together in terms of what caused them to be created, so triggerAsyncId
is given the task of propagating what resource is responsible for the new resource’s existence.
resource
resource
是一个对象,表示已初始化的实际异步资源。 访问对象的 API 可能由资源的创建者指定。 Node.js 本身创建的资源是内部的,可能随时更改。 因此没有为这些指定 API。在某些情况下,出于性能原因,资源对象会被重用,因此将其用作 WeakMap
中的键或向其添加属性是不安全的。
异步上下文示例 1:
1 | import async_hooks from 'node:async_hooks'; |
仅启动服务器输出:
1 | TCPSERVERWRAP(5): trigger: 1 execution: 1 |
如示例中所示,executionAsyncId()
和 execution
各自指定当前执行上下文的值; 这是通过调用 before
和 after
来描述的。
仅使用 execution
绘制资源分配图结果如下:
1 | root(1) |
TCPSERVERWRAP
不是这个图表的一部分,尽管它是调用 console.log()
的原因。 这是因为绑定到一个没有主机名的端口是一个同步操作,但是为了保持一个完全异步的 API,用户的回调被放在一个 process.nextTick()
中。 这就是 TickObject
出现在输出中并且是 .listen()
回调的 ‘parent’ 的原因。
该图仅显示创建资源的时间,而不显示创建原因,因此要跟踪原因,请使用 triggerAsyncId
。 可以用下图表示:
1 | bootstrap(1) |
异步上下文示例2:
结合测量工具,观测 setTimeout
异步资源:
1 | import async_hooks from 'async_hooks' |
系统输出:
1 | PerformanceEntry { |
before(asyncId)
asyncId number
当异步操作启动(如 TCP 服务器接收新连接)或完成(如将数据写入磁盘)时,会调用回调通知用户。 before
回调在所述回调执行之前被调用。 asyncId
是分配给即将执行回调的资源的唯一标识符。
before
回调将被调用 0 到 N 次。 如果异步操作被取消,或者例如,如果 TCP 服务器没有接收到连接,则 before
回调通常会被调用 0 次。 像 TCP 服务器这样的持久异步资源通常会多次调用 before
回调,而像 fs.open()
等其他操作只会调用一次。
after(asyncId)
asyncId number
在 before
中指定的回调完成后立即调用。
如果在执行回调期间发生未捕获的异常,则 after
将在 'uncaughtException'
事件触发或 domain
的处理程序运行后运行。
destroy(asyncId)
asyncId number
asyncId
对应的资源销毁后调用。 它也从嵌入器 API emitDestroy()
异步调用。
有些资源依赖垃圾回收来清理,所以如果引用传给 init
的 resource
对象,可能永远不会调用 destroy
,从而导致应用内存泄漏。 如果资源不依赖垃圾回收,则这不是问题。使用销毁钩子会导致额外的开销,因为它可以通过垃圾收集器跟踪 Promise
实例。
promiseResolve(asyncId)
asyncId number
当调用传给 Promise
构造函数的 resolve
函数时调用(直接或通过其他解决 promise 的方法)。
resolve()
不做任何可观察到的同步工作。
如果 Promise
是通过假设另一个 Promise
的状态来解决的,则此时 Promise
不一定满足或拒绝。
1 | new Promise((resolve) => resolve(true)).then((a) => {}); |
调用以下回调:
1 | init for PROMISE with id 5, trigger id: 1 |
async_hooks.executionAsyncResource()
- 返回
Object
代表当前执行的资源。 用于在资源中存储数据。 executionAsyncResource()
返回的资源对象通常是带有未记录 API 的内部 Node.js 句柄对象。 在对象上使用任何函数或属性都可能使你的应用崩溃,应该避免。
async_hooks.executionAsyncId()
- 返回
number
,表示当前执行上下文的asyncId
。 当有调用时对跟踪很有用。 executionAsyncId()
返回的 ID 与执行时机有关,与因果无关(被triggerAsyncId()
涵盖)- 默认情况下,promise 上下文可能无法获得精确的
executionAsyncIds
。
async_hooks.triggerAsyncId()
- 返回
number
,代表负责调用当前正在执行的回调的资源` ID。 - 认情况下,Promise 上下文可能无法获得有效的
triggerAsyncId
Promise 执行追踪
默认情况下,由于 V8 提供的 promise 自省 API 相对昂贵,因此不会为 promise 执行分配 asyncId
。 这意味着默认情况下,使用 promise 或 async
/await
的程序将无法正确执行并触发 promise 回调上下文的 id。
1 | import { executionAsyncId, triggerAsyncId } from 'node:async_hooks'; |
注意 then()
回调声称已在外部范围的上下文中执行,即使涉及异步的跃点。 另外,triggerAsyncId
的值是 0
,这意味着我们缺少有关导致(触发)then()
回调被执行的资源的上下文。
通过 async_hooks.createHook
安装异步钩子启用 promise 执行跟踪:
1 | import { createHook, executionAsyncId, triggerAsyncId } from 'node:async_hooks'; |
在这个示例中,添加任何实际的钩子函数启用了对 promise 的跟踪。 上面的例子中有两个 promise; Promise.resolve()
创建的 promise 和调用 then()
返回的 promise。 在上面的示例中,第一个 promise 得到 asyncId
6
,后者得到 asyncId
7
。 在执行 then()
回调期间,我们在 asyncId
7
的 promise 上下文中执行。 此 promise 由异步资源 6
触发。
promise 的另一个微妙之处是 before
和 after
回调仅在链式 promise 上运行。 这意味着不是由 then()
/catch()
创建的 promise 不会触发 before
和 after
回调。 有关更多详细信息,请参阅 V8 PromiseHooks API 的详细信息。