理解Nodejs的单线程实现高并发原理

组成和架构

Nodejs 的特点是事件驱动、非阻塞I/O、高效、轻量。

我们首先看下 Nodejs 的架构。

最上层的是 Nodejs标准库,由JavaScript实现的api库,位置在 lib 目录。

然后是 Node bindings,JavaScript 和 C/C++ 之间通信和交换数据的桥梁,实现于 node.cc。

最下层是由 C/C++ 实现:

        1. V8 解析引擎,为 Javascript 提供了在非浏览器端运行的环境;

        2. Libuv为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力。

        3.  C-ares提供了异步处理 DNS 相关的能力

        4. http_parser、OpenSSL、zli 等,提供包括 http 解析、SSL、数据压缩等能力。

比如,使用 Nodejs标准库与操作系统交互。

编写JavaScript代码:

import fs from 'fs';
fs.open('./test.txt', "w", function(err, fd) {
    //..do something
});

当我们调用 fs.open 时:

/**  lib/fs.js */
async function open(path, flags, mode) {
  mode = modeNum(mode, 0o666);
  path = getPathFromURL(path);
  validatePath(path);
  validateUint32(mode, 'mode');
  return new FileHandle(
    await binding.openFileHandle(pathModule.toNamespacedPath(path),
             stringToFlags(flags),
             mode, kUsePromises));
}

nodejs 通过 process.binding 调用 C/C++ 层面的 Open 函数:

/** src/node_file.cc */
static void Open(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  const int argc = args.Length();
  if (req_wrap_async != nullptr) {  // open(path, flags, mode, req)
    AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,
              uv_fs_open, *path, flags, mode);
  } else {  // open(path, flags, mode, undefined, ctx)
    CHECK_EQ(argc, 5);
    FSReqWrapSync req_wrap_sync;
    FS_SYNC_TRACE_BEGIN(open);
    int result = SyncCall(env, args[4], &req_wrap_sync, "open",
                          uv_fs_open, *path, flags, mode);
    FS_SYNC_TRACE_END(open);
    args.GetReturnValue().Set(result);
  }
}

然后通过它调用 Libuv 中的具体方法 uv_fs_open:

/** uv_fs */
/* 打开目标文件 */
 dstfd = uv_fs_open(NULL,
                     &fs_req,
                     req->new_path,
                     dst_flags,
                     statsbuf.st_mode,
                     NULL);
 uv_fs_req_cleanup(&fs_req);

最后执行的结果通过回调的方式传回,完成流程。

上面是调用流程图,在 Javascript 中调用的方法,最终都会通过 process.binding 传递到 C/C++ 层面,最终由它们来执行真正的操作。Node.js 就是这样与操作系统进行交互的。

单线程并发

Node.js 对http 服务的模型:

在nodejs中,单线程指的是主线程是单线程,由主线程去按顺序执行代码,可以验证,当遇到同步代码阻塞,会导致主线程占用,程序会被暂时卡住, 打开浏览器,会发现在 5 秒之后才做出反应:

import http from 'http';

function sleep(time) {
    const exitCondition = Date.now() + time * 1000;
    while(Date.now() < exitCondition) {}
    return;
}

cosnt server = http.createServer(function(req, res){
    sleep(5);
    res.end('server 休眠 5s');
});

server.listen(8080);

代码执行堆栈如下:

当主线程接受了 request 后,程序被压进同步执行的 sleep 执行块,如果此时有第二个request进来就会被压进stack里面等待上一个执行完成后再进一步处理下一个请求,后面的请求都会被挂起等待前面的同步执行完成后再执行。

那 Nodejs是如何能做到 百万级并发的呢 ? — —  事件驱动/事件循环(Event Loop。事件循环/事件驱动是一种在程序中等待和分派事件或消息的编程结构。

1.  一个Nodejs进程只有一个执行栈execution context stack)的主线程(单线程);

2. 主线程之外,维护了一个事件队列(Event Queue),当存在网络请求或者其它的异步操作到时,nodejs 都会把它放到 Event Queue 之中,不立即执行因此不阻塞主线程。

