Node 中的 AsyncLocalStorage 的前世今生和未来(二)

简介: 作者系统详实的介绍了什么是AsyncLocalStorage、如何使用、Node 是如何实现的 AsyncHook等。不论你是0基础还是对此API有些了解的读者都比较适合阅读此文。(文末有活动)

接上文:https://developer.aliyun.com/article/1496780


六、多走一步:AsyncLocalStorage 是如何实现的

下面两章,我们将多走俩步,介绍和讨论下这些 API 是如何实现的。如有错误,希望指正。

因为具体的一些细节会和 Node 的版本有强相关 所以特别声明:下面的文档、代码都以 Node v16.x LTS (Long-term Support) 中的文档和代码为例。

I Wonder Where the API Comes From

要想知道这个API是如何实现的。第一步,读文档

https://nodejs.org/docs/latest-v16.x/api/async_context.html#class-asynclocalstorage

很遗憾,文档并没有过多介绍这个API的实现,但是却透露了一个重要信息,source code 来自lib/async_hook.js

image.png

所以顺理成章,我们进入这个文件进行探索探索之前,我先大概介绍下 nodejs 项目的目录结构

image.png

配置文件和构建文件咱先不看,咱们看最主要的三个文件夹depslibsrc

名字 说明 主要使用语言
deps 包含 Node.js 使用的第三方依赖项,包括 V8 引擎和 libuv 库等。简单理解就是 node_modules C++/C
lib 包含 Node.js 核心库文件,包括用于处理 I/O 操作、网络和其他核心功能的模块。简单理解就是核心库的JS的封装,作为API提供给Nodejs使用。比如咱们熟悉的 require(http) 就是引用的 lib/http.js Javascript
src 包含 Node.js 的 C++ 源代码,包括核心组件如事件循环、模块加载机制和 HTTP 服务器。C++核心模块 C++

让我们一步步来梳理 AsyncLocalStorage API's calling chain

Javascript Zone

OK到这一步,我们大概知道了的 AsyncLocalStorageAPI 来自哪里,接着我们打开 async_hooks.js文件。

// location: lib/async_hooks.js


// 1. 真正的储存位置
const storageList = [];
const storageHook = createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    const currentResource = executionAsyncResource();
    // Value of currentResource is always a non null object
    for (let i = 0; i < storageList.length; ++i) {
      storageList[i]._propagate(resource, currentResource);
    }
  }
});

function createHook(fns) {
  return new AsyncHook(fns);
}

// 2. ALS Class 的实现
class AsyncLocalStorage {
  constructor() {
    this.kResourceStore = Symbol('kResourceStore');
    this.enabled = false;
  }

  _enable() {
    if (!this.enabled) {
      this.enabled = true;
      ArrayPrototypePush(storageList, this);
      storageHook.enable();
    }
  }

  run(store, callback, ...args) {
    // Avoid creation of an AsyncResource if store is already active
    if (ObjectIs(store, this.getStore())) {
      return ReflectApply(callback, null, args);
    }

    this._enable();

    // 新老 resource 交接班
    const resource = executionAsyncResource(); // 新的resource
    const oldStore = resource[this.kResourceStore];  // 老的resource

    resource[this.kResourceStore] = store; // 新的resource,traceId存放的地方

    try {
      return ReflectApply(callback, null, args); 
    } finally {
      resource[this.kResourceStore] = oldStore; // 等callback执行结束,将老的oldStore归还
    }
  }


  getStore() {
    if (this.enabled) {
      const resource = executionAsyncResource();
      return resource[this.kResourceStore];
    }
  }
}

为了便于阅读,上面的代码删去了不必要的部分。

当我们运行 AsyncLocalStorage.run(callback)的时候,会执行2个动作:

参照下面的API调用代码来看
  • this._enable(),激活 hook 监听
  • 通过 executionAsyncResource(),获得当前异步资源resource(AsyncResource,每次异步调用,V8都会创建一个对应的AsyncResource)
  • 然后把我们传入的 store 当做resourcekResourceStore对应的值(store就是traceId,kResourceStore就是一个Symbol而已)
  • 然后才执行我们的callback代码ReflectApply(callback, null, args)。其中ReflectApply直接理解为JS中的Function.Apply()
  • 之后这个 run 方法里面,任何通过executionAsyncResource()得到的值都是我们👆🏻上面的 traceId
  • 最后,我们通过getStore()拿到这个traceId,完美!
import { AsyncLocalStorage } from 'node:async_hooks';

const asyncLocalStorage = new AsyncLocalStorage();

let traceId = 0;
asyncLocalStorage.run(traceId++, () => {
  console.log(asyncLocalStorage.getStore())
  setImmediate(() => {
    console.log(asyncLocalStorage.getStore())
  })
});

asyncLocalStorage.run('test', () => {})

总的来说基于此,我们ALS.run()里面的callback同步请求都可以顺利拿到对应的store,但是异步的请求每次会新建 AsyncResource。所以拿不到上面的 store

