The term “asynchronous” actually predates Node. But asynchrony is rare in most high-level programming languages. Node is the first of many high-level languages or platforms to embrace asynchrony as a major programming and design concept.

Asynchronous I/O, event-driven, and single-threaded form the backbone of Node, and Nginx is similar to Node’s event-driven, asynchronous I/O design philosophy. Written in pure C, Nginx has excellent performance and a powerful ability to manage connections to the client, but it is still limited by a variety of synchronous programming languages behind it. But Node is omnidirectional, can be used as a server to deal with a large number of concurrent requests brought by the client, can also be used as a client to carry out concurrent requests to various applications in the network.

Why asynchronous I/O

The reason why asynchronous I/O is so important in Node is that Node is designed for networks, and concurrency is standard in modern programming with a cross-network architecture.

The user experience

As mentioned in High Performance JavaScript, if the script takes more than 100 milliseconds to execute, the user will feel that the page is stalling and will stop responding. However, in the B/S model, the limitation of network speed makes the real-time experience of web pages very difficult.

If the web page temporarily needs to obtain a resource through synchronous mode, the JavaScript needs to wait for the resource to be fully obtained from the server before continuing to execute, during which time the UI stops and does not respond to the user’s interaction. The user experience will be terrible. With asynchronous requests, the execution of JavaScript and UI is not in a waiting state while the resource is being downloaded and can continue to respond to the user’s interaction.

Similarly, the front end can eliminate UI blocking through asynchrony, but the speed at which the front end can acquire resources depends on the speed at which the back end can respond. If a resource comes from the return of data from two different locations, the first resource consumes M milliseconds and the second resource consumes N milliseconds. If the synchronization mode is used, the time to obtain two resources is M+N milliseconds. In asynchronous mode, the acquisition of the first resource does not block the acquisition of the second resource, consuming Max (M,N).

The values of M and N increase linearly as the site or application expands, and asynchronous performance is better than synchronous.

The allocation of resources

Assuming a business scenario has a set of unrelated tasks that need to be completed, there are two main approaches:

  • Single-threaded serial execution at a time
  • Multithreading in parallel

Multithreading is preferred if the overhead of creating multiple threads is less than that of parallel execution, but multithreading has a high overhead in thread creation and thread context switching at execution time, and multithreaded programming often faces problems such as locking and state synchronization.

The downside of single-threaded sequential execution of tasks is performance, as any task that is slightly slower will cause subsequent execution code to block. In computer resources, usually I/O and CPU calculations can be executed in parallel, but the synchronous programming model leads to I/O running and subsequent tasks waiting, resulting in poor utilization of resources.

Node uses a single thread to avoid multi-thread deadlocks and state synchronization. Use asynchronous I/O to keep single threads from blocking and make better use of CPU.

Asynchronous I/O implementation

Asynchronous I/O is most widely used in Node, but it was not invented in Node.

Asynchronous I/O and non-blocking I/O

For computer kernel I/O, asynchronous/synchronous and blocking/non-blocking are two different things.

The operating system has only two ways to deal with I/O: blocking and non-blocking. When calling blocking I/O, the application waits for the I/O to complete before returning the result.

A characteristic of blocking I/O is that the call must not end until all operations have been completed at the system kernel level. Blocking I/ OS causes the CPU to wait for I/ OS, wasting the waiting time and making insufficient use of CPU processing capabilities.

To improve performance, the kernel provides non-blocking I/O. Non-blocking I/O with the difference of blocking I/O calls will return immediately after, non-blocking I/O return after the CPU time slice can be used to deal with other things, the performance is obvious, but due to the completed I/O is not complete, return immediately is not the business layer of the desired data, but simply the current state of call.

To get the complete data, the application repeatedly calls the I/O operation to verify that it is complete. This technique of repeated calls to determine whether an operation is complete is called polling.

The existing polling technologies include read, SELECT, poll, epoll and kqueue. Here is just the polling principle of epoll.

Epoll is the most efficient I/O event notification mechanism in Linux. If no I/O event is detected during polling, epoll hibernates until an event occurs and wakes it up. It actually takes advantage of notification of events, performing callbacks instead of traversing queries, so it doesn’t waste CPU and is more efficient to execute.