3. 主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,从线程池分配线程执行 Event Queue 的事件。主线程则负责不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了。而且,当某个事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。

4. 主线程不断重复上面的第3步。

因此,主线程只负责不断的往返调度,由内部线程池进行异步操作,并没有进行真正的I/O操作,从而实现异步非阻塞I/O。这方面由 libuv 实现,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API。 此外,如果操作系统提供了异步 I /O 的接口,libuv内部会优先考虑使用这些现成的API接口来完成异步I/O,而不是使用线程池中的线程 + 轮询来实现异步I/O

在 src/node.cc 中:

Environment* CreateEnvironment(Isolate* isolate, uv_loop_t* loop, Handle<Context> context, int argc, const char* const* argv, int exec_argc, const char* const* exec_argv) {
  HandleScope handle_scope(isolate);

  Context::Scope context_scope(context);
  Environment* env = Environment::New(context, loop);

  isolate->SetAutorunMicrotasks(false);

  uv_check_init(env->event_loop(), env->immediate_check_handle());
  uv_unref(reinterpret_cast<uv_handle_t*>(env->immediate_check_handle()));
  uv_idle_init(env->event_loop(), env->immediate_idle_handle());
  uv_prepare_init(env->event_loop(), env->idle_prepare_handle());
  uv_check_init(env->event_loop(), env->idle_check_handle());
  uv_unref(reinterpret_cast<uv_handle_t*>(env->idle_prepare_handle()));
  uv_unref(reinterpret_cast<uv_handle_t*>(env->idle_check_handle()));

  // Register handle cleanups
  env->RegisterHandleCleanup(reinterpret_cast<uv_handle_t*>(env->immediate_check_handle()), HandleCleanup, nullptr);
  env->RegisterHandleCleanup(reinterpret_cast<uv_handle_t*>(env->immediate_idle_handle()), HandleCleanup, nullptr);
  env->RegisterHandleCleanup(reinterpret_cast<uv_handle_t*>(env->idle_prepare_handle()), HandleCleanup, nullptr);
  env->RegisterHandleCleanup(reinterpret_cast<uv_handle_t*>(env->idle_check_handle()), HandleCleanup, nullptr);

  if (v8_is_profiling) {
    StartProfilerIdleNotifier(env);
  }

  Local<FunctionTemplate> process_template = FunctionTemplate::New(isolate);
  process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "process"));

  Local<Object> process_object = process_template->GetFunction()->NewInstance();
  env->set_process_object(process_object);

  SetupProcessObject(env, argc, argv, exec_argc, exec_argv);
  LoadAsyncWrapperInfo(env);

  return env;
}

建立了一个 nodejs 执行 V8 环境,会把 libuv 默认 default_loop_struct,即uv _default_loop() 返回的作为参数传递进去的,之后,Node会载入执行环境并完成一些设置操作,然后启动 event loop:

