This is the 29th day of my participation in the August Wenwen Challenge.More challenges in August

The first article in this series introduced why we need asynchronous I/O in terms of user experience and resource allocation. This article focuses on asynchronous I/O support at the operating system level. The second article covered asynchronous I/O support at the operating system level, and the difference between asynchronous and non-blocking.

This article focuses on how NodeJS implements asynchronous I/O.

Event loop

When the process starts, nodeJS will have a loop similar to while(true). Each time it executes the loop body, we call it a Tick. Each Tick checks to see if there is an event to execute, and if there is, it executes the event and its associated callback function. Exit the process.

The observer

So how do you tell if there’s an event to deal with? Each event loop has one or more observers, and the process of determining if there is an event to be executed is like the observer asking if there is an event to be processed. An observer may have multiple events.

Observers in browsers handle user clicks, file loads, etc. Observers in NodeJS are mainly network requests, file I/O, etc. Observers categorize the events.

The event loop is a typical producer/consumer pattern. Asynchronous I/O, network requests are producers, events are delivered to the observer, and the event loop takes files from the observer and processes them.

As mentioned earlier, under Windows, this loop is created based on IOCP, while under * NIx it is created based on multithreading.

The request object

So what happens in asynchronous I/O when JavaScript code gets to the kernel

Let’s take fs.open() as an example. Fs.open opens a file based on the specified path and parameters to get a file descriptor

fs.open = function(path, flags, mode, callback) {
    // ...
    binding.open(pathModule._makeLong(path), stringToFlags(flags),mode,callback); 
};
Copy the code

As you can see in the fs.open implementation, the JavaScript code calls the C++ core module to perform the following operations, the overall flow is as follows:

The JavaScript code calls the nodejs core module, which calls the c++ built-in module. The built-in module makes system calls through libuv, which is the classic way to call nodejs.

As we can see from the libuv module, we end up calling the uv_fs_open() method. When this method is called, we create a FSReqWrap request object that encapsulates the parameters passed in from the JavaScript layer and the current method. The callback function is on the onComplete_sym property of this object.

req_wrap->object_->Set(oncomplete_sym, callback);
Copy the code

Once the object is wrapped, under Windows, the FSReqWrap object is pushed into the thread pool for execution by calling QueueUserWorkItem(), which looks like this:

QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTEDEFAULT)
Copy the code

The first argument is a reference to the function to be executed, the second argument is the argument to be executed, and the third argument is the execution identifier bit. When threads are available, the uv_fs_thread_proc method is executed. It determines which underlying function to execute based on the parameters passed in. In our example, the uv_fs_open() method is fs__open().

At this point, the JavaScript call returns immediately, and the actual operation is waiting in the I/O thread pool without affecting the JavaScript thread to continue executing, achieving the asynchronous purpose.

Implement the callback

The request object FSReqWrap was placed in the thread pool for execution, so let’s take a look at how callback notification works.

Thread pool after the I/O calls, will result in the req – > the result, and then executes PostQueuedCompletionStatus tell IOCP () the current operation has been completed, you can return the thread pool. Through the PostQueuedCompletionStatus () submitted by the state, can through the GetQueuedCompletionStatus () is available.

At each Tick in the process of execution, calls GetQueuedCompletionStatus () check the thread pool for execution of the request, and if so, put it on the I/O observer of the queue, I/O observer will take out the result attribute as a parameter, Take onComplete_sym as a method and call execution, and that completes the purpose of executing the callback

The overall process:

The thread pool is provided directly by the kernel (IOCP) under Windows. The thread pool is implemented by Libuv under the * NIX series

The event loop, observer, request object, and I/O thread pool constitute the basic elements of Node asynchronous I/O model.

This article is a summary of the chapter on asynchronous I/O in NodeJS