Executing JavaScript in parallel
Parallelism in JavaScript is a process that, if you understand it, can be extremely useful to you. Parallelism is the process by which multiple operations/tasks can run in parallel (at the same time). The opposite of parallelism is code that runs synchronously/sequentially - code that executes one operation/task at a time.
Let’s say we want to write a program that runs a task of some kind. Let’s keep the task simple for now. All it will do is print 2 lines to the console:
function runTask(): void
{
console.log("Task started...");
console.log("Task completed");
}
function main(): void
{
runTask();
}
main(); // Starting point
This prints out:
Task started...
Task completed
Simple enough. We have a function runTask()
that runs one task synchronously.
Let’s say that we want to update runTask()
to be more complex. So much so that runTask()
now takes 5 seconds to complete. We can simulate a long-running task by introducing a ‘sleep’ function:
/** Sleep function */
const sleep = (ms: number) => {
return new Promise(resolve => {
setTimeout(resolve, ms);
})
}
Here we add a new sleep()
function. You don’t have to worry too much about how this works.
tl;dr - sleep()
returns a new Promise
which can only be resolved until after a timer (setTimeout()
) expires. We control the timer expiration by passing in an ms
(millisecond) parameter. We can use this sleep function to simulate a long-running task. For example:
(async () => {
console.log("I'm tired. I'm going to sleep");
await sleep(5000); // Sleep for 5 seconds.
console.log("I'm waking up. I feel well-rested!");
})();
Using our new sleep()
function, let’s add a 5 second delay to runTask()
:
const sleep = (ms: number) => {
return new Promise(resolve => {
setTimeout(resolve, ms);
})
}
// Add 'async' keyword here so that we can use `await` inside of this function
async function runTask(): Promise<void>
{
console.log("Task started...");
await sleep(5000); // Simulate a long-running task
console.log("Task completed");
}
// Also add 'async' here
async function main(): Promise<void>
{
// Now 'await'-ing runTask() because it's marked as 'async'
await runTask();
}
main();
Task started...
(💤💤💤***5 second sleep***💤💤💤)
Task completed
We’ve just simulated a long running task. Now let’s say that our program needs to run this task/job multiple times. A real world example of this might be that our program needs to read and process several very large text files. Let’s simulate multiple tasks by using a for
loop:
const sleep = /** ...same as before */
// Introduce 'taskNumber' param to track each task
async function runTask(taskNumber: number): Promise<void>
{
console.log(`Task ${taskNumber} started...`);
await sleep(5000);
console.log(`Task ${taskNumber} completed`);
console.log(" "); // Print a new line here
}
const tasksToRun = 5; // Number of tasks to run
async function main(): Promise<void>
{
// Run task multiple times using a loop
for(let i = 1; i <= tasksToRun; i++)
{
await runTask(i);
}
}
main();
Task 1 started...
(💤💤💤***5 second sleep***💤💤💤)
Task 1 completed
Task 2 started...
(💤💤💤***5 second sleep***💤💤💤)
Task 2 completed
Task 3 started...
(💤💤💤***5 second sleep***💤💤💤)
Task 3 completed
Task 4 started...
(💤💤💤***5 second sleep***💤💤💤)
Task 4 completed
Task 5 started...
(💤💤💤***5 second sleep***💤💤💤)
Task 5 completed
So at this point we have a program that runs 5 5-second tasks sequentially. This brings the total runtime of the program to 25 seconds. One of the problems we can run into is if we have hundreds or thousands of long-running tasks to run, the runtime can become unacceptably long.
Right now, our tasks our executing synchronously/sequentially. Task N will block Task N + 1 until Task N finishes. Writing synchronous code for long-running/expensive operations should be avoided whenever possible. We should instead write this code in an asynchronous or parallel fashion.
What does this mean?
Instead of executing tasks one after the other:
We can execute them in parallel (at the same time):
How can we do this? Let’s start by adding the option for our program execute in parallel. We can add a simple boolean variable to control whether we want to behave sequentially or not:
const sleep = /** ...same as before */
async function runTask(taskNumber: number): Promise<void> /** ...same as before */
const tasksToRun = 5;
const parallelMode = false; // New flag that controls 'parallel mode'
async function main(): Promise<void>
{
for(let i = 1; i <= tasksToRun; i++)
{
// If we're in 'non-parallel' (sequential) mode...
if(!parallelMode)
{
await runTask(i);
}
else // otherwise, run tasks in parallel
{
// TODO
}
}
}
main();
We’ve added a check inside of the for
loop. If we’re not in ‘parallel mode’, then execute the code as normal. Otherwise, go to the else
block and run the tasks in parallel.
So, we know that when we use the await
keyword on an asynchronous function call, JavaScript will wait on that function to finish completely before continuing. So if we want to avoid this blocking behavior, all we need to do is NOT use the await
keyword whenever we call runTask()
. Then, each runTask()
call will not block any other tasks from completing.
All tasks will start (pretty much) at the same time and run in the background until they are finished (without blocking one another). So let’s add that functionality:
const sleep = /** ...same as before */
async function runTask(taskNumber: number): Promise<void> /** ...same as before */
const tasksToRun = 5;
const parallelMode = true; // Run in parallel now
async function main(): Promise<void>
{
for(let i = 1; i <= tasksToRun; i++)
{
// Let's IMMEDIATELY start running the task without using `await`.
// So by default, a task will START running in the background (non-blocking).
// We'll store the Promise from 'runTask()' inside of the 'task' variable.
const task = runTask();
// IF we want to run sequentially...
if(!parallelMode)
{
// ...we can decide to await the `task` Promise
await task;
}
// Otherwise, the current task ALREADY started running in parallel, by default
else
{
// No need to await `task` here.
// We can just catch any error that may be thrown
task.catch(error => {
console.log(error)
})
// The next task will immediately start executing when we get back to the top of the loop
}
}
}
main();
So we took runTask()
and called it immediately without using the await
keyword and stored the returned Promise
inside of task
. This will begin running the task in the background (non-blocking). Underneath that, we check if we’re in ‘sequential mode’ (if !parallelMode)
. If so, then we await task
.
❗Note: We are not using parentheses after await task
here because we already called runTask()
above. task
is just storing the result of that call. In this case, task
is storing the Promise
that runTask()
returns.
But if we’re in ‘parallel mode’ we can just NOT await
the task
. The task has already began running and will continue running in the background without blocking the next tasks from starting:
(***These lines are printed IMMEDIATELY***)
Task 1 started..
Task 2 started..
Task 3 started..
Task 4 started..
Task 5 started..
(💤💤💤 ***5 second sleep***💤💤💤)
Task 1 completed
Task 2 completed
Task 3 completed
Task 4 completed
Task 5 completed
Look how different the results are for parallel mode! In this mode, we start executing all tasks pretty much immediately and they will continue to run in the background because we are not await
-ing them.
So now instead of having to wait 25 seconds for the program to finish, we can just wait 5 seconds.