JavaScript Concurrency: Concurrent setTimeouts
Welcome to the Concurrent setTimeouts lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
setTimeoutcan have a delay of 0, which means "do this immediately". However, it's still asynchronous! Whenever we use asetTimeoutwith any delay, even 0, the JavaScript engine will finish running all the synchronous code before calling thesetTimeoutcallback.>
console.log('first');setTimeout(() => console.log('second'), 0);console.log('third');async console outputTimers are asynchronous regardless of how many we create. When we create 3 timers, each with a delay of 0, they will only run once the current code finishes. Timers with a delay of 0 will always execute in the order that they were created.
(In this example, remember that
'first'and'third'will bepushed onto the array before the timers run.)>
const array = [];array.push('first');for (const i of [1, 2]) {setTimeout(() => array.push('second ' + i), 0);}array.push('third');array;Async Result:
['first', 'third', 'second 1', 'second 2']
Since the timeout is 0,
'second 1'waspushed to the array immediately after the other code finished running. What happens when we create timers with timeouts greater than 0? Each will finish when its timeout is reached. In the example below, the'cat'timer will finish before the'dog'timer.>
setTimeout(() => console.log('cat'), 1000);setTimeout(() => console.log('dog'), 2000);async console outputThe order of the
setTimeoutcalls in the code doesn't matter here. If we switch their order, we'll get the same result: the timer with the shorter delay will finish first.>
setTimeout(() => console.log('dog'), 2000);setTimeout(() => console.log('cat'), 1000);async console outputIn those examples, we logged
'cat'after 1000 ms, then logged'dog'after 2000 ms. It's important to note that the total execution time was about 2000 ms, not 3000 ms! The timers begin at the same time, and then finish about 1000 ms and 2000 ms from when they were scheduled.Here's a code problem:
The
setTimeoutcalls below referenceDELAY1,DELAY2, andDELAY3variables that don't exist. Replace those variable references with 100, 200, and 300 to get the timers to run in an order that produces the expected array result.const array = [];setTimeout(() => array.push('cat'), 300);setTimeout(() => array.push('dog'), 100);setTimeout(() => array.push('horse'), 200);array;Async Result:
- Goal:
['dog', 'horse', 'cat']
- Yours:
['dog', 'horse', 'cat']
In our examples using
console.log, the timing is never quite exact. The next example sets a timer for 1000 ms, but the actual time will be slightly longer when you run it:>
setTimeout(() => console.log('finished'), 1000);async console outputWhen we set a 1000 ms timer, it means "wait 1000 ms, then call my function as soon as possible." Computers run at finite speeds, so we have no guarantees about how soon our timers will run. If a lot of other code is waiting to run, a 1000 ms timer could take 30 seconds instead of 1 second! (But you won't encounter anything that extreme in most systems.)
We can create that situation artificially by loading the CPU. The next example sets a timer, then runs a "busy loop" for 500 ms: the loop does nothing except take up time. The JavaScript runtime can't execute multiple blocks of code simultaneously, so this prevents it from moving on for about 500 ms.
>
const now = () => new Date().getTime();const startTime = now();setTimeout(() => console.log('cat'), 0);setTimeout(() => console.log('dog'), 1000);while (now() < startTime + 500) {// Do nothing; we're wasting 500 ms on purpose.}async console outputUnless your computer was very busy doing other things, your first console log should have come out around 500 ms. But it was scheduled for 0 ms! That shows that the timer couldn't finish while our 500 ms "busy loop" was running. However, the second
setTimeoutshould have finished at roughly 1000 ms, since there was nothing blocking it.This brings us to an important point: JavaScript runs one piece of code at a time. (There are some situations where this isn't true, but they're not applicable to this course.) When we write concurrent code, we schedule different code to run at some point in the future, like our
console.logcalls above. But only one piece of code is actually running at any given time.This explains why our "0-second"
setTimeouttook about 500 ms to run. OursetTimeoutcall told the JavaScript engine to "please run this code as soon as possible." But "as soon as possible" had to wait for our 500 ms busy loop to finish.The JavaScript engine maintains a queue of timers and other events, processing them in order. We can think of it like this:
>
while (true) {for (const timer of eventQueue) {if (timer.isPastItsScheduledTime()) {timer.runCallback();eventQueue.removeTimer(timer);}}}When a timer's callback finishes, the callback function begins, runs, and finishes, all without being interrupted. Then the JavaScript engine checks again. If there are other timers ready, they run.
Real JavaScript engines are more complex than the pseudocode above, but thinking about it in this way will get us surprisingly far!