What you need to know before you read

  • What are processes and threads, and what are the differences?
  • What’s the difference between single-threaded and multi-threaded?

Node. Js characteristics

  • Single thread

    Synchronization takes place, and only after the previous code has finished is it executed below. But Node.js programs are macroscopically parallel due to their non-blocking I/O and event-driven nature.

    Benefits: Reduced memory overhead, no more process creation, destruction overhead; It’s easy.

    Cons: One user crashes the thread and the entire service crashes.

  • Non-blocking I/O

    I/O blocks the execution of code, greatly reducing the efficiency of program execution.

    In non-blocking mode, a thread is always performing computations, and the CPU core utilization of that thread is always 100%.

  • event-driven

    In Node, only one event callback can be executed at a time, but other events (I/O events, network requests…) can be processed in the middle of a callback. These events take a lot of time. These asynchronous operations are first added to the back of the event queue and then returned to continue executing the original event callback function. This is called the event loop mechanism.

    Nearly half of the underlying C++ code is devoted to building event queues and callback function queues.

Applicable scenario

From the characteristics of Node.js, we can find that it is best at task scheduling. If a business excessively uses CPU for calculation, it actually blocks the single process, which is not suitable for Node.js development.

Node.js is best used when the application needs to handle a large amount of concurrent I/O, and there is no need for very complex processing inside the application before the response is sent to the client.

Node.js is also suitable for developing long-connected real-time applications with Web sockets.

Event loop

Execution stack and event queue

  • Execution stack

    When we call a method, JS generates an execution environment corresponding to the method. When a series of methods are called in sequence, js is single-threaded and only one method can be executed at a time, so the methods are queued in a separate place. This place is called the execution stack.

  • The event queue

    When we make an asynchronous request, the main thread does not wait for the result to return. Instead, it suspends the event and continues to perform other tasks in the stack. After the asynchronous task returns the result, the asynchronous task is added to another queue which is different from the execution stack according to the execution sequence. This queue is called the event queue.

From this we know that in the Node environment javascript runs on a single-threaded thread and event loops on a single-threaded thread, but the two are not the same thread.

JavaScript Event Loop mechanism is divided into browser Event Loop mechanism and Node Event Loop mechanism. Browser Event Loop is a specification defined in HTML, and Node Event Loop is implemented by libuv library. Let’s take a look at the browser event loop

Browser event loop

Event loops in browsers are primarily about understanding macro and micro tasks as asynchronous tasks.

  • MacroTasks

SetTimeOut, setInterval, setImmediate, I/O, various callback, UI rendering, messageChannel, etc.

Priority: Main code block > setImmediate > postMessage > setTimeOut/setInterval

  • Microtasks

Process. nextTick, Promise, MutationObserver, Async (essentially Promise)

Priority: process.nextTick > Promise > MutationOberser

Execute partition:

We often divide EventLoop into memory, execution stack, WebApi, and asynchronous callback queue (including microtask queue and macro task queue).In the first event loop, the JavaScript engine will execute the entire script code as a macro task. After the execution is completed, it will detect whether the microtask is found in this cycle. If so, it will read and execute all the microtasks from the task queue of the microtask in turn, and then read the task execution in the task queue of the macro task. Perform all the microtasks, and so on. The sequence of JS execution is macro – micro – task in each event loop.

Node.js event loop

Node.js uses V8 as the PARSING engine of JS, and uses libuv designed by itself for I/O processing. Libuv is an event-driven cross-platform abstraction layer, which encapsulates some low-level features of different operating systems and provides a unified API externally. The event loop mechanism is also implemented in it.

Based on the above figure, we can see that node.js operates in the following steps:

  • The JS code we write will be handed over to the V8 engine for processing.
  • The parsed code calls the NodeApi and is executed by the Libuv library.
  • Libuv assigns different tasks to different threads in an Event Loop.
  • When the task is processed, the execution results are asynchronously sent back to the V8 engine and back to us.

Node.js event loop principle

1. Timers: This stage performs the callback of timer (setTimeout, setInterval)

2.I/O Callbacks phase: Perform some system call errors, such as error callbacks for network communication

3. Idle, prepare: Used only internally

4. Poll phase: Fetch new I/O events, where node will block under appropriate conditions

5. Check phase: Perform the setImmediate() callback

6. Close Callbacks: Execute the close event callback of the socket

We focused on the timers, Poll, and Check phases as these are where most asynchronous tasks in daily development are handled.

  • Timers phase

Timers are the first phase of the event loop. Node checks for expired timers and pushes them back into the timer queue for execution. In fact, Node does not guarantee that the timer will run immediately after the preset time. Because Node’s timer expiration check may not be reliable, it may be affected by other programs running on the machine, or the main thread may not be idle at that point in time. In the code below, for example, setTimeout() and setImmediate() are not executed in an indeterminate order.

setTimeout(() = > {
  console.log('timeout')},0)

setImmediate(() = > {
  console.log('immediate')})Copy the code

But putting them in an I/O callback must lead to setImmediate(), because the poll phase is followed by the check phase.

  • Poll phase

    The poll stage has two main functions:

    1. Process events in the poll queue.

    2. When a timer has expired, execute its callback function.

The Event Loop synchronously executes the callbacks in the poll queue until the queue is empty or the system limits the number of callbacks performed. Then the event Loop checks for mediate().

