Concurrency Models
There's a famous joke: "Some people, when confronted with a problem, think, 'I know, I'll use threads,' and then two they hav erpoblesms." It gets a laugh because it's true — shared-state concurrency is the source of more subtle, agonizing bugs than almost any other paradigm in computing. But the joke also obscures a deeper question: why do we keep building concurrent systems with tools that make them so hard to reason about?
The answer is partly historical accident. C gave us a sequential abstract machine modelled on the PDP-11, and we've been trying to bolt concurrency onto that model ever since.1 As David Chisnall argues, the quest for instruction-level parallelism — keeping execution units busy on what is fundamentally serial code — led directly to the speculative execution that gave us Spectre and Meltdown. Modern processors have up to 180 instructions in flight at once, a far cry from the one-at-a-time model C programmers carry in their heads. GPUs achieve comparable throughput without any of this ILP complexity, but they demand explicitly parallel programs. The serial abstract machine is a tax we pay for backward compatibility with a language designed in the 1970s.
Threads, Locks, and the Failure of Encapsulation
Object-oriented programming promised encapsulation: the internal state of an object is protected from the outside world. But this promise was always conditional on single-threaded access. The moment two threads enter the same method, encapsulation evaporates. The instructions interleave in arbitrary ways, and no amount of careful API design can save you.
The standard remedy — locks — introduces its own pathology. Locks limit concurrency, create deadlock risks, and scale poorly. Worse, they work only locally. Distributed locks exist, but they're orders of magnitude slower and impose hard limits on horizontal scaling. The Akka.NET documentation makes this case with disarming clarity: OOP's encapsulation model was built for a world of single-threaded execution that hasn't existed for decades.2
There's a deeper problem too. On modern hardware, there is no real shared memory. CPUs pass cache lines to each other explicitly, much like computers on a network exchange packets. The shared-memory abstraction is a convenient fiction maintained by cache coherency protocols — themselves one of the hardest parts of CPU design to get right. As Chisnall points out, most of that coherency complexity exists to support languages where data is both shared and mutable as a matter of course.
Message Passing: Hoare Was Right
Tony Hoare's 1978 paper on Communicating Sequential Processes (CSP) proposed a different approach: connect threads with channels (queues), and let them send messages instead of sharing memory. The advantages are substantial. Each thread enjoys process-like isolation. Inputs and outputs are obvious — just the channels it receives from and sends to. And channels are the synchronization: receivers wait when channels are empty, senders wait when they're full. No locks, no deadlocks, no shared mutable state.
After decades of mutex madness, modern languages have heeded Hoare's advice. Go's goroutines and channels are CSP distilled. Rust provides std::sync::mpsc::sync_channel. Clojure has core.async. Erlang built its entire runtime around message-passing processes with no shared state whatsoever. As one Rust programmer put it, most software can stop here — threads and channels, combined with tools like Rayon for parallelizing CPU-intensive loops, handle the vast majority of concurrency needs.3
The Actor Model
The actor model takes message passing further. Where CSP focuses on the channels, actors focus on the entities that send and receive messages. Each actor has a mailbox, a behavior (its state), and an address. Messages arrive in the mailbox and are processed one at a time, sequentially — which means encapsulation is restored without locks. An actor system can process as many messages simultaneously as there are available cores.
What makes actors especially interesting is how they handle failure. Erlang's "let it crash" philosophy organises actors into supervision trees: when a child actor fails, its parent decides whether to restart it, stop it, or escalate. This resembles how we'd handle critical systems in other engineering disciplines — you build redundancy and recovery into the architecture rather than trying to prevent all failures. Joe Armstrong called it programming for the normal case and letting supervisors handle the abnormal one.
The actor model also maps naturally to distributed systems. If actors communicate only via messages and maintain only local state, there's no fundamental difference between actors on the same machine and actors on different machines. The same code works in both cases, which is why Erlang has been so successful for telecom infrastructure and why Akka powers much of the JVM distributed computing world.
Async/Await: Fast, Contentious
Some problems demand more concurrency than threads can provide. The canonical example is the C10K problem — a web server handling tens of thousands of concurrent connections. At this scale, each connection having its own OS thread is too expensive. The solution: userspace scheduling. Create lightweight tasks (variously called green threads, fibers, or coroutines), and multiplex them onto a small pool of OS threads.
Rust's approach — async/await with stackless coroutines — makes fascinating tradeoffs. The compiler transforms each async function into a state machine that advances at each .await point. These futures are tiny and fast. But async Rust sits in tension with the language's core promise of static lifetime verification. Futures fragment code and data into thousands of pieces, runnable on any thread at any time — the opposite of what Rust's borrow checker wants to verify statically.4
The result is what one critic calls "a much different flavor than normal Rust." Where Haskell or Go hide the difference between blocking and non-blocking code behind a runtime, Rust exposes every seam. Shared state between tasks requires Arc (atomic reference counting), giving you what amounts to the world's worst garbage collector — dynamic lifetimes without the wins of actual GC like better allocation throughput and automatic cycle detection. The async keyword is viral: any function that calls an async function must itself be async, spreading the complexity everywhere.
This is genuinely the hardest open problem in Language Design Philosophy: how much should a language reveal about the underlying execution model? Go and Erlang say "almost nothing" — concurrency is invisible, the runtime handles it. Rust says "almost everything" — you see the state machines, you manage the lifetimes, you choose the runtime. Both are legitimate design points, but they produce radically different programming experiences.
The Database as Shared Mutable State
Martin Kleppmann makes a provocative observation: databases are global, shared, mutable state.5 We've spent decades trying to eliminate shared mutable state within programs — actors, channels, goroutines — but at the system level, we still funnel everything through a mutable database. The typical web application architecture (stateless backends, all state in the database) just moves the concurrency problem to a different layer.
Kleppmann's proposal — turning the database inside out, treating the transaction log as a first-class citizen rather than an implementation detail — is really an argument for applying message-passing principles at the system level. Instead of shared mutable tables, you get streams of immutable events. Instead of caches maintained by brittle application logic, you get materialized views derived from the event stream. The same principles that make actors and CSP easier to reason about can make entire distributed systems more tractable. This connects directly to the ideas in Log Structured Data.
What the Models Share
Beneath the surface differences, all successful concurrency models share an insight: reasoning about concurrent systems requires limiting what can interact with what. Threads plus locks impose no such limits and leave everything to programmer discipline. CSP limits interaction to channels. Actors limit it to mailbox messages. Rust's type system limits it to what the borrow checker can verify. Even hardware memory models (acquire/release semantics, memory barriers) are fundamentally about controlling what is visible to whom.
The Paradox Of Automation applies here too. Shared-state concurrency is dangerous precisely because it feels easy at first. Everything works until it doesn't, and when it doesn't, the failures are nondeterministic nightmares that only manifest on Tuesdays when the load is high. Message-passing disciplines impose upfront costs — more boilerplate, more explicit communication — but they make the easy path the safe path.
Footnotes
Linked from
- Programming Languages Overview
Concurrency Models bridges to hardware architecture: Erlang's message-passing model, if taken seriously, would let hardware designers shed the enormous complexity of cache coherency protocols — the same point that [the C abst