How to Reason About JavaScript’s Concurrency Model with Darts

Concurrency vs. parallelism; Promises and the event loop

Justin Harjanto
Code Red

--

On the Listings team here at Redfin, we love throwing darts. While we wait for our servers to boot up, 3 or 4 of us will usually hop onto a quick game of 101.

Team Listing’s dartboard! Filled with 30 different game modes

For those of you not familiar with dart-throwing games, 101 is a game where each player starts with 101 points and tries to get to hit 0 points as fast as possible. Each round, a player will throw 3 darts onto the dartboard; for every successful hit, the number of points scored is subtracted from their current point total. If a player scores more than the total required to reach zero, the player busts. Their score then gets reverted to the score they had before they played the round.

And now you might now be wondering…

Okay, so how does this relate to JavaScript?

Let’s write out some code:

Notice how the loop is structured and take note of a few things:

  • Just as described, each player takes turns throwing darts in a round robin fashion and the while loop does not terminate until there are no more active players.
  • A player is considered active if they have not reached zero points or their server is still starting.
  • If a player’s server finishes starting before they’re done with the game, they won’t get to finish the game.
  • Every player has the capability of blocking. If playRound takes a long time to complete, all other players must wait before throwing.

This is what we refer to as synchronous execution. That is, given two lines of source code, L(n) and L(n + 1), L(n) must complete before L(n + 1). In the context of throwing darts, this means that given two players, P1 and P2, P1’s first round must complete before P2 can start his/her first round.

While this style of playing darts often works well for us when we’re taking a break, sometimes our schedules just don’t match up and it’s difficult to coordinate a good time to play a game.

Finding a solution: Let’s make a promise to play

In lieu of not playing any games of darts all together, one of my coworkers decided to start up a variant of 101. Instead of waiting for people to commit to play, everyone who wants to play can throw darts on their own. After they finish, they write their name on the board, along with how many rounds it took them to hit 0 points. For a bit of a challenge, we bumped up the number of points from 101 to 301. We coined this game mode, Async 301.

Team Listing’s async 301 board!

Once all players had finished their rounds, scoring would then resolve as usual. In code, this would look like:

Notice how each participant has a promise that executes eventually, similar to how each of us in the office promises to play before the end of the day. For those of you who aren’t familiar with promises, here’s a short definition from the MDN web docs:

The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.

In this case, the resulting value is a Player object that contains information about how many rounds it took to finish.

Promises let us express an asynchronous computation. That is, given two lines, L(n) and L(n + 1), L(n) does not have to complete before L(n + 1). When we declare our promises in the map function on lines 1–10, the code does not block and continues execution to line 15 and waits for all of the players to finish their games.

Take note of a couple things about this code snippet:

  • Players now don’t have to wait on one another! Each player can throw darts at their own leisure.
  • Once a player starts their rounds, another player cannot start playing.

Overall, the system is much more flexible, however there are still some issues. In Async 301, each player must finish their game before the next player starts. Thus, if there is a single player playing their async game and just can’t seem to hit 0 points, everyone else that wants to start a game cannot because there’s one dart board with the given restrictions above. Can we do better?

Finding another solution: getting another dartboard

Notice that the dartboard acts as a scarce resource. In the types of games that we play, two players cannot simultaneously throw darts. Everyone wants to play out their rounds on the dartboard, so why don’t we just get more dartboards so more people can play?

Sounds like a great idea! We could represent each game as a thread that would run players’ games. Each thread enables code to be executed at the same time. Having N threads would allow us to have N games going in parallel assuming we’re running on a multi-core system. Let’s write some more code:

… except we can’t.

Huh? Why not? Can’t you spawn a new thread? It’d run in parallel too so it’d be way faster right?

Not exactly. Javascript is an asynchronous single-threaded language, so none of this is possible.

HUH? What does that mean?

Let’s break this definition down word for word. Asynchronous, as previously discussed, means that code does not have to execute in order just as players in Async 301 do not have to play their game in any order. This ties into another important concept to understand which is the fact that asynchronous execution enables concurrency. Concurrent execution means that two or more computations occur in the same time frame. In the case of Async 301, we have two or more players playing over the span of a work day.

But wait… JavaScript can run concurrently so it runs code in parallel too right?

Ah yes, this is a common misconception. Parallel ≠concurrent. Having code run in parallel would mean that two snippets of code are executing at exactly the same time. Parallelism is a means to achieve concurrency. To accomplish this, we would need more than one thread and CPU to run the thread on, and because JavaScript is a single-threaded language, this is impossible to achieve. Just as we don’t have the budget nor the space in the office to get another dart board, it’s not possible for us to play two dart games simultaneously.

So… if JavaScript can’t run code in parallel, how does it run it concurrently?

More precisely, in JavaScript, concurrency is achieved through a mechanism called the event loop. The event loop is effectively an endless loop that is triggered from messages that get put onto a queue. This queue waits for messages from asynchronous functions and runs each of the functions to completion.

Per the MDN web docs, in pseudocode, this can be more or less expressed as:

while (queue.waitForMessage()) {
queue.processNextMessage();
}

Where the queue.waitForMessage() waits for a message to arrive on the queue and once it does receive a message, it processes it and goes back to waiting for another message.

This might seem slightly complicated, so we’ll map this back to our analogy in dart land. This would be like if two players in Async 301, let’s call them Mark and Jason, both wanted to play darts, but Mark just couldn’t seem to hit 0 points on his async game. Jason would then have to patiently wait behind Mark and anyone else who wanted to play would have to line up behind Jason to play. Once Mark finally finishes his game, Jason could then begin his rounds and once Jason finishes, the next person in line could start their game.

Wow, that seems pretty bad… or is it?

Although JavaScript’s concurrency model might seem restrictive, the fact that it is a single threaded language has its advantages. Unlike other multi-threaded languages like Go or Java, which can have race conditions, a situation where two threads are trying to change data shared among threads, are non-existent in JavaScript even with the introduction of HTML5 Web Workers⁰. This greatly reduces the overall complexity of the system because as a developer, you have a guarantee that there is only one line of code being executed at a time.

Closing thoughts

With the explosion of JavaScript over the past few years, it’s important to understand how code is executed on the JavaScript engine along with recognizing its limitations and restrictions. Note that this analogy only scratches the surface and there’s still more to how the JavaScript engine runs. Phillip Roberts has a great talk on everything discussed in this article and more.

So remember, the next time one of your teammates curses trying to debug complicated JavaScript that’s dealing with asynchronous callbacks, just remember, it could be a lot worse if JavaScript’s concurrency model weren’t just as simple as throwing a couple darts.

[0] Web Workers enable developers to create background threads that will run on physical CPUs to allow for true parallel execution. Under the hood, they have their own event loop, call stack, and communicate to the main thread using callbacks. Other native APIs that the browser provides such as the Fetch API for making network requests also run in parallel in an attempt to increase performance.

--

--