1. With a preset setImmediate(), the Event loop terminates the poll phase into the Check phase and executes the task queue for the check phase.

2. Without a preset setImmediate(), the Event loop blocks and waits at that stage.

None of the setImmediate() techniques does not trigger the event loop to block in the poll phase.

Therefore, when executing in the poll phase, a timeout timeout is passed, which is the maximum blocking time for the poll phase. If an event is returned before timeout, the callback registered for that event is executed. Timeout Exits the poll phase and performs the next phase.

  • The check phase

The setImmediate() callback is added to the Check phase, which executes in the following order after the poll phase.

conclusion

  • Initialization of node

    • Initialize the Node environment.
    • Execute the input code.
    • Execute the process.nextTick callback.
    • Perform microtasks.
  • Enter the event loop

    • Entering the timers Stage

      • Check whether the timer queue has expired timer callbacks. If so, the expired timer callbacks are executed in ascending timerId order.
      • Check whether there are process.nextTick tasks. If so, execute them all.
      • Check whether microTasks exist. If yes, run them all.
      • Exit this phase.
    • The IO Callbacks phase is displayed.

      • Check for pending I/O callbacks. If so, perform the callback. If not, exit the phase.
      • Check whether there are process.nextTick tasks. If so, execute them all.
      • Check whether microTasks exist. If yes, run them all.
      • Exit this phase.
  • The system enters idle and prepare.

    • These two stages have little to do with our programming.
  • Enter the poll phase

    • First check to see if there are any outstanding callbacks, and if so, in one of two cases.

      • The first case:

        • Execute all available callbacks if any are available (including expired timers and IO events, etc.).
        • Check for process.nextTick callbacks, and if so, execute them all.
        • Check whether microtaks exist. If so, perform all operations.
        • Exit this phase.
      • The second case:

        • If no callback is available.
        • Check whether the immediate callback exists. If so, exit the poll phase. If not, block at this stage, waiting for new event notifications.
    • Exit the poll phase if there are no outstanding callbacks.

  • Enter the check phase.

    • If there are immediate callbacks, all immediate callbacks are executed.
    • Check for process.nextTick callbacks, and if so, execute them all.
    • Check whether microtaks exist. If so, perform all operations.
    • Exit the Check phase
  • Enter closing phase.

    • If there are immediate callbacks, all immediate callbacks are executed.
    • Check for process.nextTick callbacks, and if so, execute them all.
    • Check whether microtaks exist. If so, perform all operations.
    • Exit closing stage
  • Check if there are active handles (timer, IO, and other event handles).

    • If so, proceed to the next cycle.
    • If not, end the event loop and exit the program.

Before each subphase of the event loop exits, the following sequence is performed:

  • Check for process.nextTick callbacks, and if so, execute them all.
  • Check whether microtaks exist. If so, perform all operations.
  • Exit the current phase.

🌰

// In the Node environment, print the execution result of the following code
console.log('1');

process.nextTick(function() {
    console.log('2');
});

setTimeout(() = > {
  console.log('3');
}, 0);

async function async1() {
    console.log('4');
    await async2();
    console.log('5');
}

async function async2() {
	console.log('6');
}

async1();

new Promise(function(resolve) {
    console.log('7')
    resolve();
}).then(function() {
    console.log('8')});console.log('9');
Copy the code
  • In order of js execution from top to bottom, the synchronization task console output 1 is encountered first.

  • Further down the line, process.nextTick is encountered, and the callback is added to the microtask queue.

  • And then setTimeout, which is a macro task, is put in the macro task queue first.

  • The asynchrony in promises is embodied in then and catch, so the code written in promises is executed immediately as a synchronous task. In async/await, the code is executed immediately before an await occurs. Async modified functions that, by default, return the resolve content of the New Promise object (if async modified functions do not return a value, then no value is returned). Inside async method, when the program executes to await method, it blocks the program after await method, goes inside await method and executes before return, and then jumps out of async method and executes synchronous task parallel to async method.

    Therefore, when we encounter async/await, execute async1 and await functions, output 4,6 immediately, and then jump out of async1 (wait until the synchronous code with async1 method is finished, jump back inside async1), and await returns a non-promise value. Continue with the code following the async function.

    Immediately after you encounter a promise, print 7, then add the then callback to the microtask queue.

  • Further down you encounter the synchronization task output 9.

  • At this point, the execution stack is cleared. Since microtasks take precedence over macro tasks, callbacks in the microtask queue are executed first. NextTick is executed asynchronously before promise. next, then await the result and jump back inside async1 to continue executing the code following the async function. Finally, callback in then. So the output is 2,5,8.

  • The microtask queue empties, the cycle ends, but there are still tasks in the event queue, and the next cycle begins.

  • A new cycle begins with the Timers phase, which finds the corresponding setTimeout callback with output 3.

The final output is 1, 4, 6, 7, 9, 2, 5, 8, and 3

Node.js differs from the Event Loop in the browser

In the browser environment, the microTask queue is executed after each MacroTask has been executed.In Node.js, microtasks are executed between each phase of the event cycle. That is, tasks in the MicroTask queue are executed after each phase is completed.

reference

Dissect the NodeJS event loop: juejin.cn/post/684490…

NodeJS event loop: juejin.cn/post/691676…