本文将结合朴灵的《深入检出Node.js》以及部分node源码讲解一下node的事件循环。
在 node 的官方文档里面,把node的事件循环分成下面几个部分。首先这几个大的部分是没有问题的,但是如果大家按照官方描述的去理解,肯定与现实情况有出入,要想真正了解node的事件循环,建议看一下《深入检出Node.js》,然后结合着node的源码进行分析。
- 定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
- 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
- idle, prepare:仅系统内部使用。
- 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
- 检测:setImmediate() 回调函数在这里执行。
- 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。
接下来我们以 fs.open() 为例,看一下这个阶段都发生了什么
1. fs.open()
- 调用 lib/fs.js 里面的 open,这儿会创建一个很重要的req,后面会根据这个req创建变量对象
function open(path, flags, mode, callback) {
...
// 创建req,会根据这个变量创建请求对象
const req = new FSReqCallback();
// 将回调函数绑定在 onComplete 上面
req.oncomplete = callback;
// 调用 c/c++ 源码
binding.open(pathModule.toNamespacedPath(path),
flagsNumber,
mode,
req);
}
- 调用 src/node_file.cc 下面的 open(),首先判断环境,然后创建一个请求对象,根据不同的环境调用不同的 uv_fs_open 方法
static void Open(const FunctionCallbackInfo<Value>& args) {
// 环境判断
Environment* env = Environment::GetCurrent(args);
...
// 根据创建的 req 生成一个请求对象
FSReqBase* req_wrap_async = GetReqWrap(args, 3);
// 异步调用,调用对应平台下面的 uv_fs_open 方法
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);
...
}
}
- 如果是windows,就调用 deps/uv/src/win/fs.c 下面的 uv_fs_open()
int uv_fs_open(uv_loop_t* loop, uv_fs_t* req, const char* path, int flags,
int mode, uv_fs_cb cb) {
int err;
INIT(UV_FS_OPEN);
err = fs__capture_path(req, path, NULL, cb != NULL);
if (err) {
SET_REQ_WIN32_ERROR(req, err);
return req->result;
}
req->fs.info.file_flags = flags;
req->fs.info.mode = mode;
POST;
}
- 将请求对象(这个请求对象是存放回调函数,执行结果的重要媒介)放进线程池中等待
- 当线程中有可用线程时,调用响应的底层函数。
- js调用立即返回
2. 线程池做的工作
- 当线程可用时;(科补一下进程与线程的关系,最开始只有进程,因为进程的释放开销太大,又引入了线程,线程是进程的子集,进程主要用来占用一定资源,线程来跑程序)
- 执行请求对象中的I/O操作
- 将结果放到请求对象中
- 通知IOCP(这个是window的异步I/O模型,Unix对应的是自定义线程池)调用完成 -> 这个状态可以通过事件循环中的 GetQueuedCompletionStatus() 进行获取
- 归还线程
其实就是在处理完成的时候,通知IOCP调用完成,而整个过程,事件循环都在不停的获取状态是否已经完成
3. 事件循环
3.1. uv_run()
node事件循环的核心就是 uv_run() 函数,这个函数的内容如下
int uv_run(uv_loop_t *loop, uv_run_mode mode) {
DWORD timeout;
int r;
int ran_pending;
// 返回事件循环是否还活着的标志(根据是否还有的活跃的请求对象 或者 活跃的操作方法)
r = uv__loop_alive(loop);
if (!r)
uv_update_time(loop);
// 循环检查事件循环是否结束,下面这个循环就是事件循环
while (r != 0 && loop->stop_flag == 0) {
uv_update_time(loop); // 更新 loop 里面的时间
uv__run_timers(loop); // timer阶段根据 loop 里面的时间与回调的timeout进行比较,如果已经超时就执行回调函数
ran_pending = uv_process_reqs(loop); // pending阶段,将在这里面消耗 loop 中生成的回调队列
uv_idle_invoke(loop); // idle阶段
uv_prepare_invoke(loop); // prepare阶段
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
// 取出下一个定时器的过期时间与当前loop时间的时间差 `diff = handle->timeout - loop->time;`
timeout = uv_backend_timeout(loop);
// 这儿其实是一个性能的选择
if (pGetQueuedCompletionStatusEx)
uv__poll(loop, timeout); // poll阶段
else
uv__poll_wine(loop, timeout);
uv_check_invoke(loop); // check阶段
uv_process_endgames(loop); // 关闭的回调函数阶段,loop里面得到的队列将在这里面执行
// 如果模式是只执行一次
if (mode == UV_RUN_ONCE) {
uv__run_timers(loop);
}
// 再次判断事件循环是否活着
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
我们可以看到所谓的事件循环就是一个while(),不停的循环,依次此次执行每个阶段,这儿要注意的就是 poll 阶段。接下来我们分别看一下每个阶段
3.2 这个是定时器阶段
主要调用了 uv__run_timers(loop)
,做了如下操作:依次取出事件堆里的最小的定时器,如果已经过期了,就马上执行对应的回调;如果没有过期或者定时器已经取完了,那么就跳出循环
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
// 取出时间堆里面最小的一个
heap_node = heap_min(timer_heap(loop));
// 如果时间堆里面没有内容了,说明定时器的回调已经执行完了
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
// 因为定时器没有过期就直接跳出
if (handle->timeout > loop->time)
break;
// 移除定时器
uv_timer_stop(handle);
// 针对 setInterval 的情况
uv_timer_again(handle);
// 执行对应的回调函数
handle->timer_cb(handle);
}
}
3.3 pending阶段
调用了uv_process_reqs(loop)
这个函数的目的就是消耗 poll 产生的队列
INLINE static int uv_process_reqs(uv_loop_t* loop) {
uv_req_t* req;
uv_req_t* first;
uv_req_t* next;
if (loop->pending_reqs_tail == NULL)
return 0;
// pending_reqs_tail 队列就是 poll 阶段产生的队列(但是绝不仅是 poll 阶段产生的,还包括其他操作产生的一些内容也会在这里面)
// 取出队列中的第一个元素
first = loop->pending_reqs_tail->next_req;
next = first;
loop->pending_reqs_tail = NULL;
while (next != NULL) {
req = next;
next = req->next_req != first ? req->next_req : NULL;
// 根据请求对象不同的 type 执行不同的内容
switch (req->type) {
case UV_READ:
DELEGATE_STREAM_REQ(loop, req, read, data);
break;
...
case UV_POLL_REQ:
uv_process_poll_req(loop, (uv_poll_t*) req->data, req);
break;
default:
assert(0);
}
}
return 1;
}
这儿的 uv_process_poll_req 函数很重要,poll阶段产生的回调都将在这儿执行
void uv_process_poll_req(uv_loop_t* loop, uv_poll_t* handle, uv_req_t* req) {
if (!(handle->flags & UV_HANDLE_POLL_SLOW)) {
uv__fast_poll_process_poll_req(loop, handle, req);
} else {
uv__slow_poll_process_poll_req(loop, handle, req);
}
}
3.4 轮询阶段
uv__poll(loop, timeout)
- 轮询阶段是一个 for 循环,
- 首先是通过 pGetQueuedCompletionStatusEx/GetQueuedCompletionStatus 获取 iocp 里面是否有 io 完成的状态,
- 如果有,那么就遍历将所有的请求对象都添加到一个队列里面,然后结束loop阶段
- 如果没有,就会检查传进来的timeout是否大于0,然后判断计算的过期时间(timeout_time = loop->time + timeout)是否大于当前的loop时间, 如果大于,那么更新timeout,然后continue
- 也就是如果还有定时器,那么loop循环将在最小的定时器有过期时间的时候结束整个循环
static void uv__poll(uv_loop_t* loop, DWORD timeout) {
BOOL success;
uv_req_t* req;
OVERLAPPED_ENTRY overlappeds[128];
ULONG count;
ULONG i;
int repeat;
uint64_t timeout_time;
// 重新计算得出过期时间(当前loop时间 + diff时间)
timeout_time = loop->time + timeout;
// 这儿就是一直循环,直到有满足退出条件的时候退出
for (repeat = 0; ; repeat++) {
// 查看iocp里面是否有io完成的状态
success = pGetQueuedCompletionStatusEx(loop->iocp,
overlappeds,
ARRAY_SIZE(overlappeds),
&count,
timeout,
FALSE);
// 如果有完成的
if (success) {
for (i = 0; i < count; i++) {
if (overlappeds[i].lpOverlapped) {
req = uv_overlapped_to_req(overlappeds[i].lpOverlapped);
uv_insert_pending_req(loop, req); // 插入到一个队列里面 pending_reqs_tail ,在里面没有执行回调的逻辑
}
}
uv_update_time(loop); // 更新loop的时间
} else if (GetLastError() != WAIT_TIMEOUT) {
/* Serious error */
uv_fatal_error(GetLastError(), "GetQueuedCompletionStatusEx");
} else if (timeout > 0) {
uv_update_time(loop);
// 如果没有到过期时间,那么就continue
if (timeout_time > loop->time) {
timeout = (DWORD)(timeout_time - loop->time);
timeout += repeat ? (1 << (repeat - 1)) : 0;
continue;
}
}
// 结束 loop
break;
}
}
- 轮询阶段 uv__poll_wine(loop, timeout); 这个方法和 uv__poll 的区别在于,查看是否有完成状态的方式用的是 GetQueuedCompletionStatus ,同时一次也就读出一个。
3.5 check 阶段
调用了uv_check_invoke(loop)
,主要执行 setImmediate 里面的回调
void uv_check_invoke(uv_loop_t* loop) {
uv_check_t* handle;
(loop)->next_check_handle = (loop)->name##_handles;
// 遍历 check_handle 链表,然后执行回调,这里面的回调都是
// 在调用 setImmediate 执行 uv_check_init()/uv_check_start 时加入进去的
while ((loop)->next_check_handle != NULL) {
handle = (loop)->next_check_handle;
(loop)->next_check_handle = handle->name##_next;
handle->name##_cb(handle);
}
}
- 我们看一下 setImmediate 的定义,这里面会在 uv_check_init 加入回调
void* SetImmediate(napi_env env, T&& cb) {
T* ptr = new T(std::move(cb));
uv_loop_t* loop = nullptr;
uv_check_t* check = new uv_check_t;
check->data = ptr;
NAPI_ASSERT(env,
napi_get_uv_event_loop(env, &loop) == napi_ok,
"can get event loop");
uv_check_init(loop, check); // 对父类进行初始化,同时将回调相关放到一个 handle_queue 队列里面
uv_check_start(check, [](uv_check_t* check) { // 增加一些标记,计数器等等
...
return nullptr;
}
3.6 关闭的回调函数阶段
调用了uv_process_endgames(loop)
,主要做一些重置操作,代码如下
INLINE static void uv_process_endgames(uv_loop_t* loop) {
uv_handle_t* handle;
// endgame_handles 是一个队列
while (loop->endgame_handles) {
// 获取队列中的第一个
handle = loop->endgame_handles;
// 指向队列中的下一个
loop->endgame_handles = handle->endgame_next;
handle->flags &= ~UV_HANDLE_ENDGAME_QUEUED;
// 根据不同的type,执行不同的函数
switch (handle->type) {
...
case UV_POLL:
// 这儿是poll相关
uv_poll_endgame(loop, (uv_poll_t*) handle);
break;
case UV_TIMER:
uv__timer_close((uv_timer_t*) handle); // 移除对里面的定时器
uv__handle_close(handle); // 执行结束回调
break;
...
default:
assert(0);
break;
}
}
}