Threads in Node.js

Guilherme Nunes Behs
CodeX
Published in
3 min readAug 15, 2024

Parallelism and Concurrency are subjects we can talk a lot about. Since the first CPUs allowed OS to process multiple tasks simultaneously or concurrently, lots of programming languages and platforms started to benefit from this capability, and thus our processing power increased and our response time decreased.

In this scenario, the concept of threads showed up to help us to take advantage of such capabilities. We can define threads as a part of an application that runs along with it. The reasons why you would do it could be:

  • You have a routine that takes too long to finish. This way, you don't block the main process.
  • You have an independent routine whose response is not necessary for other routines. Again, you don't block the main process.

Although some programming languages, like Java, have a well-defined API to handle threads, some people have the misconception that Node.js is a single-thread platform (yeah, I know the Event Loop`s main process is single-threaded), and you can't work with Parallelism and Concurrence while using it. Despite working a lot with threads in its internal structure, Node.js has a module that allows engineers to work with it. It is called worker_threads.

The worker_threads module allows you to execute a thread using another javascript file as an argument, send parameters to the thread via memory, and receive a response from it via memory too. As an example, let's consider a small app that executes the function pbkdf2Syn() from the crypto module. This function can take a lot of time to be executed, depending on the parameters. In this example, I'm going to simulate a heavy execution.

//crypto_v1.js

const { pbkdf2Sync } = require('crypto')

function crypto(){
pbkdf2Sync('a','b', 100000,512,'sha512')
}

const start = Date.now()

for(let i =0; i <10;i++){
crypto()
}

console.log('Time taken:', Date.now() - start)

The result on the console showed the time it took to execute the entire operation was 5042 mills. Now, let's refactor this code to use worker_threads.

//crypto_v2.js

const {Worker} = require('worker_threads')

const start = Date.now()

for(let i = 0; i < 10; i++){
const worker = new Worker(`${__dirname}/worker_thread.js`)
worker.postMessage({})
worker.on('message', (data)=>{
worker.terminate()
})
}

process.on('exit',()=>{
console.log('Time taken:', Date.now() - start)
})
//worker_thread.js

const { pbkdf2Sync } = require('crypto')
const {parentPort} = require('worker_threads')

function crypto(){
pbkdf2Sync('a','b', 100000,512,'sha512')
}

parentPort.on('message',()=>{
crypto()
parentPort.postMessage({})
})

As you can see, some things have changed. Let’s start with the file crypto_v2.js. We instantiated the class Worker, passing a file path as an argument, that will be executed as a thread. The method “postMessage” is used to send data to the thread, and we can listen to the event “message” to receive data from it. The method “terminate” finished the thread.

The worker_threads.js file contains all the logic to process the crypto function in a different thread from the main one. Now we import from the worker_threads module the “parentPort” object. With it, we can receive and send data from and to the main thread. It’s possible to listen to the “message” event to receive data from the main thread and to call the “postMessage” method to send data to it. This way, the routine took 1583 mills to finish the execution. Maybe it can be a small difference, but imagine how this difference could increase with a heavier routine, involving lots of async and sync operations.

So, this is the worker_threads module. Certainly, this is a feature that came to stay. If you want to know more about it, you can check the official docs here: https://nodejs.org/api/worker_threads.html

--

--