BasicsImpCodeStory_Concurrency.html
copyright © James Fawcett
Revised: 05/11/2026
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