The polling technique meets the need for non-blocking I/O to ensure complete data retrieval, but it is still a synchronization for the application, because the application still waits for the I/O to return completely and still spends a lot of time waiting. During the wait, the CPU is either used to traverse file descriptors or for sleep wait time to occur.

Realistic asynchronous I/O

Asynchronous I/O is easily implemented (although simulated) by having several threads do either blocking I/O or non-blocking I/O plus polling, allowing one thread to do the computation, and passing the DATA from the I/O through communication between threads.

But initially, Node in * NIx platform with Libeio and Libev to achieve I/O part, to achieve asynchronous I/O. In Node V0.9.3, thread pools are self-implemented for asynchronous I/O.

IOCP for Windows provides li xiang’s asynchronous I/O to some extent: call asynchronous methods, wait for notification when THE I/O is complete, and perform callbacks without the user having to worry about polling. But it is still the thread pool principle, the difference is that these thread pools are managed by the system kernel.

Due to the differences between The Windows and * NIx platforms, Node provides libuv as an abstract encapsulation layer, allowing all platform compatibility to be determined by this layer and ensuring that the upper layer of Node is independent from the lower layer of custom thread pools and IOCPs.

We often refer to Node as single-threaded, which is simply JavaScript executing in a single thread. In Node, whether on * NIx or Windows platforms, there are separate thread pools internally for I/O tasks.

Node asynchronous I/O

The entire asynchronous I/O loop is completed by an event loop, an observer, and a request object.

Event loop

The event loop is Node’s own execution model, which makes callback letters common.

When the process starts, Node creates a loop similar to while(True), and executes the body of the loop each time we call it Tick. The process of each Tick is to check to see if there are any events to handle, and if there are, to fetch the event and its associated callback function. If there are associated callback functions, execute them. It then enters the next loop and exits the process if there is no more event handling.

The observer

Each event loop has one or more observers, and the process of determining if there are events to be processed is to ask those observers if there are events to be processed.

In Node, events are generated from network requests and file I/O. The observers of these times include file I/O observers and network I/O observers. Observers categorize the events.

The event loop is a typical producer/consumer model. Asynchronous I/O, network requests, and so on are producers of events, continuously providing Node with different types of events that are passed to the corresponding observer, and the event loop picks up the event from the observer and processes it.

The request object

For Node asynchronous I/O calls, the callback function is not invoked by the developer. In the transition from a JavaScript call to an I/O operation performed by the kernel, there is a artifact called a request object

Let’s use the fs.open() method as a small example.

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

Fs.open () opens a file with the specified path and parameters to get a file descriptor, which is a preliminary operation for all subsequent I/O operations. Javascript-level code calls C++ core modules for lower-level operations.

The core module of Node is called by JavaScript. The core module calls C++ module. The built-in module makes system calls through libuv. Here libuv acts as an encapsulation layer, with two platform implementations that essentially call the uv_fs_open() method. During the call to uv_fs_open(), the parameters passed in from the JavaScript layer and the current method are wrapped in a request object, and the callback function is set on the properties of this object. Once the object is wrapped, it is pushed into the thread pool for execution.

At this point, the JavaScript call returns immediately, ending the first phase of the asynchronous call initiated by the JavaScript layer. JavaScript threads can continue to perform subsequent operations on the current task.

The request object is an important intermediary in the asynchronous I/O process, where all state is stored, including feeding into the thread pool for execution and callback processing after the I/O operation completes.

Implement the callback

Assembling the request object and sending it to the I/O thread pool for execution is just the first part of an I/O; the callback notification is the second part.

Thread pool in the I/O operations after the call, will obtain the result of stored in the req – > result attribute, and then call PostQueueCompletionStatus inform IOCP (), told the current object operation has been completed.

At this point, the asynchronous I/O process is complete.

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

summary

Putting this together, we can extract a few key words for asynchronous I/O: single thread, event loop, observer, and I/O thread pool. There is something paradoxical about single threads and thread pools. Because JavaScript is single-threaded, it’s easy to understand that it doesn’t take full advantage of multicore cpus. In fact, Node itself is multithreaded, except that JavaScript is single-threaded, but the I/O threads use less CPU. Also, all I/O (disk I/O, network I/O, etc.) can be done in parallel, except that user code cannot be executed in parallel.