此时我们来看storageHook变量,他创建了一个 Hook 来监听 init 事件,在每个异步事件初始化的时候,把当前的异步资源(AsyncResource)的值传给我们初始化的异步调用,我们命名它为异步A。所以在不久的将来,异步A执行的时候,我们通过asyncLocalStorage.getStore()可以拿到正确的值

结论,ALS也是基于AsyncHook和神秘的executionAsyncResource实现的。但是他只使用了 init Hook,而且封装的很好,所以性能和可用性都更好。

所以不管怎么看 AsyncHook都更可疑。所以下章我们来分析它是如何实现的,并且可以监听劫持任何一种异步操作的生命周期。

另外,这个 run方法 里面又是一个类似栈的结构,只不过实现的形式是通过类似于递归调用实现的。 通过这个方法完成了嵌套nest能力 其实从这段代码的 commit log 中也能证实我们的猜想

image.png

这个截图里面还有个小彩蛋 Reviewed-By: Zijian Liu其实,这种递归还有点像 Leetcode 经典的回溯算法题 51. N-Queens,它就是对 tree 的DFS遍历。DFS遍历用递归写是上面的写法,而用迭代写就是用Stack了


七、再走一步:AsyncHook 是如何在 Node Core 层实现的

Intro and Guess

在上一个章节中,我们已经发现 AsyncHook 和 executionAsyncResource 是比较可疑的。所以我们先来看 AsyncHook

// location: lib/async_hooks.js

class AsyncHook {
  constructor({
    init,
    before,
    after,
    destroy,
    promiseResolve
  }) {
    this[init_symbol] = init;
    this[before_symbol] = before;
    this[after_symbol] = after;
    this[destroy_symbol] = destroy;
    this[promise_resolve_symbol] = promiseResolve;
  }

  enable() {
    // The set of callbacks for a hook should be the same regardless of whether
    // enable()/disable() are run during their execution. The following
    // references are reassigned to the tmp arrays if a hook is currently being
    // processed.
    const {
      0: hooks_array,
      1: hook_fields
    } = getHookArrays();

    // Each hook is only allowed to be added once.
    if (ArrayPrototypeIncludes(hooks_array, this))
      return this;

    const prev_kTotals = hook_fields[kTotals];

    // createHook() has already enforced that the callbacks are all functions,
    // so here simply increment the count of whether each callbacks exists or
    // not.
    hook_fields[kTotals] = hook_fields[kInit] += +!!this[init_symbol];
    hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol];
    hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol];
    hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol];
    hook_fields[kTotals] +=
      hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol];
    ArrayPrototypePush(hooks_array, this);

    if (prev_kTotals === 0 && hook_fields[kTotals] > 0) {
      enableHooks();
    }

    updatePromiseHookMode();

    return this;
  }

}

还好,构造函数不算可疑,和预想的一样,把每个阶段的 hook 的 callback 存起来。然后再通过 enable 方法激活他们,那 line 44 的 enableHooks() 来自哪里?来自lib/internal/async_hooks.js(是的,自此我们知道原来每一个lib文件夹的API还调用了lib/internal这层内部的实现,简单理解就是又抽象了一层出来。)

看下代码

// location: lib/internal/async_hooks.js

const async_wrap = internalBinding('async_wrap');
const { setCallbackTrampoline } = async_wrap;

function enableHooks() {
  async_hook_fields[kCheck] += 1;

  setCallbackTrampoline(callbackTrampoline);
}

在里面调用了 setCallbackTrampoline,这个方法来自 async_wrap。

其实,看代码可以知道,我们刚刚调用的神秘的 executionAsyncResource 里面调用的关键变量,几乎都来自async_wrap,通过 internalBinding 获取到的。

既然这里用的是internalBinding(String),入参是个string,再加上这个方法的名字,我们自然可以猜测 internalBinding 里面还有许多string可以被调用,而且可枚举完(但是为啥没有统一管理为const、enum、或者symbol,这个可能需要聪明的你去解答了)

随便搜下,无数个方法都通过 internalBinding获取到,猜测验证结束

image.png

但是在这一步,我们遇到一些小麻烦,因为你会发现internalBinding并没有通过 require()的方式引用进代码,command+左键也只能到它的d.ts定义里面。感觉似乎陷入了死胡同或者什么黑魔法之中。

// d.ts file
declare function InternalBinding(binding: 'blob'): {
  createBlob(sources: Array<Uint8Array | InternalBlobBinding.BlobHandle>, length: number): InternalBlobBinding.BlobHandle;
  FixedSizeBlobCopyJob: typeof InternalBlobBinding.FixedSizeBlobCopyJob;
  getDataObject(id: string): [handle: InternalBlobBinding.BlobHandle | undefined, length: number, type: string] | undefined;
  storeDataObject(id: string, handle: InternalBlobBinding.BlobHandle, size: number, type: string): void;
  revokeDataObject(id: string): void;
};

https://github.com/nodejs/help/issues/3079

简单一搜,就能搜到问题的回答,柳暗花明。(其实全局搜索也能搜到这个方法)

