术语

异步的资源表示具有关联回调的对象。 此回调可能会被多次调用,比如 net.createServer() 中的 'connection' 事件、或者像 fs.open() 一样只调用一次。 资源也可以在调用回调之前关闭。 AsyncHook 没有明确区分这些不同的情况,而是将它们表示为抽象的概念,即资源。

如果使用 Worker,每个线程都有一个独立的 async_hooks 接口,每个线程都会使用一组新的 async ID。

API概述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import async_hooks from 'node:async_hooks';

// Return the ID of the current execution context.
const eid = async_hooks.executionAsyncId();

// Return the ID of the handle responsible for triggering the callback of the
// current execution scope to call.
const tid = async_hooks.triggerAsyncId();

// Create a new AsyncHook instance. All of these callbacks are optional.
const asyncHook =
async_hooks.createHook({ init, before, after, destroy, promiseResolve });

// Allow callbacks of this AsyncHook instance to call. This is not an implicit
// action after running the constructor, and must be explicitly run to begin
// executing callbacks.
asyncHook.enable();

// Disable listening for new asynchronous events.
asyncHook.disable();

//
// The following are the callbacks that can be passed to createHook().
//

// 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) { }

// promiseResolve() is called only for promise resources, when the
// resolve() function passed to the Promise constructor is invoked
// (either directly or through other means of resolving a promise).
function promiseResolve(asyncId) { }

async_hooks.createHook(callbacks)

  • callbacks (Object)
    • init
    • before
    • after
    • destroy
    • promiseResolve

回调 init()/before()/after()/destroy() 在资源的生命周期内为相应的异步事件调用,所有回调皆是可选的。

1
2
3
4
5
6
import { createHook } from 'node:async_hooks';

const asyncHook = createHook({
init(asyncId, type, triggerAsyncId, resource) { },
destroy(asyncId) { },
});

async_hooks.createHook(callbacks).enable()

返回 Asynchook 实例的引用

启用给定 AsyncHook 实例的回调。 如果没有提供回调,则启用是无操作的。

默认禁用 AsyncHook 实例。 如果 AsyncHook 实例应该在创建后立即启用,则可以使用以下模式。

1
2
3
import { createHook } from 'node:async_hooks';

const hook = createHook(callbacks).enable();

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 TLSWRAPTCPWRAPTCPSERVERWRAPGETADDRINFOREQWRAPFSREQCALLBACKMicrotaskTimeout

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createHook, executionAsyncId } from 'node:async_hooks';
import { stdout } from 'node:process';
import net from 'node:net';
import fs from 'node:fs';

createHook({
init(asyncId, type, triggerAsyncId, resource) {
const eid = executionAsyncId();
fs.writeSync(
stdout.fd,
`${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`);
},
}).enable();

net.createServer((conn) => {}).listen(8080);

​ 当使用 nc localhost 8080 访问服务器时的输出:

1
2
TCPSERVERWRAP(5): trigger: 1 execution: 1
TCPWRAP(7): trigger: 5 execution: 0

​ 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import async_hooks from 'node:async_hooks';
import fs from 'node:fs';
import net from 'node:net';
import { stdout } from 'node:process';
const { fd } = stdout;

let indent = 0;
async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
const eid = async_hooks.executionAsyncId();
const indentStr = ' '.repeat(indent);
fs.writeSync(
fd,
`${indentStr}${type}(${asyncId}):` +
` trigger: ${triggerAsyncId} execution: ${eid}\n`);
},
before(asyncId) {
const indentStr = ' '.repeat(indent);
fs.writeSync(fd, `${indentStr}before: ${asyncId}\n`);
indent += 2;
},
after(asyncId) {
indent -= 2;
const indentStr = ' '.repeat(indent);
fs.writeSync(fd, `${indentStr}after: ${asyncId}\n`);
},
destroy(asyncId) {
const indentStr = ' '.repeat(indent);
fs.writeSync(fd, `${indentStr}destroy: ${asyncId}\n`);
},
}).enable();

net.createServer(() => {}).listen(8080, () => {
// Let's wait 10ms before logging the server started.
setTimeout(() => {
console.log('>>>', async_hooks.executionAsyncId());
}, 10);
});

仅启动服务器输出:

1
2
3
4
5
6
7
8
9
10
11
12
TCPSERVERWRAP(5): trigger: 1 execution: 1
TickObject(6): trigger: 5 execution: 1
before: 6
Timeout(7): trigger: 6 execution: 6
after: 6
destroy: 6
before: 7
>>> 7
TickObject(8): trigger: 7 execution: 7
after: 7
before: 8
after: 8

