The modern times make to get you involved with technology inevitably. The internet world has taken over the real world in every sense. Programming and javascript are a significant part of the technological boom today. JavaScript was initially developed for simpler web jobs such as validating a web form but the breakthrough in the face of Node.js threads enabled developers to imply Node.js as a language for back-end code creation.
Multithreading supportive programming languages own the mechanism to sync values between oriented features and threads and inter-thread syncing too. Ryan Dahl in 2009 had to create a workaround to make it happen.
Working and the threads of Node.js
There are two types of threads used by Node.js which can be listed as such:-
- An event loop handled by the main thread.
- Different auxiliary threads in the worker pool
Event Loop: – Event Loop handles the functions and registers them for future use. JavaScript code and event loop works in the same thread. This is the reason for the blockage of the event loop when a JavaScript operation blocks the thread.
Worker pool: – Worker pool handles the task of execution of the spawning and separation of threads. The Node.js threads further do the task and return result synchronously to the event loop.
Call back is then provided with a result by the event loop.
I/O operations including the system’s disk and network’s interactions are handled by worker pool. Modules like the Fs and crypto use worker pool. An unnoticeable change is caused while Node communicates internally between languages such as C++ and javascript when the worker pool is implied in the Libuv.
The use of the above mentioned two mechanisms enables us to write code such as this:-
fs.readFile(path.join(__dirname, './package.json'), (err, content) => {
if (err) {
return null;
}
console.log(content.toString());
});
The worker pool is commanded by the Fs module for the task of using a thread for reading the content of a file and conveying it to the worker pool. The event pool executes the content of the file with the provided callback function.
It is by virtue of a non-blocking code such as the one described above that we are spared of waiting for things to happen synchronously. All we have to do is to command the worker pool to deliver the result of the provided function after reading the file.
The availability of its own thread with the worker pool empowers the event pool to function normally when the task of file reading is under process.
Until you need to simply accomplish the complex tasks in sync. Although if a scenario arises where a task consumes way too much time, blocking of the thread becomes too inevitable. If there are a lot of tough operations that are time-consuming, it would ultimately have a negative impact on the throughput of the network. If this is the case, the working pool cannot be assigned work.
When complex fields such as AI, machine learning tried to initially work with Node.js, the blockage of the main thread would cause the unresponsiveness of the server.
The emergence of the Node.js v10.5.0 changed things forever as it brought about the convenience of multiple threads usage.
Worker Threads: – Introduction
Fully functional Node.js applications are developed by the use of the worker threads. A piece of code when spawned in a separate thread is referred to as a thread worker. Thread, workers are interchangeable names used to describe a thread worker. It is a prime need to incorporate worker_ thread modules to use thread workers. Spawning of the thread workers can be shown by the task of creation as like the one below:-
type WorkerCallback = (err: any, result?: any) => any;
export function runWorker(path: string, cb: WorkerCallback, workerData: object | null = null) {
const worker = new Worker(path, { workerData });
worker.on('message', cb.bind(null, null));
worker.on('error', cb);
worker.on('exit', (exitCode) => {
if (exitCode === 0) {
return null;
}
return cb(new Error(`Worker has stopped with code ${exitCode}`));
});
return worker;
An instance of the working class has to be generated in order to develop a worker. The file that hosts the worker code has to be provided with a path by us. In the second instance, the worker data property has to be provided. The path provided by us should always refer to .js and, mjs property.
The property of workers that allows them to multiple message events makes the use of a callback approach and not the promised return which would on the other hand be resolved in case of an event fire.
It is clear from the below given instance below that we have to set up listeners to be used when the worker sends the event.
A few common events could be listed as such:-
worker.on('error', (error) => {});
In case of an unnoticed inside the worker causes the emitting of the event. This terminates the worker and the primary argument is availed to us in the callback.
worker.on('online', () => {});
The emitting of online is done when the Javascript code parsing is stopped by the worker, and its execution is begun. It does not happen too often but it is certainly providing on certain instances.
worker.on('message', (data) => {});
Message emitting happens when parent thread receives data from the worker.
Sharing of data between threads:-
The foremost argument is called ‘data’ in here and it is the very object that is transferred to the thread. Data can be anything that the copying algorithm might host.
Clone algorithm does the task of copying the data by the process of clone creation through recursion via the input of the object. Functions and errors are never copied by it. It should be understood that the copying by this process is pretty unlike the JSON as references and typed arrays are included in this method.
The supportiveness of typed arrays enables the inter-thread share of memory
Inter thread memory share
The ability of cluster or child_process modules to use the threads is highly debated.
Multiple node instances can be crafted using the cluster. It utilizes a single master process to get the task of incoming request routing done. Multiplication of the throughput of a server is enabled by the application clustering. The cluster cannot support the spawning of a separate thread.
PM2 is a tool preferred by developers for the clustering of applications as it is a more efficient way of doing it than the conventional manual execution of the task.
Child_process offers the execution of any executable regardless of it being javascript or not. The efficiency of the worker_threads wins over child_process and its popularity is a consequence of it. child_process lags several significant features that are offered by worker_threads.
Thread workers make up for better efficiency owing to their lightweight which is achieved because thread workers share the same ID as the parent threads.
Here is a typical example of memory sharing between thread which is executed by Shared Array Buffer as an argument to the other thread.
import { parentPort } from 'worker_threads';
parentPort.on('message', () => {
const numberOfElements = 100;
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * numberOfElements);
const arr = new Int32Array(sharedBuffer);
for (let i = 0; i < numberOfElements; i += 1) {
arr[i] = Math.round(Math.random() * 30);
}
parentPort.postMessage({ arr });
})
To begin with a Shared Array Buffer is created in order to make 100 32 integers. The creation of Int32Array follows up which is implied for the sake of saving the structure by the use of a buffer; and then to continue we fill any numbers in an array.
Memory share in parent thread:-
import path from 'path';
import { runWorker } from '../run-worker';
const worker = runWorker(path.join(__dirname, 'worker.js'), (err, { arr }) => {
if (err) {
return null;
}
arr[0] = 5;
});
worker.postMessage({});
- The value of arrest[0] is changed to 5 for the purpose of changing both ends of the parent thread.
There is no doubt that when we risk an unwanted change in the value in other threads when the values are changed by us in anyone as the memory between both the threads sis already shared.
Although the same memory share function works to boost its efficiency because it offers us the ease of not having to do serialization of the values in order to access them in the other thread. The management of the data is necessary so that it can be disposed of and collected easily as the garbage once we are done working with it.
Object sharing is the original way of data storage and should be our prime motive even though it is easy pretty to share a myriad of integers. We have to create a similarity to Shared Object Buffer as it is not present in there by default.
- Transfer list: – It consists of Array Buffer and Message Port though the case is that these cannot be used in the sending thread once the transfer is made to the other thread. This happens because the memory has been already transferred r moved to the other thread too.
Channel creation for communication: – It is via the ports that the communication between two threads is made possible. Event-based inter-thread communication is enabled by the message port.
There are two different and specific ways to use the ports to support the memory transfer. One is when we import Parent Port from the worker_thread. It can be shown as such:-
import { parentPort } from 'worker_threads';
const data = {
// ...
};
parentPort.postMessage(data);
The ParentPort here can be described as a single instance of the Messageport that is offered to us for the sake of enabling communication between threads. This facility is availed to us by the Node.js. Another way to communicate between threads is to craft a message channel by ourselves and then forward it to workers. An example of how to do it is given below:-
import path from 'path';
import { Worker, MessageChannel } from 'worker_threads';
const worker = new Worker(path.join(__dirname, 'worker.js'));
const { port1, port2 } = new MessageChannel();
port1.on('message', (message) => {
console.log('message from worker:', message);
});
worker.postMessage({ port: port2 }, [port2]);
Listeners are supposed to be setup on the port1 and port2 is supposed to be forwarded to the workers but this should only be done after we have moved both these ports to the transfer list.
Further, this happens inside the thread:-
import { parentPort, MessagePort } from 'worker_threads';
parentPort.on('message', (data) => {
const { port }: { port: MessagePort } = data;
port.postMessage('heres your message!');
});
The port received from the parent thread is then used in the above given way. Though the parent port can be used for the purpose but it would still be recommended to use a new Message Port to enable the transfer.
The creation of the Message Port has to be done by the aid of the Message Channel. It is then that the spawned worker comes into action after the Message Port has been transferred to it.
There are two separate ways in which the workers can be put to work:-
The first type involves the execution of the code of a spawned worker and then forwarding the result to the parent thread. The problem with this method is that it requires the creation of a new worker whenever a new task comes up. It can be shown as such-
import { parentPort } from 'worker_threads';
const collection = [];
for (let i = 0; i < 10; i += 1) {
collection[i] = i;
}
parentPort.postMessage(collection);
Node.js has received appreciation due to its support of the second way of doing the task which involves the spawning of a worker and then placing the listeners for the message in case of a message fire event. The result is always delivered to the parent thread whenever there is a message fire.
A major advantage of this method is that the worker is kept up and alive in this and can be used in the future when a new task comes up. This method can be portrayed as follows:-
import { parentPort } from 'worker_threads';
parentPort.on('message', (data: any) => {
const result = doSomething(data);
parentPort.postMessage(result);
});
Properties of Worker threads
- Is Main Thread: – This is a property that remains true until not put to use. Putting an ‘if’ statement would ensure you that it is only run as a worker.
import { isMainThread } from 'worker_threads';
if (isMainThread) {
throw new Error('It's not a worker');
}
- Worker data: – It is the data found on the constructor of the worker after the spawning of a thread.
const worker = new Worker(path, { workerData });
- Parent port: – It is the message port’s instance that becomes the way to communicate with the parent thread.
- Thread Id: – It is an identification given to every thread. The threaded is unique for every thread.
- Implementing set timeout: – A loop that runs infinite and is used to time-out an app is referred to as the Implementing set timeout function.