让我们把目光来到lib/internal/bootstrap/loader.js

// location: lib/internal/bootstrap/loader.js

// This file creates the internal module & binding loaders used by built-in
// modules. In contrast, user land modules are loaded using
// lib/internal/modules/cjs/loader.js (CommonJS Modules) or
// lib/internal/modules/esm/* (ES Modules).

// C++ binding loaders:
// - internalBinding(): the private internal C++ binding loader, inaccessible
//   from user land unless through `require('internal/test/binding')`.
//   These C++ bindings are created using NODE_MODULE_CONTEXT_AWARE_INTERNAL()
//   and have their nm_flags set to NM_F_INTERNAL.

// This file is compiled as if it's wrapped in a function with arguments
// passed by node::RunBootstrapping()
/* global process, getLinkedBinding, getInternalBinding, primordials */


// Set up internalBinding() in the closure.
/**
 * @type {InternalBinding}
 */
let internalBinding;
{
  const bindingObj = ObjectCreate(null);
  // eslint-disable-next-line no-global-assign
  internalBinding = function internalBinding(module) {
    let mod = bindingObj[module];
    if (typeof mod !== 'object') {
      mod = bindingObj[module] = getInternalBinding(module);
      ArrayPrototypePush(moduleLoadList, `Internal Binding ${module}`);
    }
    return mod;
  };
}

这是简化后的备注,简单理解就是这个文件被加载为了 loader,既然是 loader 自然要 load 文件,load 什么呢?

用于 load built-in modules(加载内部模块)。把C++的模块load到js里面进行调用

大家可以发现,In line 30,getInternalBinding 也是‘凭空’出现的。自然我们去搜下他。

image.png

看来,getInternalBinding() 是真在js文件里面找不到了,因为它的定义不来自js世界。

Alright,恭喜你,到达C++的地盘。

其实,我们是使用 internalBinding() 将 async_wrap 从async_wrap.cc加载到了 async_wrap.js

那 internalBinding 是被定义在了 js 文件里面,又为啥可以被全局访问到,而且没使用 require。这个在备注里面也有解释

// This file is compiled as if it's wrapped in a function with arguments // passed by node::RunBootstrapping()

这个文件被编译了,就像函数的参数一样被传入node::RunBootstrapping()调用。而这个方法,就是Node的C++ built-in module的启动函数。

C++ World

我们回到主线,看看async_wrap在做什么,是怎么实现的

总之,async_wrapgetInternalBinding给get到了loader.js里面,那有get就一定有个对应的set或者说注册。是的,我们把目光放到src文件夹的这里src/async_wrap.cc

// location: src/async_wrap.cc

// 该文件结尾处
// 在node_binding.h里面定义了宏macro
// #define NODE_MODULE_CONTEXT_AWARE_INTERNAL(modname, regfunc)
NODE_MODULE_CONTEXT_AWARE_INTERNAL(async_wrap, node::AsyncWrap::Initialize);  
NODE_MODULE_EXTERNAL_REFERENCE(async_wrap, node::AsyncWrap::RegisterExternalReferences);

在该文件的结尾处,我们通过NODE_MODULE_CONTEXT_AWARE_INTERNAL这个宏(macro),注册了async_wrap

自此我们在代码层面回答了 where set `async_wrap` is called其实我们的内部模块(Internal Module)是通过NODE_MODULE_CONTEXT_AWARE_INTERNAL来暴露给js以作调用的,过程中使用的 loader 就是上文提到的getInternalBinding至于NODE_MODULE_CONTEXT_AWARE_INTERNAL是怎么实现的,欢迎大家自己深挖。

NODE_MODULE_CONTEXT_AWARE_INTERNAL-> NODE_MODULE_CONTEXT_AWARE_CPP -> ...

自此小结,我们知道了async_wrap是在哪里被注册的,以及async_wrap的行为在哪里被定义。接下来我们看async_wrap.cc内部在做什么。

async_wrap.cc

这个文件有700行左右的代码,我就不全部贴出来了。

不过在看具体代码前,我们先猜下,他在干嘛。下面是我的猜测,从名字来看,他叫做 async wrap,wrap就是把我们的async call给包住了,包住代表什么?代表async call想做什么事,都得先过一层我wrap再说。

熟悉吗?这就是我们所谓的监听(listen)、劫持(hijack)、包裹(wrap)、监控(monitor),大致都是一个意思。wrap其实还有点像AOP(面向切面编程)、洋葱模型、代理proxy等东西,都是主观上给人一种层(layer)的感觉。

话说回来,在一开始我们就知道,libuv是用C写的库,负责异步I/O等工作,那我们的就猜测,你既然wrap劫持async call,那具体是劫持什么呢,多半就是和libuv的东西有关了。所以下一步,我们找文件里面和libuv和劫持相关的代码。

不过很遗憾,并没有在async_wrap.cc代码内部找到uv.h头文件(代表libuv)的引用,至少libuv没有被直接使用在这个文件里。但是大的方向不会错,那我们来看libuv官网文档http://docs.libuv.org/en/v1.x/api.html

