Coding Practices for Concurrency
Immutability, RAII locking, const-correctness as a concurrency tool, minimizing and confining shared state, and the naming and documentation conventions that make a threading contract legible without reading the implementation.
What we’ll cover
- Make immutability the default for any data shared across threads, reducing the surface that needs synchronization at all.
- Reaffirm RAII locking and const-correctness as baseline discipline, not optional polish.
- Cover minimizing shared state through thread confinement, so most data never needs a lock in the first place.
- Establish naming and ownership conventions that make it obvious, from the type alone, which thread or scope owns a piece of state.
Every chapter so far has been about a mechanism: a primitive, a pattern, a way of structuring work across threads. This one is about the coding habits that decide whether those mechanisms stay correct once a second engineer touches the code, six months after the first one got it right under deadline. None of what follows is novel in isolation — RAII locking and const-correctness are old advice — but concurrent code is where skipping them stops being a style complaint and starts being a race that reproduces once a quarter, under load, on a host nobody is watching.
Immutability by Default
The cheapest way to avoid synchronizing access to data is to make the data impossible to write to after it’s constructed. An immutable value shared across threads needs no lock for reads, because there is no write for a read to race with — every thread sees the same bytes for the object’s entire lifetime, and Atomics and the Memory Model’s acquire/release reasoning only has to cover the single act of publishing the object, not every subsequent access to it. Publication itself still needs care: a pointer to a newly constructed immutable object has to become visible to other threads through a mechanism with the right ordering — std::call_once for a lazily constructed singleton, a std::latch gating readers until a value is ready, or a release store paired with an acquire load on the pointer that exposes it — but that’s a one-time cost paid at construction, not a per-access cost paid on every read.
The common structural pattern this enables is a snapshot: rather than mutating a shared object in place under a lock, construct a new immutable version and publish it by swapping a pointer. Readers holding the old pointer keep reading a consistent, if slightly stale, value, and a std::shared_ptr<const T> (or a plain atomic pointer swap) makes the old version’s deallocation automatic once the last reader releases it. This trades a lock held for every read for an atomic swap held for none of them — readers never block each other or the writer, at the cost of occasionally reading a version a step behind the latest write. For configuration, routing tables, and reference data, read far more often than changed, that trade is almost always right, and it’s the same shape Lock-Free Data Structures used for a single value, generalized to an entire object.
Value semantics reinforce the habit at a smaller scale: passing a small object by value into a function running on another thread, rather than a pointer to a shared mutable one, means that function operates on its own copy and can never race with the original. The cost is a copy; the benefit is that an entire category of aliasing bug — two threads holding different names for the same mutable memory — cannot occur, because there is no shared memory for it to occur in.
RAII Locks and const-Correctness
A bare mutex.lock() paired with a bare mutex.unlock() is a bug waiting for an exception or an early return between the two calls. Every lock acquisition in this book’s examples, and every lock acquisition in production code that wants to survive a code review, goes through an RAII guard: std::lock_guard for the single-mutex, no-early-unlock case, std::unique_lock when the lock needs to be released early, waited on with a condition variable, or transferred, and std::scoped_lock when more than one mutex needs to be held at once, because it takes all of them through std::lock’s deadlock-avoiding algorithm rather than acquiring them one at a time in an order that might differ between two call sites. The guarantee all three provide is the same: the lock releases on every exit path — normal return, early return, or an exception unwinding through the scope — without a single line of cleanup code at each exit point. A codebase with a bare lock()/unlock() pair anywhere in it has, by construction, at least one path where an exception leaves that mutex held forever, and the bug is invisible until the exact exception that triggers it fires in production.
const-correctness earns a second job in concurrent code beyond its usual role of catching accidental mutation at compile time: a const member function is a signal to a caller that the call is read-only and, by implication, safe to call from multiple threads at once — provided the type’s invariants hold under concurrent reads, which is exactly the caveat that makes const a hint rather than a guarantee. A const accessor that quietly maintains a mutable cache (a memoized result stored in a mutable member, recomputed lazily on first access) is not thread-safe just because its signature promises not to mutate observable state; two threads calling it concurrently can race on the cache write even though neither call looks, from its signature, like a write at all. The fix is not to avoid mutable caches — they’re a legitimate pattern — but to pair every mutable member with its own synchronization (commonly a mutable std::mutex alongside the cache, locked inside the const method) so the method’s const qualifier and its actual thread-safety agree. The practice this chapter asks for is narrower than “use const more”: it’s that a const method’s thread-safety claim should be verifiable by reading its body, not assumed from its signature, and that any mutable state inside one is a flag for exactly that check.
Minimizing Shared State and Thread Confinement
The data that needs no lock at all is the data no other thread can see, and the single highest-leverage habit in concurrent code is reducing how much data falls into that category versus how much has to be shared. Thread Pools and Work Scheduling’s work-stealing deque is itself an instance of this principle applied to a queue: each worker’s own end of its deque is effectively confined, touched by that worker alone in the common case, and only the rare steal crosses the boundary — which is exactly why the common case needs no lock while the rare one does. The same idea scales down to ordinary counters and accumulators: a per-thread, cache-line-padded counter that each thread increments locally and that gets summed only at a well-defined reduction point (end of a batch, export interval, shutdown) avoids every one of the cache-coherence round trips a single shared atomic counter pays on every increment under contention, the false-sharing cost Mechanical Sympathy covered in detail.
thread_local storage is the language’s direct tool for this: a thread_local accumulator, buffer, or random-number generator state needs no synchronization whatsoever, because the language guarantees each thread its own instance, and the only discipline required is remembering that a thread_local’s lifetime is tied to the thread, not to any particular call, which matters for thread-pool workers that outlive any single task. The Actor Model and Channels generalized confinement into a full design discipline — an actor’s private state is confined to its own thread for its entire lifetime, and the only way anything crosses that boundary is a moved message through a mailbox — and that generalization is worth defaulting toward whenever a component’s correctness is easier to state as “one thread owns this, always” than as “multiple threads touch this, carefully.” Message passing, not shared mutable memory, is the mechanism that moves data between confined regions without ever letting two threads hold a mutable handle to the same object at once.
Naming and Ownership Discipline
A reviewer looking at a function signature or a type name, without reading the implementation, should be able to tell whether a call is safe to make concurrently, and naming is the cheapest tool for making that true. A Monitor<T> wrapper — a small template holding a T and a std::mutex together, exposing access only through an accessor that returns a lock-holding handle — encodes the lock requirement in the type system rather than a comment: there is no way to touch the wrapped T without going through the accessor, because the raw member is private and nothing else exposes it. This is strictly better than a data member named data_ next to a comment reading “protected by mutex_,” because the compiler enforces the wrapper’s rule and cannot enforce the comment’s.