2018/5/8 update:
It is only two months since I finished writing this article, but I have almost forgotten all kinds of details (things that are not commonly used will be forgotten immediately, and my brain will automatically make space for other things due to insufficient memory). If you have any questions, I may not be able to answer them, I am very sorry. In addition, I think it is not too meaningful to go into this kind of thing. It is better to learn more general knowledge with higher value (such as algorithm, database principle, operating system principle, TCP/IP protocol, architecture design, probability statistics of high number line generation, etc.).
Different Event loops
Event Loop is an execution model with different implementations in different places. Browsers and NodeJS implement their own event loops based on different technologies. There is a lot of information about it on the Internet, but most of it is browser-based. There is not much about the NodeJS event loop, or even a lot of things that equate the browser and nodeJS event loop. I think there are two things to discuss event loop:
- Nodejs and the event loop of the browser are two distinct things that should not be confused.
- Second, when discussing the execution order of some asynchronous JS code, base it on node’s source code rather than your own imagination.
In a nutshell,
- Nodejs events are based on Libuv, while browser event loops are clearly defined in the HTML5 specification.
- Libuv has made an implementation of event Loop, while the HTML5 specification only defines the model of event loop in the browser, and the specific implementation is left to the browser manufacturer.
Event loop in NodeJS
There are two references to nodeJS event loops: one is the official NodeJS documentation; The other is the official Libuv documentation, which has a more complete description of NodeJS, while the latter has more details. Nodejs is evolving rapidly and the source code is changing a lot. The following discussion is based on NodeJS 9.5.0.
(However, nodeJS event loop seems to be more complex than expected. In the process of checking the nodeJS source code, I was surprised to find that some stages of nodeJS event loop will also run tasks from v8 micro Task Queue. Nodejs’s browser event loop has some associations that we’ll discuss later, but we’ll focus on the main ones for now.
6 Phases of event Loop
Nodejs’s Event loop is divided into six phases, each of which performs the following functions: (Process.nexttick () is executed at the end of each of the six phases. The latter part of the article will analyze in detail how the event loop is introduced in the process.Nexttick () callback. You can’t tell how process.nexttick () is involved just from uv_run() :
- Timers: perform
setTimeout()
和setInterval()
Callback due in. - I/O Callbacks: A small number of I/ OCallbacks in the previous cycle are delayed until this stage of the cycle
- Idle, prepare: Used internally only
- Poll: The most important phase, performing I/O callback, will block in this phase under appropriate conditions
- Check: Performs the Callback of setImmediate
- Close Callbacks: Callback to perform a close event, for example
socket.on("close",func)
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ I/O callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ├ ──┤ close callbacks ────── our r companyCopy the code
Each loop of the Event Loop needs to pass through the above phases in turn. Each stage has its own callback queue. When it enters a certain stage, callback will be fetched from the queue to be executed. When the queue is empty or the number of callback executed reaches the maximum number in the system, the next stage will be entered. The completion of all six phases is called a cycle.
The core code of event Loop is in deps/uv/ SRC/Unix /core.c
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; /* From uv__loop_alive we know that the event loop continues under one of the following conditions: 1, active handles (libuv defines handles as long-lived objects, such as TCP server) 2, active Request 3, Loop closing_handles */ r = uv__loop_alive(loop); if (! r) uv__update_time(loop); while (r ! = 0 && loop->stop_flag == 0) { uv__update_time(loop); // Update the time variable, which is used in uv__run_timers(loop); // Timers ran_pending = uv__run_pending(loop); Ran_pending indicates whether the queue is empty. Uv__run_idle (loop); / / idle phase uv__run_prepare (loop); // Prepare phase timeout = 0; The poll timeout is set to 0 in the following cases, which means that the poll timeout is not blocked. The poll timeout is set to 0 in the following cases, which means that the poll timeout is not blocked. **/ if ((mode == UV_RUN_ONCE &&!); if ((mode == UV_RUN_ONCE &&!); ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); uv__io_poll(loop, timeout); / / poll phase uv__run_check (loop); / / check phase uv__run_closing_handles (loop); // If mode == UV_RUN_ONCE (meaning the process continues), timers are checked once after all phases, If (mode == UV_RUN_ONCE) {uv__update_time(loop); if (mode == UV_RUN_ONCE) {uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } if (loop->stop_flag ! = 0) loop->stop_flag = 0; return r; }Copy the code
I’ve commented on the important parts, and you can see from the code above that the six stages of the Event loop are executed in sequence. It is worth noting that in UV_RUN_ONCE mode, the Timers stage is given one more chance to execute before the current loop ends.
Timers phase
The code for the timer stage is in uv__run_timers() in deps/uv/ SRC/Unix /timer.c
void uv__run_timers(uv_loop_t* loop) { struct heap_node* heap_node; uv_timer_t* handle; for (;;) { heap_node = heap_min((struct heap*) &loop->timer_heap); If (heap_node == NULL) break; Handle = container_of(heap_node, uv_timer_t, heap_node); handle = container_of(heap_node, uv_timer_t, heap_node); If (handle->timeout > loop->time)// If (handle->timeout > loop->time)// If (handle->timeout > loop->time) uv_timer_stop(handle); // Remove the handle uv_timer_again(handle); // If handle is of type repeat, insert handle->timer_cb(handle); // Execute callback on handle}}Copy the code
From the above logic, the timer phase uses a minimum heap instead of a queue to hold all elements (which makes sense, since the timeout callback is called in the order of the timeout, not the fifo queue logic), and then loops out all expired callback executions.
I/O callbacks stage
The code for the I/O callbacks stage is in int uv__run_pending() of deps/ UV/SRC/Unix /core.c
static int uv__run_pending(uv_loop_t* loop) { QUEUE* q; QUEUE pq; uv__io_t* w; If (QUEUE_EMPTY(&loop->pending_queue))// If QUEUE_EMPTY(&loop->pending_queue)) QUEUE_MOVE(&loop->pending_queue, &pq); // Move queue while (! QUEUE_EMPTY(&pq)) { q = QUEUE_HEAD(&pq); QUEUE_REMOVE(q); QUEUE_INIT(q); W = QUEUE_DATA(q, uv__io_t, pending_queue); w->cb(loop, w, POLLOUT); // Call callbak until the queue is empty} return 1; }Copy the code
According to the Libuv documentation, some callbacks that should have been executed during the poll phase of the previous cycle are deferred to the I/O Callbacks phase of the cycle for some reason. In other words, callbacks executed at this stage are remnants of previous rounds.
Idle and Prepare
Uv__run_idle (), uv__run_prepare(), and uv__run_check() are defined in the file deps/uv/ SRC/Unix /loop-watcher.c. Their logic is very similar, and their implementation makes use of a large number of macros (I personally hate macros, to be sure, It’s really poorly readable, and using macros for that bit of performance is questionable).
void uv__run_##name(uv_loop_t* loop) { \ uv_##name##_t* h; \ QUEUE queue; \ QUEUE* q; \ QUEUE_MOVE(&loop->name##_handles, &queue); // Replace the old head node with a new head node, which is equivalent to moving the old queue to the new queue \ while (! QUEUE_EMPTY(&queue)) {// If the new queue is not empty \ q = QUEUE_HEAD(&queue); / / remove the first element \ h = new queue QUEUE_DATA (q, uv_ # # # name# _t, queue); // Get handle \ QUEUE_REMOVE(q) pointed to in the first element; QUEUE_INSERT_TAIL(&loop->name##_handles, q); \ h->name##_cb(h); // Execute the corresponding callback \} \}Copy the code
Poll phase
The code + comments in the poll stage are as high as 200 lines, which is not easy to analyze line by line. We selected some important codes
void uv__io_poll(uv_loop_t* loop, int timeout) { //... // Process the observer queue while (! QUEUE_EMPTY(&loop->watcher_queue)) { //... if (w->events == 0) op = UV__EPOLL_CTL_ADD; Else op = UV__EPOLL_CTL_MOD; // Modify this event} //... If (no_epoll_wait!) {if (no_epoll_wait! = 0 || (sigmask ! = 0 && no_epoll_pwait == 0)) { nfds = uv__epoll_pwait(loop->backend_fd, events, ARRAY_SIZE(events), timeout, sigmask); if (nfds == -1 && errno == ENOSYS) no_epoll_pwait = 1; } else { nfds = uv__epoll_wait(loop->backend_fd, events, ARRAY_SIZE(events), timeout); if (nfds == -1 && errno == ENOSYS) no_epoll_wait = 1; } / /... for (i = 0; i < nfds; i++) { if (w == &loop->signal_io_watcher) have_signals = 1; else w->cb(loop, w, pe->events); // execute callback} //... }Copy the code
It can be seen that the task of the poll phase is to block and wait for the listening event to come, and then execute the corresponding callback. The blocking has a timeout, which is 0 in each of the following cases
- Uv_run is in UV_RUN_NOWAIT mode
uv_stop()
Is called- No active handles and Request
- There are active idle handles
- You have handles that are waiting to close
If none of the above is true, the timeout time is the nearest timer. Without a timer, the poll phase would block forever
The check phase
See idle and Prepare phases above
The close phase
static void uv__run_closing_handles(uv_loop_t* loop) { uv_handle_t* p; uv_handle_t* q; p = loop->closing_handles; loop->closing_handles = NULL; while (p) { q = p->next_closing; uv__finish_close(p); p = q; }}Copy the code
This code is very simple, just loop through closing handles, needless to say. The callback calls are in uv__finish_close()
Process. NextTick in where
The documentation mentions that process.Nexttick () does not belong to any of the above phases and runs at the end of each phase. But uv_run() is a function that runs six phases in sequence, with no process.nexttick () shadow. This problem should be explained from two c++ and js source level.
Implementation of process.nextTick at JS level
Process. nextTick is implemented in next_tick.js
function nextTick(callback) { if (typeof callback ! == 'function') throw new errors.TypeError('ERR_INVALID_CALLBACK'); if (process._exiting) return; var args; switch (arguments.length) { case 1: break; case 2: args = [arguments[1]]; break; case 3: args = [arguments[1], arguments[2]]; break; case 4: args = [arguments[1], arguments[2], arguments[3]]; break; default: args = new Array(arguments.length - 1); for (var i = 1; i < arguments.length; i++) args[i - 1] = arguments[i]; } push(new TickObject(callback, args, getDefaultTriggerAsyncId())); // Wrap callback as an object into a queue}Copy the code
There’s no magic to it, and it doesn’t call any of the functions provided by C++, but simply encapsulates all callbacks as objects and queues them. The callback is executed in _tickCallback()
function _tickCallback() { let tock; do { while (tock = shift()) { const asyncId = tock[async_id_symbol]; emitBefore(asyncId, tock[trigger_async_id_symbol]); if (destroyHooksExist()) emitDestroy(asyncId); const callback = tock.callback; if (tock.args === undefined) callback(); // Execute callback placed in when calling process.nexttick () else Reflect.apply(callback, undefined, tock.args); // Execute the callback emitAfter(asyncId) placed when calling process.nexttick (); } runMicrotasks(); // Microtasks will execute at this point, such as Promise} while (head.top! == head.bottom || emitPromiseRejectionWarnings()); tickInfo[kHasPromiseRejections] = 0; }Copy the code
As you can see, _tickCallback() loops through all the callbacks in the queue. Note the timing of microTasks, so execution of _tickCallback() means execution of the process.nexttick () callback. We continue to search and find that _tickCallback() is called in several places, but we’ll focus only on those related to event loop. Found in next_tick.js
const [
tickInfo,
runMicrotasks
] = process._setupNextTick(_tickCallback);Copy the code
A lookup of the delivery is now available in Node. cc
env->SetMethod(process, "_setupNextTick", SetupNextTick); // Expose _setupNextTick to jsCopy the code
_setupNextTick() is the method exposed in Node.cc, so it is assumed that this is the bridge to the Event loop.
The process.nextTick callback is performed in c++
Find the SetupNextTick() function in Node.cc, which has this code snippet
void SetupNextTick(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); CHECK(args[0]->IsFunction()); Env ->set_tick_callback_function(args[0].as <Function>()); . }Copy the code
When will _tickCallback be called if it is placed inside env? This is also found in Node.cc
void InternalCallbackScope::Close() { if (! tick_info->has_scheduled()) { env_->isolate()->RunMicrotasks(); } / /... If (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) { env_->tick_info()->set_has_thrown(true); failed_ = true; }}Copy the code
Known InternalCallbackScope: : Close () will call it, and InternalCallbackScope: : Close () in the file node. The cc InternalMakeCallback () is invoked
MaybeLocal<Value> InternalMakeCallback(Environment* env, Local<Object> recv, const Local<Function> callback, int argc, Local<Value> argv[], async_context asyncContext) { CHECK(! recv.IsEmpty()); InternalCallbackScope scope(env, recv, asyncContext); / /... scope.Close(); //Close calls _tickCall //... }Copy the code
InternalMakeCallback() is called in async_wrap.cc AsyncWrap::MakeCallback()
MaybeLocal<Value> AsyncWrap::MakeCallback(const Local<Function> cb, int argc, Local<Value>* argv) {MaybeLocal<Value> ret = InternalMakeCallback(env(), object(), cb, argc, argv, context); }Copy the code
The AsyncWrap class encapsulates asynchronous operations. It is a top-level class that inherits TimerWrap, TcpWrap, and other classes that encapsulate asynchronous operations. This means that these classes call MakeCallback() when encapsulating asynchronous operations. The fact is that the callback in uv_run() is wrapped with AsyncWrap::MakeCallback(), so the process.nexttick () callback will be executed after the callback completes, as described in the documentation. Tidy up the process by which _tickCallback() is transferred and eventually called
In the aspect of js
Process._setupnexttick (_tickCallback) // a bridge between c++ and js, passing the callback to c++ for executionCopy the code
At this point _tickCallback() is transferred to the C++ level, where it is first stored in env
Env ->SetMethod(process, "_setupNextTick", SetupNextTick); // Call process._setupNextTickCopy the code
The _tickCallback() stored in env is called as follows:
Env_ - > tick_callback_function () / / remove _tickCallback perform left InternalCallbackScope: : Close () / / call the former left InternalMakeCallback () / / call the former ↓ AsyncWrap::MakeCallback()// Call the former ↓ is inherited by multiple classes that encapsulate asynchronous operations and the ↓ call is executed by uv_run() to implement the callback provided by process.nexttick after each phaseCopy the code
The analysis of the whole process is rather rough, and many details are not found behind, but you can complete other details from the following resources. For example, the entire execution process of timer can be seen in./lib/timers.js to see the underlying implementation of timers related API, which is a good complement to what I did not mention.
The resources
Due to node’s rapid growth, much of the early source code analysis is outdated (the directory structure of the source code has changed or the implementation code has changed), but it’s still instructive.
- Event Loop in JavaScrip: the most critical article that inspires me to find the answer
- Developing customizable applications with Google V8: this article introduces the V8 engine’s method of exposing C++ objects to js, which is very helpful for reading node source code
- In-depth understanding of Node.js: Core ideas and source code analysis: A comprehensive analysis of Node source code, based on Node 6.0
- Node source rough reading series: based on 9.0 source analysis, very detailed and keep up with the latest changes
- Node.js mining series
- Node source code detailed series