这里里面有大量句柄(handle),用于处理I/O事件的对象,它负责管理底层I/O资源的分配和释放,以及处理I/O事件的通知和回调。

注意下面只是猜想!不是对的!猜想1:

应该就是调用的 libuv 里这个API了,用作提交异步请求,并且拿到异步的回调。 两个库内部的代码直接互相调用,并不符合规范,他们都被包装到一个内部对外的API进行交互 所以我们 async_wrap <---> libuv 这种关系可以抽象为下面的图👇🏻

image.png

猜想2:

AsyncWrap作为基类,提供各种基础API和JS层交互。衍生的子类和 libuv 通过 uv_handle_t 进行交互,由 libuv 通知子类去执行对应的 async hook

我们可以在下一章看看猜测是否正确

最后,libuv里面的uv不是每日访问量UV(Unique Visitor)或者DAU(Daily Active User),而是 Lib of Unicorn Velociraptor(独角迅猛龙)。你问我为啥是独角迅猛龙,因为。。。。看他的logo吧

image.png

How Is AsyncHook.Init() Invoked by Node Core

为了回答上面那个猜想,我想我可以直接介绍下 AsyncHook 的 init 方法是如何被 Node Core 调用的。

既然我们注册了方法,把 init 存在了某个地方,那么在一个 async call 的初始化的时候,它会被触发。所以我们有了下面2个步骤:

Step 1, where is callback stored in?

上一章说了,每一个 hook cb 被存在了 AsyncHook Class 对应的 this[xxx_symbol] = xxx 里面,在被 enable 的时候,通过 ArrayPrototypePush(hooks_array, this) 被 push 到了 hooks_array。

这个 hooks_array 来自 lib/internal/async_hooks.js,叫做 active_hooks,看下定义,一个简单的 object

// location: lib/internal/async_hooks.js

// Properties in active_hooks are used to keep track of the set of hooks being
// executed in case another hook is enabled/disabled. The new set of hooks is
// then restored once the active set of hooks is finished executing.
const active_hooks = {
  // Array of all AsyncHooks that will be iterated whenever an async event
  // fires. Using var instead of (preferably const) in order to assign
  // active_hooks.tmp_array if a hook is enabled/disabled during hook
  // execution.
  array: [],
  // Use a counter to track nested calls of async hook callbacks and make sure
  // the active_hooks.array isn't altered mid execution.
  call_depth: 0,
  // Use to temporarily store and updated active_hooks.array if the user
  // enables or disables a hook while hooks are being processed. If a hook is
  // enabled() or disabled() during hook execution then the current set of
  // active hooks is duplicated and set equal to active_hooks.tmp_array. Any
  // subsequent changes are on the duplicated array. When all hooks have
  // completed executing active_hooks.tmp_array is assigned to
  // active_hooks.array.
  tmp_array: null,
  // Keep track of the field counts held in active_hooks.tmp_array. Because the
  // async_hook_fields can't be reassigned, store each uint32 in an array that
  // is written back to async_hook_fields when active_hooks.array is restored.
  tmp_fields: null
};


module.exports = {
  executionAsyncId,
  triggerAsyncId,
  // Private API
  getHookArrays,
  symbols: {
    async_id_symbol, trigger_async_id_symbol,
    init_symbol, before_symbol, after_symbol, destroy_symbol,
    promise_resolve_symbol, owner_symbol
  },
  // ..
  executionAsyncResource,
  // Internal Embedder API
  // ...
  nativeHooks: {  
    init: emitInitNative, //  <====== 看这里
    before: emitBeforeNative,
    after: emitAfterNative,
    destroy: emitDestroyNative,
    promise_resolve: emitPromiseResolveNative
  },
};

同时,这个 lib/internal/async_hooks.js 文件export的方法中有个名字比较可疑,emitInitNative。Native, Native,名字里面有 Native,实在太不对劲了。我们来看下实现:

// location: lib/internal/async_hooks.js

// Emit From Native //

// Used by C++ to call all init() callbacks. Because some state can be setup
// from C++ there's no need to perform all the same operations as in
// emitInitScript.
function emitInitNative(asyncId, type, triggerAsyncId, resource) {
  active_hooks.call_depth += 1;
  resource = lookupPublicResource(resource);
  // Use a single try/catch for all hooks to avoid setting up one per iteration.
  try {
    // Using var here instead of let because "for (var ...)" is faster than let.
    // Refs: https://github.com/nodejs/node/pull/30380#issuecomment-552948364
    // eslint-disable-next-line no-var
    for (var i = 0; i < active_hooks.array.length; i++) {
      if (typeof active_hooks.array[i][init_symbol] === 'function') {
        active_hooks.array[i][init_symbol](
          asyncId, type, triggerAsyncId,
          resource
        );
      }
    }
  } catch (e) {
    fatalError(e);
  } finally {
    active_hooks.call_depth -= 1;
  }

  // Hooks can only be restored if there have been no recursive hook calls.
  // Also the active hooks do not need to be restored if enable()/disable()
  // weren't called during hook execution, in which case active_hooks.tmp_array
  // will be null.
  if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) {
    restoreActiveHooks();
  }
}

