'Difference between Serial and parallel

I am fetching some persons from an API and then executing them in parallel, how would the code look if i did it in serial? Not even sure, if below is in parallel, i'm having a hard time figuring out the difference between the two of them.

I guess serial is one by one, and parallel (promise.all) is waiting for all promises to be resolved before it puts the value in finalResult?

Is this understood correct?

Below is a snippet of my code.

Thanks in advance.

const fetch = require('node-fetch')
    const URL = "https://swapi.co/api/people/";

    async function fetchPerson(url){

        const result = await fetch(url);
        const data = await result.json().then((data)=>data);
        return data ;
    }

    async function printNames() {
      const person1 = await fetchPerson(URL+1);
      const person2 = await fetchPerson(URL+2);
      let finalResult =await Promise.all([person1.name, person2.name]);
      console.log(finalResult);
    }

    printNames().catch((e)=>{

      console.log('There was an error :', e)

      });


Solution 1:[1]

Let me translate this code into a few different versions. First, the original version, but with extra work removed:

const fetch = require('node-fetch')
const URL = "https://swapi.co/api/people/";

async function fetchPerson(url){
    const result = await fetch(url);

    return await result.json();
}

async function printNames() {
  const person1 = await fetchPerson(URL+1);
  const person2 = await fetchPerson(URL+2);

  console.log([person1.name, person2.name]);
}


try {
  await printNames();
} catch(error) {
  console.error(error);
}

The code above is equivalent to the original code you posted. Now to get a better understanding of what's going on here, let's translate this to the exact same code pre-async/await.

const fetch = require('node-fetch')
const URL = "https://swapi.co/api/people/";

function fetchPerson(url){
    return fetch(url).then((result) => {
      return result.json();
    });
}

function printNames() {
  let results = [];

  return fetchPerson(URL+1).then((person1) => {
    results.push(person1.name);

    return fetchPerson(URL+2);
  }).then((person2) => {
    results.push(person2.name);

    console.log(results);
  });
}


printNames().catch((error) => {
  console.error(error);
});

The code above is equivalent to the original code you posted, I just did the extra work the JS translator will do. I feel like this makes it a little more clear. Following the code above, we will do the following:

  1. Call printNames()
  2. printNames() will request person1 and wait for the response.
  3. printNames() will request person2 and wait for the response.
  4. printNames() will print the results

As you can imagine, this can be improved. Let's request both persons at the same time. We can do that with the following code

const fetch = require('node-fetch')
const URL = "https://swapi.co/api/people/";

function fetchPerson(url){
    return fetch(url).then((result) => {
      return result.json();
    });
}

function printNames() {
  return Promise.all([fetchPerson(URL+1).then((person1) => person1.name), fetchPerson(URL+2).then((person2) => person2.name)]).then((results) => {
    console.log(results);
  });
}


printNames().catch((error) => {
  console.error(error);
});

This code is NOT equivalent to the original code posted. Instead of performing everything serially, we are now fetching the different users in parallel. Now our code does the following

  1. Call printNames()
  2. printNames() will
    • Send a request for person1
    • Send a request for person2
  3. printNames() will wait for a response from both of the requests
  4. printNames() will print the results

The moral of the story is that async/await is not a replacement for Promises in all situations, it is syntactic sugar to make a very specific way of handling Promises easier. If you want/can perform tasks in parallel, don't use async/await on each individual Promise. You can instead do something like the following:

const fetch = require('node-fetch')
const URL = "https://swapi.co/api/people/";

async function fetchPerson(url){
  const result = await fetch(url);

  return result.json();
}

async function printNames() {
  const [ person1, person2 ] = await Promise.all([
    fetchPerson(URL+1),
    fetchPerson(URL+2),
  ]);

  console.log([person1.name, person2.name]);
}


printNames().catch((error) => {
  console.error(error);
});

Disclaimer

printNames() (and everything else) doesn't really wait for anything. It will continue executing any and all code that comes after the I/O that does not appear in a callback for the I/O. await simply creates a callback of the remaining code that is called when the I/O has finished. For example, the following code snippet will not do what you expect.

const results = Promise.all([promise1, promise2]);

console.log(results);  // Gets executed immediately, so the Promise returned by Promise.all() is printed, not the results.