{
    SealHandleScope seal(isolate);
    bool more;
    env.performance_state()->Mark(
        node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START);
    do {
      uv_run(env.event_loop(), UV_RUN_DEFAULT);

      v8_platform.DrainVMTasks(isolate);

      more = uv_loop_alive(env.event_loop());
      if (more)
        continue;

      RunBeforeExit(&env);

      // Emit `beforeExit` if the loop became alive either after emitting
      // event, or after running some callbacks.
      more = uv_loop_alive(env.event_loop());
    } while (more == true);
    env.performance_state()->Mark(
        node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
  }

  env.set_trace_sync_io(false);

  const int exit_code = EmitExit(&env);
  RunAtExit(&env);

more 用来标识是否进行下一轮循环。 env -> event_loop()会返回之前保存在env中的default_loop_ptr,uv_run 函数将以指定的 UV_RUN_DEFAULT 模式启动 libuv 的event loop。如果当前没有 I/O 事件也没有定时器事件,则 uv_loop_alive 返回 false。

Event Loop 是被 V8 所使用一个功能模块。因此,可以说,V8 包含了 Event Loop。每次事件循环都包含了6个阶段,对应到 libuv 源码中的实现,如下图所示:

Event Loop会依次进入上述的每个阶段。每个阶段都会有一个 callback queue 与之相对应。Event Loop会遍历这个 callback queue,执行里面的每一个callback。直到 callback queue 为空或者当前callback的执行数量超过了某个阈值为止,Event Loop才会移步到下一个阶段。

  • timers:这个阶段执行 timer(setTimeoutsetInterval)的回调,调用 setTimeout 或者 setInterval 方法时传入的 callback 会在指定的延迟时间后入队到 timers callback queue。跟浏览器环境中的 setTimeout 和 setInterval 方法一样,调用时候传入的延迟时间并不是回调确切执行的时间,会受到操作系统调度层面和其他callback函数调用耗时的影响,timer callback函数的执行只会比预定的时间晚
  • pending callbacks:执行一些系统调用错误,比如网络通信的错误回调。
  • idle, prepare:仅 node 内部使用,开发者可忽略。
  • poll:检索新的 I/O 事件; 执行与 I/O 相关的回调(几乎所有回调,除了 close callback 、定时器调度的 callback 和 setImmediate);nodejs 会在这里适当的阻塞。
  • check:执行 setImmediate()的回调,当 poll 处于空闲状态的时候(也就是 I/O callback queue 为空的时候),一旦 Event Loop发现 immediate callback queue 有 callback入队了,Event Loop 就会退出轮询阶段,马上进入check 阶段。
  • close callbacks:执行 socket 的 close 事件回调。

 uv_run 内部就是一个 while 循环,在 UV_RUN_ONCE 和 UV_RUN_NOWAIT 两种模式下,循环在执行一次后就会 break。另外 在 uv__loop_alive(loop) == 0 或者 loop->stop_flag != 0 时 无法进入循环,同样循环结束,uv_run 函数返回:

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

  // 首先检查我们的loop还是否活着, 即 loop 中是否有异步任务,若没有直接就结束
  r = uv__loop_alive(loop);
  if (!r) uv__update_time(loop);
  // 事件循环,外部大循环
  while (r != 0 && loop->stop_flag == 0) {
    // 更新事件阶段
    uv__update_time(loop);

    // 处理timer回调
    uv__run_timers(loop);

    // 处理异步任务回调 
    ran_pending = uv__run_pending(loop);

    // node 内部调用的,不用关注
    uv__run_idle(loop);
    uv__run_prepare(loop);

    // poll 阶段, timeout 用于控制 uv__io_poll(loop, timeout) 的挂起时长,
    timeout = 0;
    // uv_backend_timeout 计算完毕后,传递给uv__io_poll
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop);

    // 如果timeout = 0, 则 uv__io_poll会直接跳过
    uv__io_poll(loop, timeout);

    // check 阶段,执行 setImmediate
    uv__run_check(loop);

    // close callback 阶段,关闭文件描述符等操作
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      /* UV_RUN_ONCE 意味着向前进展:当它返回时必须至少调用一个回调。 
       * uv__io_poll() 可以在它的超时到期时不做 I/O 就返回(意思是:没有回调)
       * 这意味着我们有满足“前向进度约束”的挂起计时器。
       *
       * UV_RUN_NOWAIT 不保证进度,因此不在if 的检查中
       */
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);

    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break;
  }

  /** if 语句让 gcc 将其编译为条件存储。 避免弄脏高速缓存行。*/
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

uv_backend_timeout 实现如下:

int uv_backend_timeout(const uv_loop_t* loop) {
  if (loop->stop_flag != 0)
    return 0;

  if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
    return 0;

  if (!QUEUE_EMPTY(&loop->idle_handles))
    return 0;

  if (!QUEUE_EMPTY(&loop->pending_queue))
    return 0;

  if (loop->closing_handles)
    return 0;

  return uv__next_timeout(loop);
}

1. stop_flag: 如果该标记非0,说明要退出轮询,返回的时间是0。

2. !uv__has_active_handles!uv__has_active_reqs: 如果没有任何的异步任务(包括timer和异步I/O),返回 0,退出轮询