通过 comment 证明了,emitInitNative 确实这个方法,最终会被 native 调用(C++)。可以看到,我们一路存下来的 active_hooks 会在 line 18 里面,在js层被调用。

同理,我们上面的 before, after 等也是一样的。至此,我们回答了标题,callback是存在哪里并被执行的。

先别急着走到下一步,我们还差一件事,就是如何把这个 js 的方法暴露给我们的 native 层?在这里

// location: lib/internal/bootstrap.js

const { nativeHooks } = require('internal/async_hooks');
internalBinding('async_wrap').setupHooks(nativeHooks);

神秘的,从C++层来的 async_wrap 负责把我们的 nativeHooks 注册到c++。OK,现在我们只需要记住 setupHooks is responsible for registering nativeHooks 即可。

Step 2, which code line is responsible for calling init callback?

先说结论,每一个 async call 都会由一个 C++ 的类叫做 AsyncWrap 来包装,地址在 src/async_wrap.cc

同时,上面提到,我们是通过下面这个方法把 async_wrap 暴露出去的,所以我来看Initialize

NODE_MODULE_CONTEXT_AWARE_INTERNAL(async_wrap, node::AsyncWrap::Initialize)

Initialize

// location: src/async_wrap.cc

void AsyncWrap::Initialize(Local<Object> target,
                           Local<Value> unused,
                           Local<Context> context,
                           void* priv) {
  Environment* env = Environment::GetCurrent(context);
  Isolate* isolate = env->isolate();
  HandleScope scope(isolate);

  env->SetMethod(target, "setupHooks", SetupHooks);
  env->SetMethod(target, "setCallbackTrampoline", SetCallbackTrampoline);
  env->SetMethod(target, "pushAsyncContext", PushAsyncContext);
  env->SetMethod(target, "popAsyncContext", PopAsyncContext);
  env->SetMethod(target, "executionAsyncResource", ExecutionAsyncResource);
  env->SetMethod(target, "clearAsyncIdStack", ClearAsyncIdStack);
  env->SetMethod(target, "queueDestroyAsyncId", QueueDestroyAsyncId);
  env->SetMethod(target, "setPromiseHooks", SetPromiseHooks);
  env->SetMethod(target, "registerDestroyHook", RegisterDestroyHook);

  PropertyAttribute ReadOnlyDontDelete =
      static_cast<PropertyAttribute>(ReadOnly | DontDelete);
  // ...
}

先解释几个基本概念和数据类型:

  • Isolate: line 8 中被用到,被定义在 v8.h。Isolate是V8引擎的一个独立实例。它是一个独立的JavaScript运行时,运行在一个单独的线程中,拥有自己的内存堆、垃圾回收器和执行上下文。可以在一个进程中创建多个Isolate,每个Isolate提供一个单独的运行时环境,可以独立地运行JavaScript代码。
  • Context: line 5 中被用到,被定义在v8.h。Context表示Isolate中的一个执行上下文。它是一个JavaScript对象,包含当前执行上下文的状态,包括变量、函数和其他数据。Context在Isolate中创建,并与特定的执行线程相关联。可以在单个Isolate中创建多个Context,每个Context可以独立地执行JavaScript代码。我们熟知的 vm.createContext()也是创建了一个新的 Context 实例。
  • Local: lin 5 中被用到,被定义在v8.h。在 V8 引擎(Node.js 的 JavaScript 引擎)中,用于表示 JavaScript 对象的本地句柄(Handle)
  • 看下原文描述:An object reference managed by the v8 garbage collector. All objects returned from v8 have to be tracked by the garbage collector so that it knows that the objects are still alive。
  • 可以理解为类似于指针,但是指向的内存地址会随着GC(garbage collection)而变化,确保总是指向我们需要的值,同时管理引用的对象是否可以被清理。
  • Local 句柄是一种轻量级的对象引用,它在 V8 的内存管理系统中的生命周期是有限的。当 V8 的垃圾回收器进行内存回收时,Local 句柄所引用的对象可能会被清理。Local<Context>就代表一个V8 Context 的本地句柄。除了本地句柄Local,还有MaybeLocal,Eternal等类型。
  • line 9 中的 HandleScope 也是用于管理句柄生命周期的。
  • Environment: line 7 中被用到,被定义在src/env.h。在 Node.js 的 C++ 层面,Environment 类是一个核心组件,负责管理和维护 Node.js 应用程序的上下文环境和资源。它提供了一个桥梁,让 Node.js 的 JavaScript 层与底层的 C++ 实现进行交互。Environment 类封装了许多与 JavaScript 运行时相关的资源。以下是 Environment 类的一些主要职责:
  • 管理 V8 Isolate 实例:Isolate 是 V8 引擎中表示独立的 JavaScript 运行时环境的对象。一个 Environment 实例与一个特定的 Isolate 实例关联,它们共同构成了 Node.js 应用程序的运行时环境。
  • 内存管理:Environment 类负责管理与内存相关的资源,如对象句柄、缓冲区等。通过创建 V8 HandleScope 和 EscapableHandleScope 实例,Environment 能确保 V8 能正确地管理 JavaScript 对象的生命周期。
  • 与 JavaScript 层的互操作:Environment 类提供了一系列方法,使 JavaScript 层与底层的 C++ 实现进行交互。这些方法包括设置 JavaScript 对象的属性和方法、执行回调函数等。

