Concurrency and Parallelism Patterns
Reference and agent guide for thread safety, synchronization primitives, async patterns, parallel algorithms, and concurrency failure modes.Agent Trigger
Apply when: Writing or reviewing multithreaded/async code, shared state, locks, or parallel algorithms. Rule of thumb: Prefer immutability/message-passing over shared locks; guard every shared mutable access; watch for deadlock ordering and async pitfalls.Core Concepts
Concurrency — multiple tasks making progress in overlapping time intervals (may share a single CPU via scheduling). Parallelism — tasks executing simultaneously on multiple CPU cores. True parallelism requires multiple hardware execution units. Multithreading — one technique for concurrency: divide a process into threads that share memory and run independently. The key insight: concurrency is a program structure property; parallelism is a runtime execution property. A concurrent program may or may not run in parallel depending on hardware.Thread Safety Fundamentals
A piece of code is thread-safe if it behaves correctly when called concurrently from multiple threads, without external synchronization by the caller. What makes code unsafe:- Multiple threads read and write shared mutable state without coordination
- Operations that appear atomic (e.g.,
counter++) are not — they compile to read/modify/write, which is three separate steps that can interleave
Synchronization Primitives
Mutex (Mutual Exclusion Lock)
Allows only one thread at a time into a critical section. When to use: Protecting a shared data structure from concurrent reads and writes; when the critical section is short and won’t block long. When NOT to use: High-contention hot paths (mutex becomes a bottleneck); when reads vastly outnumber writes (use read-write lock instead). Anti-patterns:- Holding a mutex across I/O or long operations (thread starvation)
- Locking at too coarse a granularity (serializes too much work)
- Locking at too fine a granularity (complex, deadlock-prone)
Semaphore
A counter that controls how many threads can access a resource simultaneously. Mutex is a special case (semaphore with max count = 1). When to use: Rate-limiting access to a bounded resource pool (e.g., max 10 concurrent DB connections); producer-consumer coordination. When NOT to use: Simple mutual exclusion — a mutex is clearer.Read-Write Lock (RWLock)
Multiple readers OR one writer at a time. When to use: Read-heavy workloads (caches, config objects, in-memory indexes) where reads dominate and writes are rare. When NOT to use: Write-heavy or evenly mixed workloads (writer starvation risk; overhead exceeds benefit).Atomic Operations
Hardware-guaranteed indivisible read-modify-write. No lock required. When to use: Simple counters, flags, reference counts, lock-free data structure building blocks. When NOT to use: Complex multi-field updates that must be consistent together — atomics can’t protect multiple variables as a unit.volatile is not an atomic. volatile prevents compiler optimization on memory-mapped I/O; it does not synchronize threads. A common beginner mistake.
Condition Variables
Used with a mutex to block threads until a condition is true. When to use: Producer-consumer; blocking until a queue is non-empty or a resource becomes available. Pattern: Always check the condition in a loop (notif) to guard against spurious wakeups.
Memory Models
Modern CPUs reorder instructions for performance. Compilers do the same. Without explicit synchronization, there is no guarantee that a write in one thread is visible in another. Memory ordering (C++/Rust/Java terminology):
False sharing: Two unrelated variables on the same CPU cache line cause cache invalidation traffic between cores even though there’s no logical sharing. Fix: pad structs to align variables to separate cache lines (
alignas(64) in C++).
Race Conditions
A race condition occurs when program behavior depends on the ordering or timing of thread execution. Detection:- Thread sanitizers (TSan, Helgrind, Java ThreadSanitizer) instrument code to detect data races at runtime
- Code review: any
if (condition) { use(condition) }pattern on shared data without a lock is suspect - Stress testing with many threads and intentional delays
- No shared mutable state — prefer message passing or immutability
- If shared state is necessary, always access it under the same lock
- Use atomic operations for single-variable shared counters/flags
- Design with ownership: one thread owns a piece of data at a time
Deadlock
Deadlock: thread A holds lock X, waits for lock Y; thread B holds lock Y, waits for lock X. Neither can proceed. Four necessary conditions (Coffman): mutual exclusion, hold-and-wait, no preemption, circular wait. Break any one to prevent deadlock. Prevention strategies:- Lock ordering — always acquire locks in the same global order. If every thread takes lock A before lock B, circular wait cannot form.
- Lock timeout — try-lock with a timeout; back off and retry if acquisition fails.
- Single lock — redesign so only one lock is needed for the operation.
- Lock-free / wait-free algorithms — eliminate locks entirely using atomics (complex; use a battle-tested library).
- Acquiring locks in different orders in different call paths
- Calling external code (callbacks, user code) while holding a lock — that code may try to re-acquire the same lock
Async / Await Patterns and Pitfalls
Async/await models concurrency on a single thread (or small thread pool) using cooperative scheduling. No shared memory races — but different failure modes. When to use: I/O-bound workloads (HTTP, DB queries, file reads); high-concurrency servers where threads would be wasteful. When NOT to use: CPU-bound work — blocking the event loop starves all other coroutines. Offload to a thread pool. Common pitfalls:Actor Model
Each actor is an isolated unit of state and behavior. Actors communicate only by sending messages; they never share memory directly. When to use: Systems where independent agents manage their own state (game entities, distributed nodes, user sessions). Eliminates shared-memory races by design. When NOT to use: Low-latency scenarios where message-passing overhead matters; simple in-process data pipelines. Implementations: Erlang/Elixir (built-in), Akka (JVM), Actix (Rust), Swift concurrency (actor keyword).CSP (Communicating Sequential Processes)
Concurrent goroutines/processes communicate via typed channels. Goroutines block on send/receive until the other side is ready (or the channel is buffered). When to use: Go pipelines; producer-consumer; fan-out / fan-in patterns; cleaner than explicit locks for many coordination tasks. When NOT to use: Sharing large data structures (copying is expensive); when you need random access to shared state. Anti-patterns:- Unbuffered channels causing deadlock (sender blocks forever waiting for receiver)
- Leaking goroutines (goroutine blocks on channel receive, no one ever sends)
Parallel Algorithm Patterns
Map-Reduce
Split input into independent chunks (map), process each in parallel, aggregate results (reduce). When to use: Data parallelism over large independent collections (log processing, image batch transforms, ML feature computation). Anti-pattern: Map-reduce where the reduce step has global shared state that requires heavy locking — kills parallelism.Fork-Join
Fork N parallel tasks; join (wait) until all complete; combine results. When to use: Recursive divide-and-conquer algorithms (parallel merge sort, parallel tree traversal); bounded parallel subtasks. Anti-pattern: Forking too-fine-grained tasks — thread creation / scheduling overhead dominates for tiny work units.Thread Pool
Maintain a fixed set of worker threads. Dispatch tasks to the pool rather than spawning a new thread per task. When to use: Almost always — raw thread-per-task creation is expensive. Most language runtimes provide one (JavaExecutorService, Python ThreadPoolExecutor, Go goroutine scheduler).
When NOT to use: CPU-bound work that needs dedicated OS threads with custom affinity (use system threads directly).
Producer-Consumer
Producer threads enqueue work; consumer threads dequeue and process. Decouples production rate from consumption rate via a bounded buffer. When to use: Pipeline stages with different throughput; rate smoothing; background processing queues. Backpressure: When the consumer is slower than the producer, the buffer fills. Handle by: blocking the producer (simple, can starve), dropping items (loss-tolerant), or signaling upstream to slow down (preferred for reliability).Backpressure
Backpressure is explicit signaling from a slow consumer to a fast producer to slow down or stop. Without it, the producer overwhelms memory or drops data silently. Mechanisms:- Blocking bounded queues (producer blocks on
putwhen queue full) - Reactive Streams / async streams with
demandsignals - TCP flow control (the network stack does this automatically)
Deadlock vs. Livelock vs. Starvation
Cross-references
- Distributed Systems — distributed concurrency: consistency models, CAP, distributed locks
- Scalability and Reliability — connection pools, async servers, backpressure at system level
- Software Design Principles — minimize shared mutable state is a core principle, not just a concurrency tip