3. QUEUE_EMPTY(idle_handles)QUEUE_EMPTY(pending_queue): 异步任务是通过注册的方式放进了pending_queue中,无论是否成功,都已经被注册,如果这两个队列空,返回 0,退出轮询。

4. closing_handles: 循环进入了关闭阶段,返回 0,也要退出轮询。

5. uv__next_timeout(loop) 实现如下。

int uv__next_timeout(const uv_loop_t* loop) {
  const struct heap_node* heap_node;
  const uv_timer_t* handle;
  uint64_t diff;

  heap_node = heap_min((const struct heap*) &loop->timer_heap);
  if (heap_node == NULL) return -1; /* 无限期封锁 */

  handle = container_of(heap_node, uv_timer_t, heap_node);
  if (handle->timeout <= loop->time) return 0;

  diff = handle->timeout - loop->time;

  if (diff > INT_MAX) diff = INT_MAX;

  return diff;
}

Event Loop本身有着以下的几个队列:

  • timer callback queue
  • I/O callback queue
  • immediate callback queue
  • close callback queue

还有两个队列值得我们注意:

  • nextTick callback queue。调用 process.nextTick() 时传入的 callback 会被入队到这里。
  • microtask callback queue。一个 promise 对象 reslove 或者 reject 时传入的 callback 会被入队到这里。

Event Loop 示意图:

当以上所有阶段被顺序执行一次后,Event Loop 完成了一个 Tick。 一旦进入事件循环之后,每执行完当前阶段的队列一个 callback 之后,就必须检查微任务队列。如果微任务有callback要执行,则需要执行完所有的 微任务 calback 之后才会回归到事件循环里面。

注意:在 node v11.15.0 之前(不包括本身)表现是:event loop执行完当前阶段 callback queue 里面的所有 callback 才会进入微任务队列。

DNS

DNS 模块使用了 异步DNS解析库 cares 和 Libuv 的线程池实现。cares 实现了 DNS 协议的封包和解析,由于通过 IP 查询域名或者域名查询 IP 是直接调用操作系统提供的是阻塞式的API,借助 Libuv 事件驱动机制和线程池,最终实现异步的 DNS 解析。除此之外,基于cares,Node.js 还提供了设置 DNS 服务器、新建一个 DNS 解析实例(Resolver)等功能。

发起一个查找操作的时候,Node.js 会往线程池提及一个任务,然后就继续处理其他事情,同时,线程池的子线程会调用底层函数做 DNS 查询,查询结束后,子线程会把结果交给主线程。

UDP

UDP 是传输层非面向连接的不可靠协议,使用 UDP 时,不需要建立连接就可以往对端直接发送数据,减少了三次握手带来的时延,但是 UDP 的不可靠可能会导致数据丢失,所以比较适合要求时延低,少量丢包不影响整体功能的场景,另外 UDP 支持多播、端口复用,可以实现一次给多个主机的多个进程发送数据。

发送一个 UDP 数据包的时候,Libuv 会把数据先插入等待发送队列,接着在 epoll 中注册等待可写事件,当可写事件触发的时候,Libuv 会遍历等待发送队列,逐个节点发送,成功发送后,Libuv 会把节点移到发送成功队列,并往 pending 阶段插入一个节点,在 pending 阶段,Libuv 就会执行发送完成队列里每个节点的会调通知调用方发送结束:

TCP

TCP 首先是启动服务器,等待连接建立:

  1. 首先获取一个 socket。
  2. 然后绑定地址到该 socket 中。
  3. 接着调用 listen 函数把该 socket 改成监听状态。
  4. 最后把该 socket 注册到 epoll 中,等待连接的到来。

建立连接后:

  • Node.js 会调用 accept 接下一个 TCP 连接。
  • 接着会调 C++ 层新建一个对象表示和客户端通信的实例。
  • 接着回调 JS 层,JS 也会新建一个对象表示通信的实例,主要是给用户使用。
  • 最后注册等待可读事件,等待客户端发送数据过来。

处理完一个连接后,Node.js 会判断是否设置了 single_accept 标记,如果有则睡眠一段时间,给其他进程处理剩下的连接,一定程度上避免负责不均衡,如果没有设置该标记,Node.js 会继续尝试处理下一个连接。