OK,基于此我们再来看代码。

在 line 7,我们通过 Environment::GetCurrent(context) 获取到当前上下文的 Environment* 指针,接着在line 11,通过这个指针所指的方法 SetMethod,讲我们的 SetupHooks 绑定到上一节提到过的 internalBinding('async_wrap').setupHooks(nativeHooks);

那 SetupHooks 是怎么实现的

SetupHooks

// location: src/async_wrap.cc

static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  CHECK(args[0]->IsObject());

  // All of init, before, after, destroy, and promise_resolve are supplied by
  // async_hooks internally, so this should only ever be called once. At which
  // time all the functions should be set. Detect this by checking if
  // init !IsEmpty().
  CHECK(env->async_hooks_init_function().IsEmpty());

  Local<Object> fn_obj = args[0].As<Object>();

#define SET_HOOK_FN(name)                                                      \
  do {                                                                         \
    Local<Value> v =                                                           \
        fn_obj->Get(env->context(),                                            \
                    FIXED_ONE_BYTE_STRING(env->isolate(), #name))              \
            .ToLocalChecked();                                                 \
    CHECK(v->IsFunction());                                                    \
    env->set_async_hooks_##name##_function(v.As<Function>());                  \
  } while (0)

  SET_HOOK_FN(init);
  SET_HOOK_FN(before);
  SET_HOOK_FN(after);
  SET_HOOK_FN(destroy);
  SET_HOOK_FN(promise_resolve);
#undef SET_HOOK_FN
}

这里第一步,是获取 Environment* 指针,接着确保 args[0] 是一个 Objext,同时 async_hooks_init_function 是 empty,确保只会被初始化1次。

接着定义了 SET_HOOK_FN 这个宏(marco),通过这个宏,将 init 方法绑定到触发函数

env -> set_async_hooks_##name##_function()

##name## 就是我们的init、before、after、destroy变量,‘#’ 和 ‘##’ 语法用于 C/C++ 中的 macro 命令。最后的 #undef 使用是去掉宏的定义,目的是为了防止此宏在其他地方调用。

所以最终,这个方法,在 AsyncWrap::EmitAsyncInit 中调用

EmitAsyncInit

// location: src/async_wrap.cc

void AsyncWrap::EmitAsyncInit(Environment* env,
                              Local<Object> object,
                              Local<String> type,
                              double async_id,
                              double trigger_async_id) {
  CHECK(!object.IsEmpty());
  CHECK(!type.IsEmpty());
  AsyncHooks* async_hooks = env->async_hooks();

  // Nothing to execute, so can continue normally.
  if (async_hooks->fields()[AsyncHooks::kInit] == 0) {
    return;
  }

  HandleScope scope(env->isolate());
  Local<Function> init_fn = env->async_hooks_init_function();

  Local<Value> argv[] = {
    Number::New(env->isolate(), async_id),
    type,
    Number::New(env->isolate(), trigger_async_id),
    object,
  };

  TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal);
  USE(init_fn->Call(env->context(), object, arraysize(argv), argv));
}

// location: v8.h

/**
 * A JavaScript function object (ECMA-262, 15.3).
 */
class V8_EXPORT Function : public Object {}
set the fn: env -> set_async_hooks_##name##_function() get the corresponding fn: env -> async_hooks_init_function()

在 line 18,我们获得了之前注册的 init,这是一个 Local 句柄,Local<Function> 就是一个指向 js 的 function 的句柄,最后,我们通过 line 28 的 init_fn -> Call() 可以来触发 js 函数。

至此,说完了一个 Async Init Hook 是如何被完整调用的。

下面我们来回顾下整体的关系,算是是一个小结。

Sum Up: High-Level Overview Flowchart

