Understanding Node.js Worker Threads From Ground Up

Shubham SharmaShubham Sharma
7 min read

Welcome to the guide on Node.js worker threads! In this series, we're diving into how worker threads can supercharge your Node.js apps. Today, we'll start with the basics—understanding threads, processes, and how Node.js handles heavy tasks.

Let's begin by exploring CPU-intensive and I/O-intensive tasks. We'll break down the differences and check out practical examples to make things clearer. Along the way, we'll uncover the inner workings of Node.js, especially its main and worker threads and Libuv.

As we go through this post, you'll get a grip on Node.js' architecture, and we'll highlight the importance of Libuv in making things run smoothly.

By the end, you'll have a solid foundation to understand Node.js worker threads. Ready to get started? Let's jump into the world of Node.js multithreading! 🚀

Topics We Will Cover

  1. What is a thread?

  2. What is a process?

  3. I/O-intensive task

  4. CPU-intensive task

  5. Node.js behaviour for I/O-intensive operations

  6. Node.js behaviour for CPU-intensive operations

  7. Node.js Single Threaded Nature and Libuv

  8. What is a worker thread, and its relation with the main thread in node.js?

  9. Creating Worker Threads And Accessing The Properties And Methods

  10. Conclusion

Let's go topic by topic, first see what is thread. what is the process? And how do both relate to each other?

What is a thread?

A thread is the smallest unit of execution within a process. Two or more threads share the same memory and resource. Two threads which are part of the same process can communicate and collaborate.

Example: Google Docs application have threads for spell-checking, formatting, auto-saving, etc.

What is a process?

A process is a group of one or more threads. Each process has its own memory and resources, they don't share memory. Each process is isolated from the other process.

Example: A Computer System has a word processing app, media player, web browser, etc.

Understanding I/O-intensive tasks and CPU-intensive tasks will help us get a good grasp of worker threads.

I/O-intensive (I/O Bound) task

I/O-intensive task refers to a task that involves Input/Output Operations.

Some common I/O operations:

  • File I/O: File Creation, Deletion, Reading, Writing, etc.

  • Network I/O: Sending, and receiving data over the network, opening and closing network socket.

  • Database I/O: Database insertion, deletion, querying, etc.

  • Console I/O, IPC (Interprocess communication), etc.

CPU-intensive (CPU-bound) task

A CPU-intensive task is one that involves CPU operations.

Some common CPU-intensive operations

  • Complex mathematical Calculations

  • Video Encoding/Decoding

  • Image/Audio/Video Processing

  • Cryptography, etc.

Now that we know what a CPU-intensive task and an I/O-intensive task are, let's see how Node.js behaves for each one.

Node.js behaviour for I/O-intensive operations

Node.js is good at async I/O-intensive operations, let's understand why.

Node.js is based on Event-Driven, Asynchronous, Non-Blocking architecture, and it works on a single thread. It uses Google Chrome V8 Engine, Event Loop and Libuv library for I/O operations.

Whenever any request comes into the node, the event loop registers a callback and is processed asynchronously, when the request is completed that callback will be processed by the event loop. Under the hood node.js uses the Libuv library for performing I/O operations, these are the several key reasons why Node.js is good at async I/O operations.

Node.js behaviour for CPU-intensive operations

As Node.js is single-threaded and based on an event loop, it's critical to understand that whenever JavaScript is running event loop is not running, i.e. event loop will be blocked for long-running JavaScript operations.

For example, if we execute a for loop a million times in a request performing a complex operation. The event loop will be blocked and all the requests will be pending until the current operation completes.

Let's further understand by the below example:

This above example demonstrates a CPU-intensive operation using a Node.js Express server with two endpoints: '/non-blocking-operation' and '/complex-blocking-operation'. If we attempt to run the second one and then the first one in parallel, the first operation will be blocked until the second one completes. This illustrates that CPU-intensive operations in Node block the event loop.