如示例中所示,executionAsyncId()execution 各自指定当前执行上下文的值; 这是通过调用 beforeafter 来描述的。

仅使用 execution 绘制资源分配图结果如下:

1
2
3
4
5
6
7
root(1)
^
|
TickObject(6)
^
|
Timeout(7)

TCPSERVERWRAP 不是这个图表的一部分,尽管它是调用 console.log() 的原因。 这是因为绑定到一个没有主机名的端口是一个同步操作,但是为了保持一个完全异步的 API,用户的回调被放在一个 process.nextTick() 中。 这就是 TickObject 出现在输出中并且是 .listen() 回调的 ‘parent’ 的原因。

该图仅显示创建资源的时间,而不显示创建原因,因此要跟踪原因,请使用 triggerAsyncId。 可以用下图表示:

1
2
3
4
5
6
7
8
9
10
 bootstrap(1)
|
˅
TCPSERVERWRAP(5)
|
˅
TickObject(6)
|
˅
Timeout(7)

异步上下文示例2:

结合测量工具,观测 setTimeout 异步资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import async_hooks from 'async_hooks'
import {performance, PerformanceObserver} from "perf_hooks"
const set = new Set()
const hook = async_hooks.createHook({
init(id, type) {
if (type === 'Timeout') {
performance.mark(`Timeout-${id}-Init`)
set.add(id)
}
},
destroy(id) {
if (set.has(id)) {
set.delete(id);
performance.mark(`Timeout-${id}-Destroy`)
performance.measure(`Timeout-${id}`,
`Timeout-${id}-Init`,
`Timeout-${id}-Destroy`)
}
}
})
hook.enable()
const obs = new PerformanceObserver((list, observer) => {
console.log(list.getEntries()[0])
performance.clearMarks()
observer.disconnect()
})
obs.observe({ entryTypes: ['measure'], buffered: true })
setTimeout(() => {}, 1000)

系统输出:

1
2
3
4
5
6
PerformanceEntry {
name: 'Timeout-8',
entryType: 'measure',
startTime: 89.764083,
duration: 1001.982167
}

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() 异步调用。

有些资源依赖垃圾回收来清理,所以如果引用传给 initresource 对象,可能永远不会调用 destroy,从而导致应用内存泄漏。 如果资源不依赖垃圾回收,则这不是问题。使用销毁钩子会导致额外的开销,因为它可以通过垃圾收集器跟踪 Promise 实例。

promiseResolve(asyncId)

asyncId number

当调用传给 Promise 构造函数的 resolve 函数时调用(直接或通过其他解决 promise 的方法)。

resolve() 不做任何可观察到的同步工作。

如果 Promise 是通过假设另一个 Promise 的状态来解决的,则此时 Promise 不一定满足或拒绝。

1
new Promise((resolve) => resolve(true)).then((a) => {});

调用以下回调:

1
2
3
4
5
6
init for PROMISE with id 5, trigger id: 1
promise resolve 5 # corresponds to resolve(true)
init for PROMISE with id 6, trigger id: 5 # the Promise returned by then()
before 6 # the then() callback is entered
promise resolve 6 # the then() callback resolves the promise by returning
after 6

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
2
3
4
5
6
7
import { executionAsyncId, triggerAsyncId } from 'node:async_hooks';

Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`);
});
// produces:
// eid 1 tid 0

注意 then() 回调声称已在外部范围的上下文中执行,即使涉及异步的跃点。 另外,triggerAsyncId 的值是 0,这意味着我们缺少有关导致(触发)then() 回调被执行的资源的上下文。

通过 async_hooks.createHook 安装异步钩子启用 promise 执行跟踪:

1
2
3
4
5
6
7
import { createHook, executionAsyncId, triggerAsyncId } from 'node:async_hooks';
createHook({ init() {} }).enable(); // forces PromiseHooks to be enabled.
Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`);
});
// produces:
// eid 7 tid 6

在这个示例中,添加任何实际的钩子函数启用了对 promise 的跟踪。 上面的例子中有两个 promise; Promise.resolve() 创建的 promise 和调用 then() 返回的 promise。 在上面的示例中,第一个 promise 得到 asyncId 6,后者得到 asyncId 7。 在执行 then() 回调期间,我们在 asyncId 7 的 promise 上下文中执行。 此 promise 由异步资源 6 触发。

promise 的另一个微妙之处是 beforeafter 回调仅在链式 promise 上运行。 这意味着不是由 then()/catch() 创建的 promise 不会触发 beforeafter 回调。 有关更多详细信息,请参阅 V8 PromiseHooks API 的详细信息。