It's particularly useful to understand what this line of code actually does:
Promise.all([await3seconds(), await3seconds(), await3seconds(), await3seconds(), await3seconds()]).then(...)
That is fundamentally the same as:
const p1 = await3seconds();
const p2 = await3seconds();
const p3 = await3seconds();
const p4 = await3seconds();
const p5 = await3seconds();
Promise.all([p1, p2, p3, p4, p5]).then(...)
What I'm trying to show here is that ALL your functions are executed serially one after the other in the order declared and they have all returned BEFORE Promise.all()
is even executed.
So, some conclusions from that:
Promise.all()
didn't "run" anything. It accepts an array of promises and it justs monitors all those promises, collects their results in order and notifies you (via .then()
or await
) when they are all done or tells you when the first one rejects.
- Your functions are already executed and have returned a promise BEFORE
Promise.all()
even runs. So, Promise.all()
doesn't determine anything about how those functions run.
- If the functions you were calling were blocking, the first would run to completion before the second was even called. Again, this has nothing to do with
Promise.all()
before the functions are all executed before Promise.all()
is even called.
- In your particular example, your functions each start a timer and immediately return. So, you essentially start 5 timers within ms of each other that are all set to fire in 3 seconds.
setTimeout()
is non-blocking. It tells the system to create a timer and gives the system a callback to call when that timer fires and then IMMEDIATELY returns. Sometime later, when the event loop is free, the timer will fire and the callback will get called. So, that's why all the timers are set at once and all fire at about the same time. If you wanted them to each be spread out by 3 seconds apart, you'd have to write this code differently, either to set increasing times for each timer or to not start the 2nd timer until the first one fires and so on.
So, what Promise.all()
allows you to do is to monitor multiple asynchronous operations that are, by themselves (independent of Promise.all()
) capable of running asynchronously. Nodejs itself, nothing to do with Promise.all()
, has the ability to run multiple asynchronous operations in parallel. For example, you can make multiple http requests or make multiple read requests from the file system and nodejs will run those in parallel. They will all be "in flight" at the same time.
So, Promise.all()
isn't enabling parallelism of asynchronous operations. That capability is built into the asynchronous operations themselves and how they interact with nodejs and how they are implemented. Promise.all()
allows you to track multiple asynchronous operations and know when they are all done, get their results in order and/or know when there was an error in one of the operations.
If you're curious how timers work in nodejs, they are managed by libuv which is a cross platform library that nodejs uses for managing the event loop, timers, file system access, networking and a whole bunch of things.
Inside of libuv, it manages a sorted list of pending timers. The timers are sorted by their next time to fire so the soonest timer to fire is at the start of the list.
The event loop within nodejs goes in a cycle to check for a bunch of different things and one of those things is to see if the current head of the timer list has reached its firing time. If so, it removes that timer from the list, grabs the callback associated with that timer and calls it.
Other types of asynchronous operations such as file system access work completely differently. The asynchronous file operations in the fs
module, actually use a native code thread pool. So, when you request an asynchronous file operation, it actually grabs a thread from the thread pool, gives it the job for the particular file operation you requested and sends the thread on its way. That native code thread, then runs independently from the Javascript interpreter which is free to go do other things. At some future time when the thread finishes the file operation, it calls a thread safe interface of the event loop to add a file completion event to a queue. When whatever Javascript is currently executing finishes and returns control back to the event loop, one of the things the event loop will do is check if there are any file system completion events waiting to be executed. If so, it will remove it from the queue and call the callback associated with it.
So, while individual asynchronous operation can themselves run in parallel if designed appropriately (usually backed by native code), signaling completion or errors to your Javascript all runs through the event loop and the interpreter is only running one piece of Javascript at a time.
Note: this is ignoring for the purposes of this discussion, the WorkerThread
capability which actually fires up a whole different interpreter and can run multiple sets of Javascript at the same time. But, each individual interpreter still runs your Javascript single threaded and is still coordinated with the outside world through the event loop.