image.png

  • API:很好理解,暴露了这3个重要的API
  • Node Core - Native JS Module:
  • 上面的3个API来自async_hooks.js中的3个类:AsyncLocalStorage/AsyncResource/AsyncHook
  • AsyncHook负责注册4个阶段的Callback function
  • 在这里通过 internalBinding('async_wrap') 获得C++层的 AsyncWrap
  • Node Core - C++ Binding:
  • 在 async_wrap.cc 中定义了关键的基类 AsyncWrap,它继承自 BaseObject
  • 通过 NODE_MODULE_CONTEXT_AWARE_INTERNAL 方法暴露给 JS 层
  • AsyncWrap只是一个基类。UPDWrap、TCPWrap、FSEventWrap等直接或间接继承者它,为各种 Wrap 提供负责触发Hook回调的方法。
  • 比如 TCPWrap -> ConnectionWrap -> LibuvStreamWrap -> HandleWrap -> AsyncWrap
  • libuv的方法在具体的 Wrap 里面调用。
  • 举个例子,当一个 TCP 网络请求发出时,会执行 new TCPWrap,通过 uv_tcp_connect() 发起链接(方法来自libuv);
  • 链接成功后,会通过一个句柄(uv_tcp_t),对 libuv 保持访问。整个过程中句柄类型会被转变 uv_tcp_t -> uv_stream_t
  • 当请求返回的时候, TCPHandle 对象会触发 uv__stream_io() 方法去执行 uv__read(),最终通知 TCPWrap 或者其父类执行回调
  • src/api 文件夹中给三方addons提供了一些API,其中AsyncResource是基于AsyncWrap的封装,AsyncWrap触发before和after的异步事件是通过 AsyncWrap::MakeCallback 方法,该方法调用 CallbackScope 内部的 InternalMakeCallback
  • Deps:
  • Libuv: 对I/O异步、网络异步回调负责
  • V8: 对Promise 和 async/await 语法负责
  • 最终通过 AsyncWrap 通知到 JS 层的 AsyncHook

Add-On

这里是一些收集资料过程中发现的相关信息和彩蛋,分享给大家

Performance Improvement

image.png

PR:async_hooks: use resource stack for AsyncLocalStorage run #39890

https://github.com/nodejs/node/pull/39890

关键字:

  • using stack instead of AsyncResouce instance
  • eliminate extra lifecycle event

上文提到过,执行AsyncLocalStorage.run的时候有个 commit log,这次的 PR 目的是为了提升性能。

PromiseHook

image.png

PR: async_hooks: fast-path for PromiseHooks in JS #32891

https://github.com/nodejs/node/pull/32891

又是这个哥们。

也有一个彩蛋。

Reviewed-By: Chengzhong Wu

(但是为啥明明被关闭的PR,代码 commit 却出现了在了 Node v16.18?因为Node发版和代码,不使用Github的PR,有一套自己的流程)

image.png

How is loader created

这里放下 lib/internal/bootstrap/loader.js文件的完整备注,有兴趣的同学可以看下,里面的分情况讨论了require语法里拿到的各种对象是怎么被加载进去的。

其实把文档的注释读完,整个 loader 这一层就会清楚很多。所以非常喜欢 Node 的丰富的注释。

// location: lib/internal/bootstrap/loader.js

// This file creates the internal module & binding loaders used by built-in
// modules. In contrast, user land modules are loaded using
// lib/internal/modules/cjs/loader.js (CommonJS Modules) or
// lib/internal/modules/esm/* (ES Modules).
//
// This file is compiled and run by node.cc before bootstrap/node.js
// was called, therefore the loaders are bootstrapped before we start to
// actually bootstrap Node.js. It creates the following objects:
//
// C++ binding loaders:
// - process.binding(): the legacy C++ binding loader, accessible from user land
//   because it is an object attached to the global process object.
//   These C++ bindings are created using NODE_BUILTIN_MODULE_CONTEXT_AWARE()
//   and have their nm_flags set to NM_F_BUILTIN. We do not make any guarantees
//   about the stability of these bindings, but still have to take care of
//   compatibility issues caused by them from time to time.
// - process._linkedBinding(): intended to be used by embedders to add
//   additional C++ bindings in their applications. These C++ bindings
//   can be created using NODE_MODULE_CONTEXT_AWARE_CPP() with the flag
//   NM_F_LINKED.
// - internalBinding(): the private internal C++ binding loader, inaccessible
//   from user land unless through `require('internal/test/binding')`.
//   These C++ bindings are created using NODE_MODULE_CONTEXT_AWARE_INTERNAL()
//   and have their nm_flags set to NM_F_INTERNAL.
//
// Internal JavaScript module loader:
// - NativeModule: a minimal module system used to load the JavaScript core
//   modules found in lib/**/*.js and deps/**/*.js. All core modules are
//   compiled into the node binary via node_javascript.cc generated by js2c.py,
//   so they can be loaded faster without the cost of I/O. This class makes the
//   lib/internal/*, deps/internal/* modules and internalBinding() available by
//   default to core modules, and lets the core modules require itself via
//   require('internal/bootstrap/loaders') even when this file is not written in
//   CommonJS style.
//
// Other objects:
// - process.moduleLoadList: an array recording the bindings and the modules
//   loaded in the process and the order in which they are loaded.

'use strict';

// This file is compiled as if it's wrapped in a function with arguments
// passed by node::RunBootstrapping()
/* global process, getLinkedBinding, getInternalBinding, primordials */

The Workflow of Node.js Startup

图片来源: https://leezhenghui.github.io/node.js/2018/11/11/demystify-node.js-modularity.html

image.png

image.png

我们的上面提到过的 loader 就是在 LoadEnv 阶段被执行的。