Serial vs. Parallel

In regards to my discussion with the OP in the comments, I wanted to also add a description of serial vs. parallel work. I'm not sure how familiar you are with the different concepts, so I'll give a pretty abstraction description of them.

First, I find it prudent to say that JS does not support parallel operations within the same JS environment. Specifically, unlike other languages where I can spin up a thread to perform any work in parallel, JS can only (appear to) perform work in parallel if something else is doing the work (I/O).

That being said, let's start off with a simple description of what serial vs. parallel looks like. Imagine, if you will, doing homework for 4 different classes. The time it takes to do each class's homework can be seen in the table below.

Class | Time
1     | 5
2     | 10
3     | 15
4     | 2

Naturally, the work you do will happen serially and look something like

You_Instance1: doClass1Homework() -> doClass2Homework() -> doClass3Homework() -> doClass4Homework()

Doing the homework serially would take 32 units of time. However, wouldn't be great if you could split yourself into 4 different instances of yourself? If that were the case, you could have an instance of yourself for each of your classes. This might look something like

You_Instance1: doClass1Homework()
You_Instance2: doClass2Homework()
You_Instance3: doClass3Homework()
You_Instance4: doClass4Homework()

Working in parallel, you can now finish your homework in 15 units of time! That's less than half the time.

"But wait," you say, "there has to be some disadvantage to splitting myself into multiple instances to do my homework or everybody would be doing it."

You are correct. There is some overhead to splitting yourself into multiple instances. Let's say that splitting yourself requires deep meditation and an out of body experience, which takes 5 units of time. Now finishing your homework would look something like:

You_Instance1: split() -> split() -> doClass1Homework()
You_Instance2:            split() -> doClass2Homework()
You_Instance3:                       doClass3Homework()
You_Instance4:                       doClass4Homework()

Now instead of taking 15 units of time, completing your homework takes 25 units of time. This is still cheaper than doing all of your homework by yourself.

Summary (Skip here if you understand serial vs. parallel execution)

This may be a silly example, but this is exactly what serial vs. parallel execution looks like. The main advantage of parallel execution is that you can perform several long running tasks at the same time. Since multiple workers are doing something at the same time, the work gets done faster.

However, there are disadvantages. Two of the big ones are overhead and complexity. Executing code in parallel isn't free, no matter what environment/language you use. In JS, parallel execution can be quite expensive because this is achieved by sending a request to a server. Depending on various factors, the round trip can take 10s to 100s of milliseconds. That is extremely slow for modern computers. This is why parallel execution is usually reserved for long running processes (completing homework) or when it just cannot be avoided (loading data from disk or a server).

The other main disadvantage is the added complexity. Coordinating multiple tasks occurring in parallel can be difficult (Dining Philosophers, Consumer-Producer Problem, Starvation, Race Conditions). But in JS the complexity also comes from understanding the code (Callback Hell, understanding what gets executed when). As mentioned above, a set of instructions occurring after asynchronous code does not wait to execute until the asynchronous code completes (unless it occurs in a callback).

How could I get multiple instances of myself to do my homework in JS?

There are a couple of different ways to accomplish this. One way you could do this is by setting up 4 different servers. Let's call them class1Server, class2Server, class3Server, and class4Server. Now to make these servers do your homework in parallel, you would do something like this:

Promise.all([
  startServer1(),
  startServer2(),
  startServer3(),
  startServer4()
]).then(() => {
  console.log("Homework done!");
}).catch(() => {
  console.error("One of me had trouble completing my homework :(");
});

Promise.all() returns a Promise that either resolves when all of the Promises are resolved or rejects when one of them is rejected.

Solution 2:[2]

Both functions fetchPerson and printNames run in serial as you are awaiting the results. The Promise.all use is pointless in your case, since the both persons already have been awaited (resolved).

To fetch two persons in parallel:

const [p1, p2] = await Promise.all([fetchPerson(URL + '1'), fetchPerson(URL + '2')])

Given two async functions:

const foo = async () => {...}
const bar = async () => {...}

This is serial:

const x = await foo()
const y = await bar()

This is parallel:

const [x,y] = await Promise.all([foo(), bar()])

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 lipp