Threads and processes

1. The concept

We often say that JS is single-threaded, meaning that there is only one main thread in a process. So what is a thread? What is a process?

Officially, processes are the smallest unit of CPU allocation. Threads are the smallest unit of CPU scheduling. These two sentences are not easy to understand, so let’s take a look at the picture:

Processes are like factories in the figure, with their own factory resources.

A thread is like a worker in the picture. Multiple workers work cooperatively in a factory with a 1:n relationship between the factory and the worker. That is, a process is composed of one or more threads, threads are different lines of code execution in a process;

The factory space is shared by workers, which means that the memory space of a process is shared and available to each thread.

Multiple factories exist independently of each other.

2. Multi-process and multi-thread

Multi-process: Allowing two or more processes to run at the same time on the same computer system. The benefits of multiple processes are obvious. For example, you can listen to the song while opening the editor and typing code, and there is no interference between the editor and the process of the listening software.

Multithreading: Multiple execution streams in a program, that is, multiple threads can run simultaneously in a program to perform different tasks, that is, a single program is allowed to create multiple threads of parallel execution to complete their respective tasks. In Chrome, for example, when you open a Tab page, you create a process that can have multiple threads (more on that below), such as a rendering thread, a JS engine thread, an HTTP request thread, and so on. When you make a request, you create a thread that can be destroyed when the request ends.

Second, browser kernel

To put it simply, the browser kernel is a visual image output by taking page content, organizing information (applying CSS), calculating and combining the final output, often referred to as a rendering engine.

The browser kernel is multi-threaded, under the control of the kernel threads cooperate with each other to keep synchronized, a browser usually consists of the following resident threads:

GUI rendering thread

JavaScript engine threads

Timing trigger thread

Event trigger thread

Asynchronous HTTP request threads

1. GUI rendering thread

Mainly responsible for page rendering, parsing HTML, CSS, building DOM tree, layout and drawing, etc.

This thread is executed when the interface needs to be redrawn or when a backflow is caused by some operation.

This thread is mutually exclusive with the JS engine thread. When the JS engine thread executes, the GUI rendering will be suspended. When the task queue is idle, the JS engine will execute the GUI rendering.

2. JS engine threads

The thread is of course primarily responsible for processing JavaScript scripts and executing code.

It is also mainly responsible for the execution of events ready to be executed, that is, when the timer count ends, or the asynchronous request succeeds and returns correctly, it will enter the task queue in turn and wait for the execution of the JS engine thread.

Of course, this thread is mutually exclusive with the GUI rendering thread, and when the JavaScript engine thread takes too long to execute the script, the page rendering will block.

3. The timer triggers the thread

The thread responsible for executing functions such as asynchronous timers, such as setTimeout and setInterval.

When the main thread executes the code in turn, it will give the timer to the thread for processing. After counting, the event triggering thread will add the event after counting to the tail of the task queue and wait for the JS engine thread to execute.

4. The event triggers the thread

It is responsible for handing the prepared events to the JS engine thread for execution.

For example, when the setTimeout timer counts out, ajax and other asynchronous requests succeed and trigger the callback function, or the user triggers the click event, the thread will successively add the ready events to the end of the task queue and wait for the JS engine thread to execute.

5. Asynchronous HTTP request threads

A thread responsible for executing functions such as asynchronous requests, such as Promises, AXIos, Ajax, etc.

When the main thread executes the code in turn, it meets the asynchronous request and gives the function to the thread for processing. When the status code changes, if there is a callback function, the event-triggering thread will add the callback function to the tail of the task queue and wait for the JS engine thread to execute.

3. Event Loop in browser

1. The Micro – Task and Macro – a Task

There are two types of asynchronous queues in an event loop: Macro (macro task) and Micro (micro task) queues. There can be multiple macro task queues and only one microtask queue.

Common macro-tasks include setTimeout, setInterval, setImmediate, Script, I/O, UI rendering, etc.

