Many Things at Once
A kitchen with one chef is simple. She plans, chops, stirs, plates---one action after another in a clear sequence. Add a second chef, and suddenly: who uses the knife? Who stirs the pot? What if both reach for the salt at the same moment?
This is concurrency---multiple activities happening at the same time, sharing resources, potentially interfering with each other. It's one of the most challenging aspects of programming, and one of the most important.
Why Many Things?
Why would we want multiple things happening at once? Several reasons:
Responsiveness. A user interface shouldn't freeze while loading data. The UI should stay alive---responding to clicks, showing animations---while work happens in the background.
Utilizing hardware. Modern computers have multiple cores. A single sequential program uses only one. Concurrent programs can spread work across all cores.
Modeling the real world. The world is concurrent. Multiple users access a website simultaneously. Multiple sensors report readings. Multiple players make moves. Sometimes the most natural model is concurrent.
Waiting efficiently. Programs spend enormous time waiting: for disk reads, network responses, user input. While waiting for one thing, why not do something else?
Sequential Thinking
Our minds naturally think sequentially. Do this, then that, then the other thing:
wake_up()
make_coffee()
drink_coffee()
start_work() Each step completes before the next begins. The state of the world is predictable at every moment. If make_coffee fails, we know drink_coffee won't happen.
This is the procedural model we started with. Safe. Predictable. Limited.
Concurrent Thinking
Now imagine a household with two people:
Person A: Person B:
wake_up() wake_up()
make_coffee() make_toast()
drink_coffee() eat_toast()
start_work() start_work()Both sequences are internally sequential. But they run at the same time. And here's where it gets interesting: what if both need the stove? What if Person A's coffee-making uses the water that Person B needs for tea?
Interleaving vs Parallelism
There are two ways "at the same time" can happen:
True parallelism: Multiple activities literally happen simultaneously. Two chefs, two stoves, two dishes being prepared at the exact same instant. This requires multiple processors (cores).
Interleaving: A single processor rapidly switches between activities, giving the illusion of simultaneity. One chef, switching between stirring soup and chopping vegetables---never truly doing both at once, but making progress on both.
// True parallelism (two cores)
Core 1: A1 -> A2 -> A3 -> A4
Core 2: B1 -> B2 -> B3 -> B4
// Interleaving (one core, time-sliced)
Core 1: A1 -> B1 -> A2 -> B2 -> A3 -> B3 -> A4 -> B4The distinction matters for performance, but often not for correctness. The challenges of concurrency appear in both cases.
The Fundamental Problem
Here's where concurrency becomes dangerous. Consider a simple counter:
counter = 0
increment():
counter = counter + 1One thread incrementing? After 1000 calls, counter equals 1000. Guaranteed.
Two threads, each incrementing 500 times? Counter should equal 1000. But it might equal 873. Or 912. Or any number between 500 and 1000.
Why? Because counter = counter + 1 isn't atomic. It's actually three steps:
1. Read counter into temporary
2. Add 1 to temporary
3. Write temporary back to counterIf two threads execute these steps interleaved:
Thread A: Thread B:
1. read counter (= 0)
1. read counter (= 0)
2. add 1 (temp = 1)
2. add 1 (temp = 1)
3. write counter (= 1)
3. write counter (= 1)
// Both incremented, but counter is 1, not 2!Threads: The Basic Unit
The most common abstraction for concurrency is the thread---an independent sequence of execution within a program.
main:
thread_1 = spawn(do_task_a)
thread_2 = spawn(do_task_b)
// Both running concurrently now
wait(thread_1)
wait(thread_2)
// Both doneEach thread has its own:
Threads share:
- Call stack (local variables, function calls)
- Instruction pointer (where it is in the code)
That shared memory is both the promise and the peril of threads.
- Heap memory (objects, data structures)
- Global variables
- Open files, network connections
The Costs of Concurrency
Concurrency isn't free:
Complexity. Concurrent code is harder to write, read, and debug. The number of possible execution orderings explodes exponentially.
Overhead. Creating threads, switching between them, and synchronizing access all cost CPU cycles.
Non-determinism. Bugs may appear only under specific timing conditions---running correctly 99 times, failing on the 100th.
Difficult debugging. Adding print statements or breakpoints changes timing, potentially hiding (or creating) bugs.
The Promise of Concurrency
Despite these costs, concurrency is essential:
- Modern CPUs have many cores---sequential programs waste most of the hardware
- Responsive applications require background work
- Distributed systems are inherently concurrent
- Many problems are naturally parallel (processing a million records)
The key is managing concurrency's dangers while capturing its benefits. The next chapters explore how.
We've seen why concurrency matters and why it's dangerous. But much of programming isn't about parallel computation---it's about waiting. Waiting for files, networks, users. The next chapter explores asynchronous programming: how to wait without freezing.