学习使用 NodeJs 中 async-hooks 模块

小编 2026-06-18 阅读:871 评论:0
Async Hooks 是 Node8 新出来的特性,提供了一些 API 用于跟踪 NodeJs 中的异步资源的生命周期。属于内置模块,可以直接引用: let asycnHooks = re...

Async Hooks 是 Node8 新出来的特性,提供了一些 API 用于跟踪 NodeJs 中的异步资源的生命周期。属于内置模块,可以直接引用:


let asycnHooks = require(\'async_hooks\');

之所以会引入 async_hooks 模块,是因为在异步调用中我们很难正确的追踪异步调用的处理逻辑及关系。而 async_hooks 模块友好的解决了上述问题,主要提供以下功能和特性:

  • 每一个函数都会提供一个上下文,我们称之为 async scope;
  • 每一个 async scope 中都有一个 asyncId, 是当前 async scope 的标志,同一个的 async scope 中 asyncId 必然相同,最外层的 asyncId 是 1,每个异步资源在创建时 asyncId 全量递增的;
  • 每一个 async scope 中都有一个 triggerAsyncId 表示当前函数是由那个 async scope 触发生成的;
  • 通过 asyncId 和 triggerAsyncId 我们可以很方便的追踪整个异步的调用关系及链路;
  • 我们可以通过 async_hooks.createHook 函数来注册关于每个异步资源在生命周期中发生的 init/before/after/destory/promiseResolve 等相关事件的监听函数;
  • 同一个 async scope 可能会被调用及执行多次,不管执行多少次,其 asyncId 必然相同,通过监听函数,我们很方便追踪其执行的次数及时间及上线文关系;

executionAsyncId 和 triggerAsyncId

async_hooks 模块提供了 executionAsyncId 函数和 triggerAsyncId 函数来获取当前上下文的 asyncId 和 triggerAsyncId:

const async_hooks = require(\'async_hooks\');
const fs = require(\'fs\');
console.log(\'global.asyncId:\', async_hooks.executionAsyncId());  // global.asyncId: 1
console.log(\'global.triggerAsyncId:\', async_hooks.triggerAsyncId()); // global.triggerAsyncId: 0
fs.open(\'./app.js\', \'r\', (err, fd) => {
    console.log(\'fs.open.asyncId:\', async_hooks.executionAsyncId()); // fs.open.asyncId: 7
    console.log(\'fs.open.triggerAsyncId:\', async_hooks.triggerAsyncId()); // fs.open.triggerAsyncId: 1
});

通过上述打印结果我们可以看到全局的 asyncId 为 1,而 fs.open 回调函数的 asyncId 为 7,triggerAsyncId为 1。可以看出 fs.open 这个异步资源是由全局进来调用触发的。

async_hooks.createHook(callbacks)

我们可以使用 async_hooks.createHook 来创建一个异步资源的钩子,注册一些关于异步资源生命周期中可能发生事件的回调函数作为 async_hooks.createHook 的输入。每当异步资源被创建/执行/销毁时这些钩子函数会被触发:

const async_hooks = require(\'async_hooks\');
const asyncHook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) { },
  destroy(asyncId) { }
});
asyncHook.enable();   //通过 enable 函数开启钩子功能

目前 createHook 函数可以接受五类 Hook Callbacks 如下:

init(asyncId, type, triggerAsyncId, resource)

init 回调函数一般在异步资源初始化的时候被触发,其参数解释如下:

  • asyncId: 每一个异步资源都会生成一个唯一性标志
  • type: 异步资源的类型,一般都是资源的构造函数的名字,想知道都有哪些可以参考 async_hooks官方文档
  • triggerAsyncId: 表示触发当前异步资源被创建的对应的 async scope 的 asyncId
  • resource: 代表被初始化的异步资源对象
before(asyncId)

before 回调函数一般在 asyncId 对应的异步资源操作完成后准备执行回调前被调用,before 回调函数可能被执行多次,由其被回调的次数决定。

after(asyncId)

after 回调函数一般在异步资源执行完回调函数后会立即被调用,如果在执行回调函数的过程中发生未捕获的异常,after 事件会在触发 “uncaughtException” 事件后被调用。

destroy(asyncId)

当 asyncId 对应的异步资源被销毁时调用,有些异步资源的销毁要依赖垃圾回收机制,所以有些情况下由于内存泄漏的原因,destory 事件可能永远不会被触发。

promiseResolve(asyncId)

当 Promise 构造器中的 resovle 函数被执行时,promiseResolve 事件被触发。有些情况下,有些 resolve 函数是被隐式执行的,比如 .then 函数会返回一个新的 Promise,这个时候也会被调用。

以下表达式会触发两次 promiseResolve 事件,第一次是 new Promise() 时被显式执行的 resolve 函数, 第二次是.then函数的回调中被隐式执行的 resolve 函数

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

Promise 的执行追踪

由于 V8 的 promise introspection API 对于获取 asyncId 的执行成本比较高,所以默认情况下,我们是不给 Promise 分配新的 asyncId。也就是说默认情况下,我们使用 promises 或者 async/await 时是获取不到当前上下文正确的 asyncId 和 triggerId 。不过没关系,我们可以通过执行 async_hooks.createHook(callbacks).enable() 函数强制开启对 Promise 分配 asyncId:

const ah = require(\'async_hooks\');
ah.createHook({ init() {} }).enable(); // PromiseHooks 会被强制开启
Promise.resolve(1729).then(() => {
  console.log(`asyncId ${ah.executionAsyncId()} triggerId ${ah.triggerAsyncId()}`);
});

另外需要注意的是,beforeafter 事件的钩子函数只会在 Promise 的链式调用时被触发,也就是只有在 .then/.catch 函数中生成的 Promise 时会被触发,其它地方只会触发 init 和 promiseResolve 钩子事件函数。

所以当我们执行如何操作时:

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

分别被触发的事件流程如下(假设两次 Promise 上下文对应的 asyncId 分别为 5, 6):

  1. asyncId = 5 的 init 事件函数
  2. asyncId = 5 的 promiseResolve 事件函数
  3. asyncId = 6 的 init 事件函数
  4. asyncId = 6 的 before 事件函数
  5. asyncId = 6 的 promiseResovle 事件函数
  6. asyncId = 6 的 after 事件函数

异常处理

如果一个 AsyncHook 回调函数中发生异常,那么服务将打印错误日志并立即退出,同时所有 ‘uncaughtException’ 监听器将被移除,同时会触发 ‘exit’ 事件。之所以会立即退出进程,是因为如果这些 AsyncHook 函数运行不稳定,下一个相同事件被触发时很可能又抛出异常,这些函数主要就是为了监听异步事件的,如果不稳定应该及时发现并进行更正。

日志打印

由于 console.log 函数也是一个异步调用,如果我们在 asyncHook 函数中再调用 console.log 那么将再次触发相应的 hook 事件,造成死循环调用,所以我们在 asyncHook 函数中必须使用同步打印日志方式来跟踪,可以使用 fs.writeSync 函数:

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

参考文献

版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

热门文章
  • 机房智能化温湿度解决方式之POE供电以太网温湿度传感器

    机房智能化温湿度解决方式之POE供电以太网温湿度传感器
    机房智能化温湿度解决方式之POE供电以太网温湿度传感器 北京盈创力和电子科技有限公司 智能型TCP网口温湿度记录仪 北京IP网络温湿度记录仪厂家,北京盈创力和 北京智能型TCP网口温湿度记录仪IP网络温湿度记录仪是一种新型的基于TCP/IP协议双绞线以太网标准温湿度采集模块,利用它可以实现现场温度值、相对湿度值的采集,同时利用其自身的RJ45通信接口可以方便地和机房监控主机或交换机集线器进行联网。 工作于-40℃~85℃工业级带...
  • Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering

    Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering
    Problem Statement 我们考虑一个具有马尔可夫性质、非线性、非高斯的状态空间模型(State Space Model):对于一个时间序列上的观测结果{yt,t∈N}\\{ y_t , t \\in N \\}{yt​,t∈N},我们认为每个观测结果yty_tyt​的生成依赖于一个无法直接观察的隐变量xt∈{xt,t∈N}x_t \\in \\{x_t , t \\in N \\}xt​∈{xt​,t∈N},即:p(...
  • HTTP状态保持的原理

    HTTP状态保持的原理
    a)在用户登录之后,浏览器返回响应的时候会在响应中添加上cookieb)浏览器接收到cookie之后会自动保存c)当用户再次请求同一服务器中的其他网页的时候,浏览器会自动带上之前保存的cookied)服务接收到请求之后可以请 request 对象中取到cookie 判断当前用户是否登录  Http是无状态的,就是连接时数据互通,关闭后...
  • Hive 系统函数及示例

    Hive 系统函数及示例
    查看所有系统函数 show functions; 函数分类 内置函数【系统函数】 数学函数: floor、round、ceil、cos、log2等 字符串函数: length、reverse、trim、lower、get_json_object、repeat等 收集函数: size 转换函数: cast 日期函数: year、month、datediff、date、date_add等 条件函数: coalesce、case…w...
  • CSRF的原理和防范措施

    CSRF的原理和防范措施
    a)攻击原理:i.用户C访问正常网站A时进行登录,浏览器保存A的cookieii.用户C再访问攻击网站B,网站B上有某个隐藏的链接或者图片标签会自动请求网站A的URL地址,例如表单提交,传指定的参数iii.而攻击网站B在访问网站A的时候,浏览器会自动带上网站A的cookieiv.所以网站A在接收到请求之后可判断当前用户是登录状态,所以...
标签列表