It is said that Node.js can achieve high performance server, but what is high performance?

All software code ultimately runs through the CPU, and efficient utilization of the CPU is the hallmark of performance, that is, not letting it run idle.

So when does it idle?

  • When a program is doing network and disk I/O, the CPU is idle.
  • A multi-core CPU can run multiple programs at the same time, and if only one of the cores is being utilized, the others are idling.

So, in order to achieve high performance, you have to solve these two problems.

The operating system provides an abstraction of threads, where different execution branches of code can be run on different cpus at the same time, a way to take advantage of the performance of multiple core cpus.

And if some threads are doing I/O, that is, to block waiting for the read and write to complete, this is a relatively inefficient way, so the operating system implements DMA mechanism, is the device controller, the hardware is responsible for moving from the device to memory, when finished to tell the CPU. This allows the thread to pause while it is IO and continue running after receiving notification that DMA traffic is complete.

Multithreading, DMA, which is the use of many core CPU advantages, solve the PROBLEM of CPU blocking IO operating system to provide solutions.

Various programming languages encapsulate this mechanism, as does Node.js, which is high-performance because of its asynchronous IO design.

Node.js’s asynchronous IO implementation in Libuv is based on asynchronous system calls provided by the operating system, which are typically hardware-level asynchronous, such as DMA moving data. However, some of these synchronous system calls are also made asynchronous by libuv encapsulation because libuv has a thread pool to perform these tasks, making the synchronous API asynchronous. The size of this thread pool can be set using the UV_THREADPOOL_SIZE environment variable, which defaults to 4.

A lot of the asynchronous apis we call in our code are implemented through threads.

Such as:

const fsPromises = require('fs').promises;

const data = await fsPromises.readFile('./filename');
Copy the code

However, this asynchronous API only solves the IO problem, so how do you take advantage of multi-core cpus for computing?

Node.js was introduced experimentally in 10.5 (and officially in 12) with the worker_Thread module, which can create threads and eventually run on multiple cpus, which is a way of doing calculations using multi-core cpus.

Asynchronous apis can leverage multiple threads to do IO, while worker_threads can create threads to do calculations for different purposes.

To understand worker_threads, start with the browser’s Web worker.

The browser’s Web worker

Browsers also face the problem of not being able to use multi-core cpus to do calculations, so HTML5 introduces Web workers, which can do calculations through another thread.

<! DOCTYPEhtml>
<html lang="en">
<head></head>
<body>
    <script>
        (async function () {
            const res = await runCalcWorker(2.3.3.3);
            console.log(res); }) ();function runCalcWorker(. nums) {
            return new Promise((resolve, reject) = > {
                const calcWorker = new Worker('./webWorker.js');
                calcWorker.postMessage(nums)
                calcWorker.onmessage = function (msg) {
                    resolve(msg.data);
                };
                calcWorker.onerror = reject;
            });
        }
    </script>

</body>
</html>
Copy the code

We create a Worker object, specify the JS code that runs on another thread, and then pass messages to it via postMessage and receive messages via onMessage. This process is also asynchronous, and we further encapsulate it as a promise.

It then receives the data in Webworker.js, performs the calculation, and returns the result via postMessage.

// webWorker.js
onmessage = function(msg) {
    if (Array.isArray(msg.data)) {
        const res = msg.data.reduce((total, cur) = > {
            return total += cur;
        }, 0); postMessage(res); }}Copy the code

This way, we use another CPU core to run the calculation, just like normal asynchronous code. But this asynchrony is not actually asynchrony of IO, but asynchrony of computation.

Node.js worker threads are similar to Web workers, and I even suspect that the names of worker threads are influenced by Web workers.

Node. Js worker thread

Implementing the above asynchronous computation logic in Node.js looks like this:

const runCalcWorker = require('./runCalcWorker');

(async function () {
    const res = await runCalcWorker(2.3.3.3);
    console.log(res); }) ();Copy the code

Call asynchronously, because asynchronous computation and asynchronous IO are used in the same way.

// runCalcWorker.js
const  { Worker } = require('worker_threads');

module.exports = function(. nums) {
    return new Promise(function(resolve, reject) {
        const calcWorker = new Worker('./nodeWorker.js');
        calcWorker.postMessage(nums);

        calcWorker.on('message', resolve);
        calcWorker.on('error', reject);
    });
}
Copy the code

Then asynchronous computation is implemented by creating Worker objects, specifying JS to run in another thread, and passing messages via postMessage and receiving messages via Message. This is similar to web workers.

// nodeWorker.js
const {
    parentPort
} = require('worker_threads');

parentPort.on('message'.(data) = > {
    const res = data.reduce((total, cur) = > {
        return total += cur;
    }, 0);
    parentPort.postMessage(res);
});
Copy the code

In nodeworker.js where the computation is performed, listen for the message, perform the computation, and return data via parentPost.postMessage.

Compare web workers, and you’ll find something very similar. Therefore, I think the API of Node.js worker thread is designed by referring to Web worker.

However, worker threads also support passing data through wokerData at creation time:

const  { Worker } = require('worker_threads');

module.exports = function(. nums) {
    return new Promise(function(resolve, reject) {
        const calcWorker = new Worker('./nodeWorker.js', {
            workerData: nums
        });
        calcWorker.on('message', resolve);
        calcWorker.on('error', reject);
    });
}
Copy the code

Then the worker thread uses workerData to fetch:

const {
    parentPort,
    workerData
} = require('worker_threads');

const data = workerData;
const res = data.reduce((total, cur) = > {
    return total += cur;
}, 0);
parentPort.postMessage(res);
Copy the code

Since there is a mechanism for passing messages, serialization and deserialization are required, and data such as functions that cannot be serialized cannot be transferred. This is also a characteristic of worker threads.

Node.js worker threads compared to web woker

In terms of usage, they can be encapsulated as normal asynchronous calls, and are used just like any other asynchronous API.

Both require data serialization and deserialization, and both support postMessage and onMessage for sending and receiving messages.

In addition to message, Node.js worker threads support more ways to pass data, such as workerData.

But in essence, both are designed for asynchronous computing, taking full advantage of multi-core cpus.

conclusion

High performance programs that is to make full use of CPU resources, do not let it idle, that is, DO not let THE CPU IO, etc., multi-core CPU can also be used at the same time to do calculations. Operating systems provide threading, DMA mechanisms to solve this problem. Node.js also makes corresponding packaging, that is, the ASYNCHRONOUS IO API implemented by Libuv, but the asynchronous calculation was officially introduced in Node 12, that is, worker thread. The API design refers to the web worker of the browser. Passing messages through postMessage and onMessage requires serialization of data, so functions cannot be passed.

In terms of usage, asynchronous computing and asynchronous I/O are used in the same way. However, asynchronous I/O just waits for THE I/O to complete. Asynchronous computing uses multi-core cpus to perform parallel computing at the same time, improving computing performance several times.