而我们之前讨论的 AsyncHook 的实现,就在上面的 [node native module] - [node-core(c/c++)] 两层之间。

有兴趣的同学可以看看原文章。

非常详细得介绍了 Node,名字也很有意思 《Demystify node.js - Modularization》https://leezhenghui.github.io/node.js/2018/11/11/demystify-node.js-modularity.html


八、最后的最后:打个总结

写这篇文章的想法非常突然。大概是2月初,吞吞老师在群里分享了关于 AsyncContext 提案进入 Stage1 的消息,就瞄了一眼发现看不懂,于是在大群里求教。

后来,了解到这个概念和Node中的一些API类似,之后就开始慢慢了解这个东西的背景,心里想着都整理了些内容了,不如写篇文章总结下吧。但是光介绍 API 咋用由有点浅,官网文档也有,于是所幸梳理下这个概念的发展过程。后来感觉,发展过程都看了,不如实现原理也一把梭吧,况且要写就质量写高点嘛,正好借这个机会再了解下Node,于是,便有了这篇文章。

不过,整个过程还是比较艰难的,边猜边学边写。难点有二,难点之一,是文章内容对我有一定难度;从一个点切入进入(一个API),发现还需要一条线(Node的机制)甚至一个面的知识(其他语言、甚至相关领域的知识)。一开始有点overwhelming,发现需要同步学的知识太多了。后来可算找到了方法,就是我只要紧盯着这个点,不要盲目展开 Todo List,就慢慢向外扩展即可。难点之二,是行文如何展开,因为就我自己读技术书籍的感觉,很少描写为什么,需要自己去想或者本身就有知识储备才能得到答案,我还是希望能提供更多的背景来一步一步介绍这个概念。想法很美好,不过事实上自己上手之后发现,还是很难理出一条思路,你需要像演员带入角色一样来带入读者,很考验功力,特别是最后3章,把握不住了。

最后,希望大家在有收获的地方不吝点赞,发现错误的地方不吝指正,谢谢!


作者 | 逻千

来源 | 阿里云开发者公众号

相关文章
|
6天前
|
存储 JavaScript 前端开发
Node 中的 AsyncLocalStorage 的前世今生和未来(一)
作者系统详实的介绍了什么是AsyncLocalStorage、如何使用、Node 是如何实现的 AsyncHook等。不论你是0基础还是对此API有些了解的读者都比较适合阅读此文。(文末有活动)
|
6天前
|
JSON JavaScript 前端开发
❤Nodejs 第九章(token身份认证和express-jwt的安装认识)
【4月更文挑战第9天】Node.js第九章介绍了Token身份认证,特别是JWT(JSON Web Token)作为跨域认证的解决方案。JWT由Header、Payload和Signature三部分组成,用于在客户端和服务器间安全传输用户信息。前端收到JWT后存储在localStorage或sessionStorage中,并在请求头中发送。Express-JWT是一个中间件,用于解析JWT。基本用法包括设置secret和algorithms。注意安全问题,避免混合使用不同算法以防止降级攻击。
46 0
|
6天前
|
JavaScript Windows
Win7内网安装高版本的Node方法,亲测有效node-v16.16.0
Win7内网安装高版本的Node方法,亲测有效node-v16.16.0
70 0
|
6天前
|
JavaScript Linux 数据安全/隐私保护
node内网安装npm私服以及依赖包上传发布verdaccio
node内网安装npm私服以及依赖包上传发布verdaccio
119 1
|
6天前
|
移动开发 JavaScript 前端开发
为了学习vue3,安装nvm进行node的多版本管理
为了学习vue3,安装nvm进行node的多版本管理
23 2
|
6天前
|
资源调度 jenkins 持续交付
jenkins 自动安装nodejs16.16.0版本报错处理
jenkins 自动安装nodejs16.16.0版本报错处理
35 0
|
6天前
|
JavaScript 前端开发 Linux
Node.js安装与配置
`Node.js` 是一个基于 `Chrome V8` 引擎的 `JavaScript` 运行环境,它允许开发者使用 `JavaScript` 编写服务器端代码。以下是 `Node.js` 的安装与配置的详细步骤:
Node.js安装与配置
|
6天前
|
JSON JavaScript 关系型数据库
❤Nodejs 第十六章(Nodejs环境安装和依赖使用)
【4月更文挑战第16天】本文介绍了Node.js环境安装和项目搭建步骤。检查Node.js和npm版本安装核心依赖,如Express(Web应用框架)、MySQL库、CORS(解决跨域问题)、body-parser(解析POST数据)、express-jwt和jsonwebtoken(JWT验证)、multer(文件上传处理)、ws(WebSocket支持),以及可选的dotenv(管理环境变量)和ejs(模板引擎)。完整源码可在Gitee开源项目[nexusapi](https://gitee.com/lintaibai/nexusapi)中找到。
24 0
|
6天前
|
JavaScript Linux Python
Linux 安装 Node.js | NPM
Linux 安装 Node.js | NPM
11 0
|
6天前
node安装常用工具
node安装常用工具
9 0