There are a lot of other CPU-intensive operations that we need to perform in our application as described here CPU-intensive task. Considering the above example, now we are in good shape to understand that node.js is not good at handling CPU-intensive operations.

Node.js Single Threaded Nature and Libuv

Node.js is single-threaded by default, but internally it utilizes the Libuv library to perform async I/O operations. Libuv is a C library that provides support for asynchronous I/O based on event loops. Node.js uses the Libuv library to perform async I/O operations.

Libuv uses a thread pool for performing I/O operations, it has default four threads that are used for fs, zlib, crypto. We can also increase the number of threads depending on the system resources. Go through the link for detailed information: Libuv Docs

Up to this point, you now have a good understanding of what CPU-intensive and I/O-intensive tasks are and how Node.js behaves differently for each. Additionally, you are aware of how Node.js utilizes the Libuv library for asynchronous I/O operations. Now, let's explore Node.js worker threads and their relationship with the main thread in Node.js.

What is a worker thread and its relationship with the main thread in Node.js?

Node.js works on a single thread and is often known as the main thread. worker_threads module in node.js gives us the ability to use multiple threads in parallel with the main thread.

Each worker thread consists of the following

  • Node.js instance

  • Google Chrome V8 instance

  • Event loop

A single worker thread is the same copy of the main thread, except that both the main thread and worker thread share the same libuv thread pool.

Visualization of the interaction of the main thread and worker thread with the Libuv thread pool in node.js

To access Node.js worker threads (worker_threads) module, use the below code:

Creating Worker Threads and Accessing The Properties and Methods

Now, let's see how we can create worker threads and access their properties and methods using the worker_threads module. For this, I have created an example.

In the above example, we are creating worker threads using new Worker('worker_file_name.js') constructor.

We are using the following properties and methods of worker threads

  • isMainThread is used to check for the main thread and inside the if block we create a worker thread using new Worker() constructor, we specify the current file(__filename) as the worker for this example.

  • In the same if block, we are registering a worker.on('message') listener for listening to messages from the worker.

  • Inside the listener, we are terminating the worker once the message is received using worker.terminate();

  • In the else block, we have simulated a time-consuming task using a for loop and performing unshift(adding an element at the first index) on an array.

  • In the same else block, we are invoking the simulateTask function and sending the result of the for loop to the parent using parentPort.postMessage(result);

I have used a simulation of long-running operation (or CPU-intensive operation) inside worker threads, because as per node.js official docs: Workers (threads) are useful for performing CPU-intensive JavaScript operations. They do not help much with I/O-intensive work. The Node.js built-in asynchronous I/O operations are more efficient than Workers can be.

Conclusion

In this blog post, we covered various topics. These include what a thread and process are, the distinction between CPU-intensive and I/O-intensive tasks, examples of each, and how Node.js behaves differently for these tasks. The single-threaded nature of Node.js, and the importance of Libuv in Node.js architecture. We also delved into the concepts of the Node.js main thread and worker thread, along with some of their methods and properties, illustrated with code examples.

Worker Threads has some more methods and properties that are used in real-world projects, which we will cover in the next blog post. Additionally, we will provide an end-to-end working example of Node.js worker threads by performing CPU-intensive operations using worker threads.

This blog post aims to familiarize you with Node.js worker threads and the essential concepts you need to understand before working with them. As mentioned earlier, in the next blog post, we will explore additional methods and provide an in-depth working example of Node.js worker threads. I hope you find this post helpful! 😊

0
Subscribe to my newsletter

Read articles from Shubham Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shubham Sharma
Shubham Sharma

I am a seasoned software engineer with over 5 years of experience, specializing in JavaScript technologies like Node.js and React.js. My passion for technology fuels my drive to continuously learn, adapt, and create impactful solutions that meet the needs of users and businesses. I excel in designing and developing high-performance, secure, and scalable components and APIs. Collaboration and clean code are at the core of my approach, ensuring that I deliver value to clients through best practices and a customer-centric mindset.