Common micro-tasks include process.nexttick, new Promise().then(callback), MutationObserver(new HTML5 feature), etc.

2. Event Loop process parsing

A complete Event Loop process can be summarized as the following stages:

When the execution stack is empty, we can think of the execution stack as a stack structure that stores function calls, following the principle of first in, last out. The micro queue is empty, and the Macro queue has one and only one script.

The global context (script tag) is pushed onto the execution stack to synchronize code execution. During execution, the task is determined to be synchronous or asynchronous, and new macro-tasks and micro-tasks can be generated by calling some interfaces, which are pushed into their respective task queues. The script will be removed from the macro queue after the synchronized code is executed. This process is essentially the execution and dequeuing of the macro task in the queue.

In the previous step, we assigned a macro-task. In this step, we dealt with a micro-task. But it’s important to note that when Macro-Task is out, tasks are executed one by one; Micro-tasks, on the other hand, are executed in teams. Therefore, we process the micro queue by executing the tasks in the queue one by one and de-queuing them until the queue is empty.

Perform render operations to update the interface

Check whether Web worker tasks exist and process them if so

The process repeats until both queues are empty

To summarize, each cycle is a process like this:

When a macro task completes, it checks to see if there is a microtask queue. If yes, all the tasks in the microtask queue are executed first. If no, the first task in the macro task queue is read. During the execution of the macro task, microtasks are added to the microtask queue one by one. When the stack is empty, the tasks in the microtask queue are read again, and so on.

4. Node Event Loop

1. The introduction of the Node

The Event Loop in Node is a completely different thing from the Event Loop in the browser. Node.js uses V8 as the PARSING engine of JS, and libuv designed by itself is used for I/O processing. Libuv is an event-driven cross-platform abstraction layer, which encapsulates some underlying features of different operating systems and provides a unified API externally. The event loop mechanism is also implemented within it (more on that later).

Node.js works as follows:

V8 engine parses JavaScript scripts;

The parsed code calls the Node API;

The Libuv library is responsible for executing the Node API. It assigns different tasks to different threads to form an Event Loop, which asynchronously returns the execution results of tasks to V8 engine.

The V8 engine returns the results to the user.

2. Six stages

The event loop in the Libuv engine is divided into six phases, which are repeated sequentially. Each time a phase is entered, the function is fetched from the corresponding callback queue and executed. The next phase occurs when the queue is empty or the number of callback functions executed reaches a threshold set by the system.

The event loop in the Libuv engine is divided into six phases, which are repeated sequentially. Each time a phase is entered, the function is fetched from the corresponding callback queue and executed. The next phase occurs when the queue is empty or the number of callback functions executed reaches a threshold set by the system.

In the figure above, you can roughly see the sequence of events in node:

External input data –> Polling stage –> Check stage –> Close callback stage –> Timer detection stage –>I/O callback stage –> IDLE stage Prepare)–> Polling phase (repeated in this order)…

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

The I/O Callbacks phase: handles some of the few outstanding I/O callbacks from the previous cycle;

Idle, prepare: Used only internally.

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

The Check phase: Executes the setImmediate() callback;

Close Callbacks: Execute the socket’s close event callback;

Note: None of the above six phases includes process.nexttick ()(described below)

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

(1) timer

The Timers phase performs setTimeout and setInterval callbacks and is controlled by the Poll phase. Similarly, the timer specified in Node is not the exact time and can only be executed as soon as possible.

(2) poll

Poll is a crucial stage in which the system does two things

Go back to the Timer phase and perform the callback perform the I/O callback and two things happen when you enter the phase if the timer is not set

If the poll queue is not empty, it will iterate through the callback queue and execute synchronously until the queue is empty or reaches the system limit. If the poll queue is empty, two things will happen. If there is a setImmediate callback that needs to be executed, The poll phase stops and goes to the Check phase to perform the callback. If there is no setImmediate callback to perform, it waits for the callback to be added to the queue and executes the callback immediately. There’s also a timeout set here to prevent waiting and of course if you set a timer and the poll queue is empty, it will determine if any timer has expired and if any timer has expired it will go back to the timer phase and do a callback.

