Running async/await in parallel

Felipe Lima
Jun 06, 2019
Running async/await in parallel

I love async/await because it makes the code way more readable then with promises.

First, let me explain the base code.

This function delays execution by N milliseconds.

function delay(millis) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, millis);
  });
}

This function waits 1 second and then prints the process name as completed.

async function run(processName) {
  await delay(1000);
  console.log(`Process ${processName} completed.`);
}

This is an anonymous asynchronous block that runs when you execute the script. Ex.: node index.js.

(async () => {
  // Implementations
})();

Sequential

Ok, let's start with the sequential implementation.

(async () => {
  const initialTimestamp = new Date();

  await run("A");
  await run("B");
  await run("C");
  await run("D");

  console.log(
    `All Completed! ${Number(new Date()) - Number(initialTimestamp)}ms.`
  );
})();

Output:

Process A completed.
Process B completed.
Process C completed.
Process D completed.
All Completed! 4017ms.

No secrets here, each process waits for the previous to finish.

Loops - The wrong way

Probably everyone tried to use forEach as the first attempt to run multiple promises at once.

(async () => {
  const initialTimestamp = new Date();

  const processes = ["A", "B", "C", "D"];

  processes.forEach(async process => {
    await run(process);
  });

  console.log(
    `Ops, it didn't wait =/. ${Number(new Date()) -
      Number(initialTimestamp)}ms.`
  );
})();

Output:

Ops, it didn't wait =/. 1ms.
Process A completed.
Process B completed.
Process C completed.
Process D completed.

The problem is that the forEach tries to run the callback code as it is synchronous code. The result is forEach not waiting for the promises to finish.

Loops - The right way (Sequential)

To run async/await synchronously with loops, use for..of.

(async () => {
  const initialTimestamp = new Date();

  const processes = ["A", "B", "C", "D"];

  for (let process of processes) {
    await run(process);
  }

  console.log(
    `All completed. ${Number(new Date()) - Number(initialTimestamp)}ms.`
  );
})();

Output:

Process A completed.
Process B completed.
Process C completed.
Process D completed.
All completed. 4016ms.

for..of is less performatic then forEach, BUT as we are running async code, it doesn't matter.

Loops - In Parallel

Now, to run promises in parallel, we use Promise.all. That's useful when the execution of the next promise doesn't depend on the last — downloading files, for example.

Promise.all accepts an array of promises, so we must build this array as pass it as an argument.

(async () => {
  const initialTimestamp = new Date();

  const processes = ["A", "B", "C", "D"];

  const promises = [];

  processes.forEach(process => {
    promises.push(run(process));
  });

  await Promise.all(promises);

  console.log(
    `All Completed! (But not the best code). ${Number(new Date()) -
      Number(initialTimestamp)}ms.`
  );
})();

Output:

Process A completed.
Process B completed.
Process C completed.
Process D completed.
All Completed! (But not the best code). 1013ms.

Loops - In Parallel (Improved)

Now let's make the code short using the map method. It creates a new array with the result of the callback, which in our case is the promise of the process.

(async () => {
  const initialTimestamp = new Date();

  const processes = ["A", "B", "C", "D"];

  await Promise.all(
    processes.map(async process => {
      await run(process);
    })
  );

  console.log(
    `All Completed! ${Number(new Date()) - Number(initialTimestamp)}ms.`
  );
})();

Output:

Process A completed.
Process B completed.
Process C completed.
Process D completed.
All Completed! 1010ms.