Code Story

Chapter #10 – Concurrency

threads, synchronization, channels, async/await, atomics

10.0  Prologue

Concurrency is the composition of independently executing computations. It lets a program overlap I/O with CPU work, use multiple CPU cores, and remain responsive while waiting for external events. The central challenge is correctness: ensuring that concurrent access to shared state does not produce races, deadlocks, or inconsistent results.

10.1  Threads

An OS thread is an independently scheduled execution unit within a process. Threads share the process’s address space, enabling low-latency communication but requiring explicit synchronization. // Rust: std::thread::spawn; join handle waits for completion use std::thread; let handle = thread::spawn(|| { println!("from thread"); }); handle.join().unwrap(); // C++ (C++11): std::thread #include <thread> std::thread t([]{ std::cout << "from thread\n"; }); t.join(); // C#: System.Threading.Thread or Task var t = new Thread(() => Console.WriteLine("from thread")); t.Start(); t.Join(); // Python: threading.Thread (GIL limits true parallelism for CPU work) import threading t = threading.Thread(target=lambda: print("from thread")) t.start(); t.join() Python’s Global Interpreter Lock (GIL) prevents multiple threads from executing Python bytecode simultaneously, making threads unsuitable for CPU-bound parallelism. Use multiprocessing or extension modules (NumPy, etc.) that release the GIL for CPU-bound work.

10.2  Synchronization Primitives

Shared mutable state requires synchronization to prevent data races. Common primitives:
  • Mutex (mutual exclusion): only one thread holds the lock at a time. All others block until the lock is released. Used to protect a critical section.
  • RwLock (read-write lock): allows multiple concurrent readers or one exclusive writer. Better throughput than a mutex when reads dominate.
  • Semaphore: a counter that limits the number of concurrent accessors. Used to cap resource consumption (thread pool slots, file handles).
  • Condition variable: lets a thread sleep until another thread signals a state change. Requires a mutex for the predicate check.
// Rust: Mutex wraps the data it protects (prevents access without lock) use std::sync::{Arc, Mutex}; let counter = Arc::new(Mutex::new(0)); let c = Arc::clone(&counter); thread::spawn(move || { let mut n = c.lock().unwrap(); *n += 1; }).join().unwrap(); Rust’s borrow checker enforces that data protected by a mutex is only accessed through the lock guard, preventing the common bug of accessing shared data without holding the lock.

10.3  Channels and Message Passing

Message passing avoids shared state by sending copies (or ownership transfers) of data between threads. A channel is the conduit. // Rust: mpsc (multi-producer, single-consumer) use std::sync::mpsc; let (tx, rx) = mpsc::channel(); thread::spawn(move || tx.send(42).unwrap()); let val = rx.recv().unwrap(); // C#: BlockingCollection or Channel<T> (System.Threading.Channels) var ch = System.Threading.Channels.Channel.CreateUnbounded<int>(); await ch.Writer.WriteAsync(42); int val = await ch.Reader.ReadAsync(); // Python: queue.Queue (thread-safe) import queue q = queue.Queue() threading.Thread(target=lambda: q.put(42)).start() val = q.get() Go popularized the idiom “do not communicate by sharing memory; share memory by communicating.” Rust enforces this via ownership: sending a value through a channel transfers ownership to the receiver, preventing concurrent access to the original.

10.4  Async / Await

Async/await is a cooperative concurrency model. An async function may suspend (yield) at await points without blocking an OS thread, allowing other tasks to run. This is highly efficient for I/O-bound workloads. // Rust: async fn + .await; requires an async runtime (Tokio, async-std) async fn fetch(url: &str) -> Result<String, reqwest::Error> { reqwest::get(url).await?.text().await } // C#: async/await built into the runtime (Task-based) async Task<string> FetchAsync(string url) { return await httpClient.GetStringAsync(url); } // Python: asyncio import asyncio, aiohttp async def fetch(url): async with aiohttp.ClientSession() as s: async with s.get(url) as r: return await r.text() Rust async functions compile to state machines that implement the Future trait. They are zero-cost: no heap allocation per suspension point when using impl Future return types. The executor (runtime) polls futures until they complete.

10.5  Atomic Operations

Atomic operations are hardware-guaranteed to be indivisible: no other thread can observe a partial state. They enable lock-free data structures and counters without mutex overhead. // Rust: std::sync::atomic use std::sync::atomic::{AtomicU64, Ordering}; static COUNTER: AtomicU64 = AtomicU64::new(0); COUNTER.fetch_add(1, Ordering::Relaxed); // C++: std::atomic<T> (C++11) #include <atomic> std::atomic<uint64_t> counter{0}; counter.fetch_add(1, std::memory_order_relaxed); // C#: Interlocked class System.Threading.Interlocked.Increment(ref counter); Memory ordering (Relaxed, Acquire, Release, SeqCst) controls how atomic operations interact with the compiler and CPU reordering. SeqCst is the safest and slowest; Relaxed is fastest but provides no ordering guarantees beyond atomicity of the operation itself. Incorrect ordering leads to subtle races that are hard to reproduce.

10.6  Epilogue

Concurrency multiplies complexity: shared state, scheduling non-determinism, and memory ordering interact in ways that break intuition built from sequential code. Language-enforced safety (Rust’s Send/Sync) and architectural discipline (message passing, immutable sharing) are the most reliable paths to correct concurrent programs. The next chapter covers generics, which let one implementation serve many types.

10.7  References

Rust Fearless Concurrency - The Book
C++ Thread Support - cppreference
C# Threading Best Practices - Microsoft
Python asyncio - Docs