Concurrency & Parallelism Concepts Cheat Sheet
Threads, processes, locks, race conditions, and deadlocks explained alongside async/await, goroutines, and channel-based concurrency patterns.
Key Terms
Core vocabulary for reasoning about concurrent and parallel systems.
- Concurrency- Structuring a program so multiple tasks can make progress during overlapping time periods, not necessarily simultaneously
- Parallelism- Executing multiple computations at literally the same instant, typically across multiple CPU cores
- Race Condition- A bug where the outcome depends on the unpredictable timing or interleaving of concurrent operations on shared state
- Deadlock- Two or more threads wait forever for locks held by each other, so none of them can proceed
- Mutex (Lock)- A synchronization primitive that allows only one thread at a time to access a critical section
- Semaphore- A counter-based primitive that allows up to N concurrent holders of a resource
- Critical Section- The part of code that touches shared resources and must not run concurrently on more than one thread
- Context Switch- The CPU saving one thread's state and loading another's, enabling multitasking on a single core
- Starvation- A thread is perpetually denied the resources it needs because other threads keep getting priority
- GIL (Global Interpreter Lock)- CPython's lock that allows only one thread to execute Python bytecode at a time, limiting CPU-bound thread parallelism
Threading with a Lock
Protecting shared state from race conditions using a mutex.
import threadingcounter = 0lock = threading.Lock()def increment(): global counter for _ in range(100_000): with lock: # acquire on enter, release on exit counter += 1threads = [threading.Thread(target=increment) for _ in range(4)]for t in threads: t.start()for t in threads: t.join()print(counter) # 400000, safe because of the lock
Asyncio Concurrency
Running I/O-bound tasks concurrently on a single thread.
import asyncioasync def fetch(name, delay): await asyncio.sleep(delay) # non-blocking wait return f"{name} done"async def main(): results = await asyncio.gather( fetch("A", 1), fetch("B", 2), fetch("C", 1), ) print(results) # runs concurrently, total time ~2s not 4sasyncio.run(main())
Goroutines & Channels (Go)
Go's lightweight concurrency primitives for message-passing style concurrency.
package mainimport ( "fmt" "sync")func main() { var wg sync.WaitGroup results := make(chan int, 3) for i := 1; i <= 3; i++ { wg.Add(1) go func(n int) { // goroutine: lightweight concurrent function defer wg.Done() results <- n * n }(i) } go func() { wg.Wait() close(results) }() for r := range results { // channel: typed pipe between goroutines fmt.Println(r) }}
Multiprocessing for CPU-Bound Work
Using separate processes to get true parallelism on CPU-bound work, sidestepping the GIL.
from multiprocessing import Poolimport osdef cpu_bound(n): return sum(i * i for i in range(n))if __name__ == "__main__": # Processes have separate memory + interpreters, so the GIL no longer # serializes CPU-bound work -- true parallelism across cores with Pool(processes=os.cpu_count()) as pool: results = pool.map(cpu_bound, [10_000_000] * 4) print(results)
Lock-Free Counter with CAS
Using compare-and-swap via AtomicInteger to update shared state without ever blocking a thread.
import java.util.concurrent.atomic.AtomicInteger;public class Counter { private final AtomicInteger value = new AtomicInteger(0); public void increment() { int current, next; do { current = value.get(); next = current + 1; } while (!value.compareAndSet(current, next)); // CAS retries on contention // Equivalent, built-in: // value.incrementAndGet(); }}// Lock-free: no thread ever blocks; a losing thread just retries instead of waiting
Producer-Consumer with a Condition Variable
Coordinating producer and consumer threads around a shared queue using a threading.Condition.
import threading, collectionsqueue = collections.deque()lock = threading.Lock()not_empty = threading.Condition(lock)def producer(): for item in range(5): with not_empty: queue.append(item) not_empty.notify() # wake one waiting consumerdef consumer(): while True: with not_empty: while not queue: not_empty.wait() # releases the lock while waiting, reacquires on wake item = queue.popleft() print("consumed", item)
Structured Concurrency with TaskGroup
Using asyncio.TaskGroup so sibling tasks are cancelled automatically if one of them fails.
import asyncioasync def fetch(name, delay, fail=False): await asyncio.sleep(delay) if fail: raise RuntimeError(f"{name} failed") return nameasync def main(): # TaskGroup (3.11+): if any child task raises, siblings are cancelled # automatically and the exception propagates -- no orphaned tasks async with asyncio.TaskGroup() as tg: t1 = tg.create_task(fetch("A", 1)) t2 = tg.create_task(fetch("B", 2)) print(t1.result(), t2.result())asyncio.run(main())
Advanced Concurrency Patterns
Models and pitfalls that show up once you move past basic locks and threads.
- Actor Model- Independent actors that only communicate via asynchronous messages, each processing one message at a time
- CSP (Communicating Sequential Processes)- Concurrency model built on synchronous message passing over channels, as used in Go
- Fork/Join- Recursively splitting a task into parallel subtasks, then joining their results back together
- Work Stealing- Idle worker threads steal queued tasks from busy threads' queues to balance load automatically
- Software Transactional Memory (STM)- Shared state updated inside atomic transactions that auto-retry on conflict instead of using explicit locks
- ABA Problem- A CAS check passes because a value returned to its original state after being changed and changed back, hiding a real modification
- False Sharing- Threads on different cores contend over the same CPU cache line even though they modify unrelated variables
- Amdahl's Law- The maximum speedup from parallelizing part of a program is capped by the fraction of work that remains serial
Prefer message passing (channels, queues) over shared mutable state guarded by locks when you can — it eliminates whole classes of race conditions by design, per Go's 'share memory by communicating' philosophy.