Concurrency Deep Dive
Beyond the Basics of Doing Several Things at Once
This appendix goes deeper into the concurrency concepts from Chapter 10. These are the patterns quietly running everything from web servers to operating systems, doing several things at once while keeping a straight face about it.
The Actor Model: Departmental Memos
Think of concurrent programs as an organization where departments don’t share filing cabinets (because that leads to chaos). Instead, they send memos back and forth.
Marketing doesn’t walk into Accounting’s office and rummage through their drawers. Instead, Marketing sends a memo: “How much budget do we have for the spring campaign?” Accounting receives it, checks their records, and sends a memo back: “$15,000.” Nobody touches anyone else’s belongings. No race conditions over who’s updating which file. Just polite correspondence.
This is the actor model, and it is a wonderfully civilized approach to concurrency.
Each “actor” in your program:
- Has its own private state (its filing cabinet)
- Receives messages (memos)
- Processes messages one at a time
- Can send messages to other actors
The beauty is that actors never directly interfere with each other. If ten actors all want to know something from Accounting, those requests queue up, and Accounting handles them one at a time. No stepping on toes. No race conditions. It’s like British queueing culture, but for software.
The actor model is particularly beloved in systems that need to be extremely reliable: telephone switches, messaging systems, distributed databases. It’s harder to create subtle bugs when nobody can reach into anyone else’s state and muck about. Each actor minds its own business, and the only way to get anything out of it is to send a message and wait your turn.
Languages like Erlang were built around this model from the ground up. In Erlang, you can have millions of lightweight actors, all sending messages to each other. If one crashes, the others continue merrily along, the way one stalled checkout lane doesn’t close the whole supermarket. It’s concurrency through isolation rather than coordination.
The key insight: sometimes the best way to handle multiple things at once is to keep them separate, and let them talk rather than touch.
Promises and Futures: “I’ll Get Back to You”
You’re at a deli counter. You order a sandwich. The person behind the counter hands you a numbered ticket and says, “I’ll call you when it’s ready.” You don’t stand there, unblinking, watching them slice tomatoes. You sit down, check your phone, chat with a friend. When your number is called, you collect your sandwich.
That ticket is a promise (or a future: different languages use different terms for the same idea).
A promise represents a value that doesn’t exist yet but will eventually. Maybe. Hopefully. It’s a placeholder for a result that’s still being computed. Instead of waiting around for the result, you get a promise, and you can do other things while the work happens in the background.
Promises have three states:
- Pending: The sandwich is being made
- Fulfilled: The sandwich is ready, here it is
- Rejected: Sorry, we are out of bacon
Promises can be chained: each step waits for the previous one, but the whole chain doesn’t block your program. You’re planning your afternoon: “After the sandwich, I’ll eat it. After eating, I’ll pay. After paying, I’ll leave.” But if anything goes wrong (they’re out of bacon), you can catch that error and handle it.
Promises are concurrent programming made more intuitive. Instead of dealing with callbacks within callbacks within callbacks (which old-timers called “callback hell,” and they weren’t being dramatic), you get a clean sequence that doesn’t block your program. You describe what should happen when things are ready, not manage the precise timing yourself.
Backpressure: Drinking from a Firehose
Imagine you are at a buffet with a friend who is terribly enthusiastic about trying everything. They keep loading your plate with food faster than you can eat. Soon your plate is overflowing, food is falling on the floor, and you are frantically trying to chew fast enough to make room for the next helping they are already scooping up.
This is backpressure, and it is what happens when one part of your system produces data faster than another part can consume it.
In real systems, this happens all the time:
- A web scraper downloading data faster than your database can save it
- Users uploading photos faster than your server can process them
- Sensors in a factory generating readings faster than your analysis can handle them
- Logs being written faster than your monitoring system can read them
If you don’t handle backpressure, bad things happen. Memory fills up, programs crash, data gets lost, everything grinds to a halt. The plate hits the floor, in other words.
So what do you do?
Option 1: Push back. You politely tell your friend to slow down. In programming, this means the consumer signals to the producer: “I am overwhelmed. Give me a moment to catch up.”
Option 2: Use a buffer. Put a plate between you and your friend, a waiting area for food. They can load up that plate while you are eating, and when it is full, they must wait. In programming, this is a queue or buffer with a maximum size.
Option 3: Drop data. You tell your friend, “If my plate is full, just throw the extra food away. I cannot eat it anyway.” This sounds wasteful, but sometimes it is the right choice. If you are measuring temperature every millisecond, missing a few readings might not matter.
Option 4: Sample. Instead of every single item, take every tenth one. Your friend loads the plate, you eat one thing and scrape off nine. For metrics and monitoring, this often works perfectly well.
The key is recognizing when backpressure might happen and making a conscious choice about how to handle it. Ignoring it is like hoping your friend will magically slow down. They won’t, and you’ll end up with a mess.
Good concurrent systems think about backpressure from the start. They don’t assume the producer and consumer will politely agree to run at the same speed, any more than the post office and your letterbox ever fill and empty in step. They plan for the mismatch.
Race Conditions: The Coordination Problem
When multiple things happen at once, they can get in each other’s way. And the moment they do, it’s never at a convenient time.
Imagine you and a roommate share a shopping list on the refrigerator. You both check it Thursday morning. It says you are out of milk. You both go shopping independently. You both buy milk. Thursday evening, you open the fridge to find two cartons of milk, both destined to expire before you can drink them.
This is called a race condition, and it is one of the classic headaches of concurrent programming.
Here is how it might look in code:
let counter = 0;
async function increment() {
const current = counter;
await Promise.resolve(); // Some other task can run here...
counter = current + 1;
}
Notice the await sitting in the middle. That’s not decoration, and it’s the whole story. JavaScript runs your code on a single thread, one instruction after another, with no other task allowed to interrupt it mid-sentence. A plain synchronous function always runs start to finish without anyone else getting a word in. But await is a deliberate pause: “I’m stepping out, someone else can have the stage for a moment.” That pause is the only place a race condition can sneak in.
Fire off two calls at once, say with Promise.all([increment(), increment()]), and here’s what happens at that pause:
- Task A reads counter (currently 0), then hits
awaitand steps aside - Task B reads counter (also 0), then hits
awaitand steps aside - Task A resumes, calculates 0 + 1 = 1, writes it back
- Task B resumes, calculates 0 + 1 = 1, writes it back
- Counter is now 1, not 2
You have lost an increment. It vanished into thin air because two tasks stepped on each other’s toes during the gap await left open.
The particularly nasty thing about race conditions is that they are intermittent. They depend on precise timing. The bug might not appear in testing. It might not appear for weeks in production. Then one day, under just the right conditions, it manifests, like a leak that only drips when nobody is standing under it. And it is devilishly hard to reproduce and debug.
This is why experienced programmers get a haunted look in their eyes when you mention concurrency bugs. These bugs don’t reliably appear. They’re like ghosts: there one moment, gone the next, and nobody quite believes you saw one. The Schrödinger’s cat of software defects.
A Moment of Appreciation
Take a step back and look at what’s actually happening here. Your computer is managing hundreds of tasks, switching between them thousands of times per second, maintaining the illusion that they are all progressing smoothly and simultaneously. Your music plays without stuttering while you browse the web, download files, and respond to notifications. The operating system coordinates all of this, ensuring that tasks share resources fairly, preventing them from interfering with each other.
It’s extraordinary. And it’s one of those marvels that becomes invisible through familiarity.
The first computers could do exactly one thing at a time. Early operating systems ran your work in batches: you’d hand in your program and come back later, like dropping film off to be developed. The idea of interactive computing, of many programs running seemingly at once, was once a research frontier. Now it’s so mundane that we get annoyed if an app freezes for half a second.
As a programmer, you’re not just writing code for the computer to execute. You’re writing code that will be juggled with hundreds of other things, that might be paused and resumed at any moment, that needs to play nicely with a whole ecosystem of other running programs. You’re writing one part for an orchestra where every other section is playing something completely different, and nobody handed you the full score.
Once you start thinking about concurrency, you realize the world itself is concurrent. Multiple conversations at a party. Traffic flowing in many lanes. Life isn’t a neat sequence of one thing after another. It’s organized chaos, many things all at once, somehow coordinated into something functional. Programming concurrent systems means teaching computers to handle what comes naturally to humans: doing several things at once, or at least maintaining the convincing illusion that we are.