Concurrency
Concurrency is the composition of independently executing processes or threads — a structural property of a program rather than a guarantee of parallelism. Two computations are concurrent if they can overlap in time; they are parallel only if they literally execute simultaneously on separate processors. This distinction matters because concurrent code must be correct under arbitrary interleaving of operations even on a single core. The dominant models for expressing concurrency are shared-memory threads with locks, message-passing (actors, channels), and event-driven callbacks or async/await.
Key Concepts
Threads and Processes — A process owns its address space; threads within a process share memory. Threads are cheaper to create and faster to communicate through, but shared state creates correctness hazards. Operating systems schedule threads preemptively, so a thread can be interrupted between any two machine instructions.
Race Conditions — A race condition occurs when the correctness of a program depends on the relative timing of concurrent operations. The classic example is two threads both reading a counter, incrementing it, and writing it back: if both reads happen before either write, one increment is lost. Race conditions are notoriously hard to reproduce because they are timing-dependent.
Mutual Exclusion (Mutex) — A mutex is a synchronization primitive that enforces that only one thread executes a critical section at a time. A thread acquires the lock before entering the critical section and releases it on exit. Mutexes prevent races but introduce new hazards: if a thread holds a lock and crashes without releasing it, other threads block forever.
Deadlock — Deadlock occurs when two or more threads each hold a resource the other needs, and none can proceed. The four necessary conditions (Coffman conditions) are: mutual exclusion, hold-and-wait, no preemption, and circular wait. Breaking any one condition prevents deadlock. A common strategy is lock ordering — always acquire locks in a globally consistent sequence to eliminate circular wait.
Atomic Operations — Modern CPUs provide atomic instructions (compare-and-swap, fetch-and-add) that execute indivisibly with respect to other processors. Lock-free data structures rely on these to coordinate without mutexes, trading simplicity for throughput in high-contention scenarios.
Memory Models — Compilers and CPUs reorder instructions for performance. A memory model defines which reorderings are legal and when one thread’s writes become visible to another. Java and C++ have explicit memory models; Go and Rust expose happens-before guarantees. Writing correct concurrent code requires understanding the model the language guarantees — not just what the hardware does.
Async/Await and Event Loops — Single-threaded event loops (Node.js, Python asyncio) achieve concurrency through cooperative multitasking: a coroutine yields control at await points, allowing other coroutines to run. This avoids shared-state races entirely but introduces its own hazard: a coroutine that never yields starves the event loop.
Common Pitfalls
- Assuming that
i++or any multi-step read-modify-write is atomic - Holding a lock while calling external I/O (maximizes contention, risks deadlock)
- Using
volatileas a substitute for proper synchronization (it prevents caching but not reordering) - Ignoring the memory model and assuming sequential consistency
Best Practices
- Keep critical sections as short as possible to reduce contention
- Prefer higher-level concurrency primitives (channels, queues, thread pools) over raw locks when available
- Use immutable data structures where sharing is needed — they require no synchronization
- Instrument with race detectors early (
-racein Go, ThreadSanitizer in C/C++)