Node is not single-threaded!

Yonatan Merkebu
Dev Genius
Published in
5 min readOct 25, 2022

--

You may have read online from time to time that node is single-threaded.

People say node is single-threaded. However, that’s not the full story. The truth is much more complicated. So in this section, we’re going to start to investigate whether or not node is truly single-threaded.

When we start up a program with node, a single instance of the event loop is created and placed into one thread.

Now, that’s commonly seen as kind of a bad thing because it means that our program can only run on one core of our CPU. So if we have many cores inside of our CPU on our computer, then node is not going to automatically take advantage of those.

So in other words, our program might not run as fast as it could because it’s limited to one single thread.

However, some of the functions that are included inside of the standard library of node are not actually single-threaded.

In other words, some of the functions that are included inside of node that we run, run outside of the event loop and outside of that single thread.

The event loop uses a single thread, but a lot of the code that you and I write does not actually execute inside that thread entirely.

So simply declaring that node is single-threaded is not absolutely true.

To demonstrate this, let's see this simple example below.

We take the start time, we call the function, and then when the callback is triggered, we take the new current time, we subtract the old time and we should get out how long it took in milliseconds to calculate this hash value.

You can see it took about 600 milliseconds to finish(Results might be different for your machine).

Now let's copy and paste the hashing function one more time and run the code again.

You’re going to see two times that are very close to the original ones.

First off, when we run this file, both functions are going to be invoked at more or less the exact same time.

Remember that a thread represents a linear series of instructions to our CPU, so the CPU has to follow them all in order of the order that we present them.

We know that one call to PBKDF2 takes about 0.6 seconds.

If this really was a single-threaded system, I would have expected this entire process to take 1.2 seconds total, we get 0.6 seconds for the first call that completes, and then after 0.6 additional seconds, we should see a second console log appear.

However, this is not what actually happened.

When we ran our code, we very clearly saw that both these logs occurred at basically the same time and it took just about equal to the original run of the function.

The reality was that we started up our program at zero seconds, and then it took exactly 0.6 seconds for both of those function calls to get to the callback.

So clearly, this is indicating that something is happening to indicate that we are breaking out of a single thread setup with node because if we only had one single thread, we would have seen the first function call complete and then the second one startup.

We’re going to expand on this example a little bit and figure out why we are seeing this behavior.

First, let’s look at a diagram of what’s actually going on with a PBKDF2 function behind the scenes.

If you go to the node repo on Github, you’re going to find a bunch of different files and folders. But there are two folders that are very relevant to what we’re trying to do right now, the lib and src folders.

The lib folder contains all the JavaScript definitions of functions and modules that you and I require into our projects. And inside the src directory is the C++ implementation of all those functions.

The src directory is where node actually pulls in libuv and the V8 project and actually flushes out the implementation of all the modules that you and I use.

So for our example above, the PBKDF2 function actually delegates all the work to be done to the C++ side. So that’s where the actual hashing process takes place.

Showing you how node uses libuv internally will make this post a bit longer so I will leave that to you.

If you recall from the previous post, libuv gives node some access to the underlying operating system.

The libuv module has another responsibility that is relevant to some very particular functions in the standard library.

For some standard library function calls, the node C++ side and libuv decide to do expensive calculations outside of the event loop entirely.

Instead, they make use of something called a thread pool.

The thread pool is a series of four threads that can be used for running computationally intensive tasks such as the PBKDF2.

By default, libuv creates four threads in this thread pool. So that means in addition to the thread used for the event loop, there are four other threads that can be used to offload expensive calculations that need to occur inside of our application.

Many of the functions, including the node standard library, will automatically make use of this thread pool. And as you might imagine, the PBKDF2 function is one of them.

If the event loop was responsible for doing this computationally intensive task, that means that our node application could do absolutely nothing else while we were running the PBKDF2 function. So by using the thread pool, we can do other things inside of our event loop while other heavy calculations are occurring.

So this shows that node is actually not entirely single-threaded. In the next post, we will look more into thread pools and how we can use them effectively. So make sure to follow.

--

--