(3) the check phase

The setImmediate() callback is added to the Check queue, and the phase diagram of the Event loop shows that the check phase is executed after the poll phase.

Let’s start with an example:

console.log('start')
setTimeout(() = > {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')})},0)
setTimeout(() = > {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')})},0)
Promise.resolve().then(function() {
  console.log('promise3')})console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
Copy the code

When a macro task is executed (printing the start end and putting the two timers into the timer queue), the macro task is executed (the same as in the browser), so the promise3 is printed

Then enter the Timers phase, execute the timer1 callback function, print timer1, and place the promise.then callback into the MicroTask queue, and do the same for timer2, print timer2; This is quite different from the browser side. Several setTimeout/setInterval are executed successively in the Timers stage, unlike the browser side, which executes a micro task after each macro task (As for the difference between Node and browser Event Loop, More on this later).

  1. Pay attention to the point

(1) the setTimeout and setImmediate

They are very similar, but the main difference lies in the timing of the call.

SetImmediate setImmediate is designed to perform when the poll phase is complete, the Check phase; The setTimeout design is executed when the poll phase is idle and the set time is up, but it is executed during the timer phase

setTimeout(function timeout () {
  console.log('timeout');
},0);
setImmediate(function immediate () {
  console.log('immediate');
});
Copy the code

For the above code, setTimeout may be executed before or after.

First setTimeout(fn, 0) === setTimeout(fn, 1), this is determined by the source code and there is a cost to enter the event loop. If it takes more than 1ms to prepare, then the setTimeout callback will be executed directly during the timer phase

If the setup time is less than 1ms, then the setImmediate callback executes first

However, setImmediate is always used before setTimeout is used when the two are called inside the asynchronous I/O callback

const fs = require('fs')
fs.readFile(__filename, () = > {
    setTimeout(() = > {
        console.log('timeout');
    }, 0)
    setImmediate(() = > {
        console.log('immediate')})})// immediate
// timeout
Copy the code

In the code above, setImmediate always executes first. The IO callback is performed in the POLL phase. The queue is empty after the POLL callback is completed. SetImmediate does not mediate, so it jumps directly to the Check phase to perform the callback.

(2) process.nextTick

This function is actually separate from the Event Loop and has its own queue. When each stage is complete, if there is a nextTick queue, it will empty all callback functions in the queue and take precedence over other Microtasks.

setTimeout(() = > {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')})},0)
process.nextTick(() = > {
 console.log('nextTick')
 process.nextTick(() = > {
   console.log('nextTick')
   process.nextTick(() = > {
     console.log('nextTick')
     process.nextTick(() = > {
       console.log('nextTick')})})})})// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
Copy the code

5. Event Loop difference between Node and browser

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

Let’s use an example to illustrate the difference:

setTimeout(() = >{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')})},0)
setTimeout(() = >{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')})},0)
Copy the code

Running result on the browser: Timer1 =>promise1=> Timer2 =>promise2

Node running result: Timer1 => Timer2 =>promise1=>promise2

The global script (main()) is executed, and the two timers are put into the timer queue one by one. After main() is executed, the call stack is idle, and the task queue starts to execute. First enter the timers stage, execute the callback function of Timer1, print timer1, and place the promise1. Then callback into the MicroTask queue. Repeat the same steps to run timer2 and print Timer2. Before the Event loop enters the next phase, all tasks in the MicroTask queue are executed and promisE1 and promisE2 are printed successively

Refer to the link: www.cnblogs.com/fundebug/p/…

Six, summarized

The execution timing of microTask queues varies between browsers and Nodes

On the Node side, MicroTask executes on the browser side between the stages of the event loop, and MicroTask executes after the MacroTask of the event loop completes execution