文件操作和监听

文件操作

Node.js 中文件操作分为同步和异步模式,同步模式就是在主进程中直接调用文件系统的 API,这种方式可能会引起进程的阻塞,异步方式是借助了 Libuv 线程池,把阻塞操作放到子线程中去处理,主线程可以继续处理其他操作。

同步模式流程:

 异步模式流程:

Node.js 中 文件复制基于 Copy-on-write(写时复制)技术实现的:

  • 复制只是添加一个引用到之前的内容,如果不修改并不会真正复制,只有到第一次修改内容的时候才去真正复制对应的数据块,这样就避免了大量硬盘空间的浪费。
  • 写文件时会先在另一个空闲磁盘块做修改,等修改完之后才会复制到目标位置,这样就不会有断电无法回滚的问题

copyFile 方法的第三个参数指定复制的策略(支持按位或把它们合并之后传入):

  • COPYFILE_EXCL: 如果目标文件已存在,会报错(默认是覆盖);
  • COPYFILE_FICLONE: 以 copy-on-write 模式复制,如果操作系统不支持就转为真正的复制(默认是直接复制);
  • COPYFILE_FICLONE_FORCE:以 copy-on-write 模式复制,如果操作系统不支持就报错。

文件监听

文件监听,比如我们修改了文件后 webpack 重新打包代码或者 Node.js 服务重启,都用到了文件监听的功能,Node.js 提供了两套文件监听的机制:基于轮询的文件监听机制和基于inotify的文件监听机制,后者 inotify 是基于订阅发布模式的,避免了无效的轮询。

基于轮询的文件监听机制流程:

基于inotify的文件监听机制流程:

Cluster

Cluster 模块使得单进程架构的 Node.js 支持多进程的服务器架构,更好的利用多核。Node.js 支持两种通常的多进程服务器架构:轮询(主进程 accept )和共享(子进程 accept )。前者是主进程处理连接,然后分发给子进程处理;后者是子进程共享 socket,通过竞争的方式获取连接进行处理。可以通过环境变量在Node.js中进行设置。

轮询模式:

共享模式: 

线、进程间通信

进程间通信

Node.js 中的进程是使用 fork+exec 模式创建的,fork 就是复制主进程的数据,exec 是加载新的程序执行。Node.js 提供了异步和同步创建进程两种模式。

进程在内存中分为代码段、数据段、堆栈段这 3 部分:

  • 代码段:存放要执行的代码
  • 数据段:存放一些全局数据
  • 堆栈段:存放执行的状态

fork 的实现就是一种 copy-on-write (写时复制)技术,fork 并不会真正的复制内存,而是创建一个新的进程,引用父进程的内存,当做数据的修改的时候,才会真正复制该部分的内存。而执行的新代码是使用 exec。

异步方式:

同步方式:

Node.js 选取的进程间通信方式是 Unix 域,因为只有 Unix 域支持文件描述符传递。

  1. Node.js 底层通过 socketpair 创建两个文件描述符,主进程拿到其中一个文件描述符,并且封装 send和 on meesage 方法进行进程间通信。
  2. 接着主进程通过环境变量把另一个文件描述符传给子进程。
  3. 子进程同样基于文件描述符封装发送和接收数据的接口。

线程间通信

Node.js 中多线程的架构:

Node.js 中创建线程的流程:

  • 主线程会首先创建创建两个通信的数据结构,接着往对端发送一个加载 JS 文件的消息。
  • 然后调用底层接口创建一个线程。
  • 这时候子线程就被创建出来了,子线程被创建后首先初始化自己的执行环境和上下文。
  • 接着从通信的数据结构中读取消息,然后加载对应的js文件执行,最后进入事件循环。

线程和进程不一样,进程的地址空间是独立的,不能直接通信,但是线程的地址是共享的,所以可以基于进程的内存直接进行通信。线程通信的具体流程:

  1. 线程 A 调用 postMessage 发送消息。
  2. postMessage 会先对消息进行序列化。
  3. 然后由线程 B 拿到对端消息队列的锁,并把消息插入对端消息队列中。
  4. 线程 A 成功发送插入消息后,还需要通知消息接收者所在的线程 B。
  5. 消息接收者(线程B)会在事件循环的 Poll IO 阶段处理这个消息。

