Simulate Multithreading on a Single Thread Using Asynchronous Programming in JavaScript (Node.js)

Sameer HumayunComputer ScienceLeave a Comment

Concurrent programming using a single thread, Asynchronous programming (Blocking IO and non-blocking IO)

In computing, a thread is a sequence of execution of a program. A thread has a beginning a sequence and an end. At any point during the runtime of a thread, there is only a single point of execution. A thread is not a program it only runs within a program.


Blocking IO and non-blocking IO

There are two types of IO operation blocking and non-blocking. With a blocking operation when a request is made, the thread gets blocked and it waits until that operation finishes before that thread is available for another operation. Until that operation finishes the thread cannot do anything else but wait. To do more operations concurrently we need to have multiple threads. Some programming languages do support multi-threading. One example is JavaScript which only runs on single threads. How do we achieve concurrent operations in single-threaded programming language? This is where non-blocking IO comes into play.

Non-blocking IO is when the thread is available for other operations without waiting for one to finish. In non-blocking IO the thread is always available for more operations. Non-blocking IO is concurrent programming on a single thread. Non-blocking IO programming is not only fast but multiple tasks can be done in less time when compared to blocking IO.

The below code shows the difference between the execution time of blocking and non-blocking IO.

Blocking IO


const usersSync = require('./userSync')

 const user1 = userSync(1)
 console.log(user1)

 const user2 = userSync(2)
 console.log(user2)

 const sum = 10 + 30
 console.log(sum)

Non-blocking IO


const getUsers = require ('./users')

 getUsers(1, (user) => {
    console.log(user)
 })

 getUsers(2, (user) => {
    console.log(user)
 })

 const sum = 10 + 30
 console.log(sum)

Execution time

We assume the two functions userSync(1)and userSync(2) takes 2 seconds to executes.


Blocking IO

When both the functions get executes the program waits 2 seconds untill it executes other functions.


Image

Non-blocking IO

The same functions have been used in the non blocking IO. When the function getUeser 1 and getUser 2 get executes the program does not wait for the two functions to finish.


Image

The non-blocking IO code takes half the time to execute as compared to blocking IO.

Blocking IO is synchronous programming and non-blocking IO is asynchronous programming.


Synchronous programming

Synchronous program gets executed from top to bottom line by line sequentially. It waits for every function to complete before moving to another line. As we saw in the above blocking IO example.


Asynchronous programming

Asynchronous program gets executed same as synchronous program from top to bottom but the only difference is that is does not wait for the function to finish.

Before we start Asynchronous programming, there are few concepts that need further explanation:


Call stack

Is a simple data structure. Its job is to track the execution of the program and it does that by keeping track of the functions that are currently running. Call stack works as a FILO (first in last out). Whichever function gets added to call stack first, gets executed last.


Call back

Is when a function gets added to call stack and that function is an Asynchronous function. Then call stack will add that function to the call back. That function then waits there until the criteria are met (if the function needs to wait 2 seconds). Once the function is ready it gets added to call back queue where it waits in a queue to be executed.


Call back queue

When a function is ready for execution call back adds the function to the end of call back queue. It maintains the list of all the of the call back functions that are ready to get executed.


Event loop

Event loop looks at two things: call stack and call back queue. If the call stack is empty it is going to run items from the call back queue. The event loop needs to wait for the call stack to be empty before it adds the call back function to the call stack.


Now let’s look at an Asynchronous program. I have used setTimeout() which is an Asynchronous function. Read more about timers in Node.js here.


console.log('Starting')

 setTimeout(()=> {
    console.log('2 Second Timer')
 }, 2000)

 setTimeout(()=> {
    console.log('0 second timer')
 }, 0)

 console.log('Stopping')

The output of the above program:


Starting
 Stoping
 0 second timer
 2 second timer

If the above program was synchronous, then the output would have been something like this:


Starting
 2 second timer (program waits 2 seconds doing nothing else)
 0 second timer
 Stopping

What is going behind the scenes?

The call stack keeps track of all the functions. Let’s have a step by step look at how it is done.

  1. The main function gets added to the call Stack which starts the execution of the program.
  2. Next log (log is a function) gets added to the call stack. It prints “Starting” to the console and then log gets removed as the function completes.
  3. The next function is setTimeout (2seconds) it is, an asynchronous function. A new event gets registered to the callback API and it waits 2 seconds there. The call stack moves this event to call back, the 2-second clock then starts ticking down. While we wait those two seconds the call stack runs other functions.
  4. Another setTimeout (0 second) function is set to add yet another event in the call back API where the event is 0 seconds. And at that point, we now have two events waiting in the background. We can continue to do other things while both of those are waiting for the events to complete.
    While the 0 seconds are up, this call back needs to get executed. This is where the callback queue and the event loop come into play. When a given event is done, in this case 0 seconds, the timer is complete. That call back function is going to get added to the call back queue. Every new ready to execute call back function is added to the back of the queue. The front item is the one that will get executed first. In this case, since there aren’t any items in the list the call back is added right up-front. The call back function is now ready to get executed but before it can be executed it needs to be added on to the call stack. Now it’s time for the event loop to do its job. Since the call stack is not empty the main function continues to run. The callback function cannot run and needs to wait.
  5. The next thing is the last log function is added to the call stack and it prints “Stopping” to the console (after step 2, step 5 get executed the program does not wait for step 3 and 4 to finish). The function gets popped off the call stack and at this point, the main function is done and gets removed from the call stack. In the regular synchronous scripts, this is when the program finishes the end of main means the end of the application. This is not the case with the asynchronous program. Now the event loop can start to do its job it can see that call stack is empty. It takes the first function from the callback queue and moves if to the call stack so the callback can run. At this stage, the call back is running, and it prints “0 Second timer” to the console. None of the asynchronous callbacks are going to run before the main function is done.
  6. At this point the program is not done yet. Both the call stack and call back queues are empty which means the event loop cannot do anything and the program sits there for 2 seconds. When the other event is done the callback pushes it to the call back queue. The event loops then add this to the call stack since the call stack is empty. The function gets executed and it prints “2 Second timer” to the console.

The asynchronous code will execute in a different sequence which means the programmer has to make sure the code works every time it executes.

Asynchronous code is not always the best option to use e.g. if we want to read a file in our program and then delete the file. If we do this asynchronously the file will get deleted first without the code reading the file.

Asynchronous code is great for fetching functions when we don’t know how long it will take to run the function. Another good example for when to use asynchronous code is when making database calls. e.g. If a program makes 100 database calls and is still making more database calls it will not wait for the database to return the data it will keep executing the next line of code.


About the Author

Sameer Humayun

Software Developer


Sameer is a software developer at Optima Systems. He studied BSC Computer Science at university and recently completed his studies. After graduating from university, he started at Optima Systems.


More Posts



Other Posts