AsyncHooks

async_hooks 模块是 Node.js 中提供了用于跟踪异步资源的 API。

异步资源表示有关联回调的对象。该回调可以被调用一次( fs.open()中)或多次调用( net.createServer 中的 'connection' 事件),也可以在调用回调之前关闭资源。AsyncHook 对这些不同情况进行抽象成异步资源。如果使用 Workers,每个线程都有一个独立的 async_hooks 接口,每个线程都会使用一组新的独立 asyncId。

追踪异步资源能清晰的确认任务之间的调用链和完整正确的异步回调调用栈。

当开启 asyncHook 的时候,每个异步资源都会触发这些生命周期钩子。

import async_hooks from 'node:async_hooks';

/** 返回当前执行上下文的 asyncId */
const eid = async_hooks.executionAsyncId();

/** 返回负责触发当前执行作用域的回调的 asyncID */
const tid = async_hooks.triggerAsyncId();

/** 创建一个新的 AsyncHook 实例。 所有这些回调都是可选的 */
const asyncHook =
    async_hooks.createHook({ init, before, after, destroy, promiseResolve });

/** 允许调用此 AsyncHook 实例的回调。这不是运行构造函数后的隐式操作,必须显式运行才能开始执行回调. */
asyncHook.enable();

/** 禁用监听新的异步事件 */
asyncHook.disable();

/** 以下是可以传递给 createHook() 的回调 */
/** init() 在对象构造期间被调用。 此回调运行时,资源可能尚未完成构造。 因此,“asyncId”引用的资源的所有字段可能都没有被填充。*/
/**
 * @params asyncId 异步资源的唯一 id,从 1 开始的自增
 * @param  type 标识异步资源的字符串
 * @param  triggerAsyncId 在其执行上下文中创建此异步资源的异步资源的 asyncId
 * @param  resource 对表示异步操作的资源的引用,有异步资源相关的数据,需要在destroy期间释放
 */
function init(asyncId, type, triggerAsyncId, resource) { }

/** 在调用资源的回调之前调用 before()。对于处理函数(例如 TCPWrap),它可以被调用 0-N 次,而对于请求(例如 FSReqCallback),它将被准确地调用 1 次。 */
function before(asyncId) { }

/** after() 在资源的回调完成后被调用。 */
function after(asyncId) { }

/** destroy() 在资源的消耗后被调用。 */
function destroy(asyncId) { }

/** 当调用传递给 Promise 构造函数的 resolve() 函数时(直接或通过其他解决 Promise 的方法),promise Resolve() 仅对 Promise 资源调用。*/
function promiseResolve(asyncId) { }

比如,用 AsyncHooks 确认任务之间的调用链,callback归属关系: 

const fs = require('fs')
const async_hooks = require('async_hooks');
const { fd } = process.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();

function callback(err, data) {
    console.log('callback', data)
}

fs.readFile("a.txt", callback)
console.log('after a')
fs.readFile("b.txt", callback)
console.log('after b')

/**
FSREQCALLBACK(4): trigger: 1 execution: 1      # a
after a
TickObject(5): trigger: 1 execution: 1
FSREQCALLBACK(6): trigger: 1 execution: 1      # b
after b
before:  5
after:  5

before:  4
callback undefined
  TickObject(7): trigger: 4 execution: 4        // trigger by a
after:  4

before:  7
after:  7

before:  6
callback undefined
  TickObject(8): trigger: 6 execution: 6       // trigger by b
after:  6

before:  8
after:  8
destroy:  5
destroy:  7
destroy:  4
destroy:  8
destroy:  6
*/

可以看到,a 的调用链路:1 -> 4 -> 7,b 的调用链路:1 -> 6 -> 8,所以第一个 callback 是 a,第二个 callback 是 b。

使用 AsyncHook 会带来一定的额外性能开销:

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值