Overview
Prerequisites
- Prior topics: 4 · Just Enough Python -- every
script in this topic is fully type-annotated Python, and you should already be comfortable reading
functions, classes, exceptions, context managers, and
dict/listliterals the way that primer taught them; 7 · Data Structures & Algorithms Essentials -- this topic leans on the queue and deque shapes you already built there for the producer/consumer and work-stealing examples; 22 · Programming Paradigms -- the "reduce shared mutable state" mindset that topic introduced through the functional lens is the same discipline this topic applies under real concurrent execution, where an unprotected mutable variable is not just harder to reason about, it is actively unsafe. - Tools & environment: a macOS/Linux terminal; Python 3.13.12 (stdlib
threading,multiprocessing,asyncio,concurrent.futures,queue,dis,sys-- no third-party packages for examples 1-81);reactivex4.1.0 (RxPY) for the two Observable-based reactive-streams examples (82-83) -- installed only intolearning/code/ex-82-observable-map-filter-rxpy/andlearning/code/ex-83-hot-vs-cold-subscription/via a per-examplerequirements.txt, since every other example in this topic is pure stdlib; pytest 9.1.1 to run each example's test file; pyright in--strictmode (pyrightconfig.jsonat the topic root) -- everyexample.pyandtest_example.pyin this topic passespyright --strictwith zero errors. Optionally, a free-threadedpython3.14tbuild (PEP 703/779) to see examples 4, 58, and 59 exercise true thread-level parallelism instead of the GIL-serialized behavior the standard build shows -- not required, since every free-threading example branches onsys._is_gil_enabled()and is genuinely correct on whichever interpreter actually runs it. - Assumed knowledge: writing Python functions, classes, and loops; running a script from the CLI; reading a stack trace; the general idea that two things running "at once" can interfere with each other, even without prior hands-on concurrency experience.
Why this exists -- the big idea
The problem before the solution: one thing at a time is simple but slow and unresponsive -- the moment two things run at once, shared state corrupts and bugs stop being reproducible. The one idea worth keeping if you forget everything else: don't share mutable state; when you must, protect it -- almost every concurrency bug in this topic, from a lost counter update to a five-way dining-philosophers deadlock, is a shared-state bug wearing a different costume.
Cross-cutting big ideas, taught here and then reused for the rest of this topic: taming-state --
the whole discipline of concurrent programming is containing the state two things can touch, whether
through a lock, a single owner, or a message-passing queue. determinism-vs-emergence -- interleavings
turn deterministic single-threaded code into emergent, order-dependent behavior, which is exactly why a
race condition can pass every test run in isolation and still fail unpredictably under load (see the
stress-harness example, ex-80). This topic covers the core concurrency model every engineer needs --
threads vs processes vs async, synchronization, races and deadlocks, message passing, and parallel
decomposition -- in Python, including the GIL and free-threaded CPython, and closes with a six-example
reactive-streams sub-group (Observables, hot/cold subscriptions, backpressure, and reactive pull) that
applies the same shared-state and flow-control discipline to a push-based, operator-composed stream
model.
Install and run your first example
Confirm the toolchain this topic's Beginner tier needs is installed:
$ python3 --version
Python 3.13.12
$ python3 -c "import threading, multiprocessing, asyncio, concurrent.futures, queue; print('stdlib concurrency primitives OK')"
stdlib concurrency primitives OK
$ python3 -m pytest --version
pytest 9.1.1Every example is a complete, self-contained runnable file colocated under learning/code/ex-NN-slug/,
paired with its own test_example.py, and both files are actually executed to capture the documented
output -- never a fabricated transcript. Run any example directly:
cd learning/code/ex-01-concurrency-vs-parallelism-illustration
python3 example.py
pytest -qTwo examples (82 and 83) need one extra package -- install it inside that example's own directory only:
cd learning/code/ex-82-observable-map-filter-rxpy
pip install -r requirements.txt # reactivex==4.1.0A note on free-threaded CPython: examples 4, 58, and 59 discuss and exercise the free-threaded
(python3.14t) build. PEP 779 made free-threading officially supported as of Python 3.14, but on
macOS the installer's free-threaded option is still literally labeled "[experimental]" in the
python.org installer UI -- supported by the PEP, but the platform installer's own copy has not yet
caught up. All three examples are written to branch on sys._is_gil_enabled(), so they run correctly
and honestly on whichever interpreter actually executes them, standard or free-threaded.
How this topic's examples are organized
- Beginner (Examples 1-28) -- concurrency vs. parallelism, processes vs. threads,
the GIL,
threading.Threadstart/join, the shared-counter race and itsLockfix,RLock,Semaphore,Event,Barrier,Condition,queue.Queue, a basic producer/consumer pipeline,ThreadPoolExecutor,ProcessPoolExecutor,Future, and your firstasynciocoroutine andgather. - Intermediate (Examples 29-57) -- a reproduced two-lock deadlock and two
different fixes (lock ordering, timeout), livelock, starvation, the four Coffman conditions, memory
visibility, data races vs. logic races, bounded-queue backpressure, multi-producer/multi-consumer
queues, a hand-built
Condition-based bounded buffer,as_completed, exception propagation through aFuture,multiprocessingIPC (Queue,Value), pool-vs-serial benchmarks for I/O and CPU work,asyncio.create_task/timeout/Queue/Semaphore, the cooperative-blocking hazard, offloading blocking calls withrun_in_executor, Amdahl's Law, and a map-reduce decomposition. - Advanced (Examples 58-87) -- free-threaded CPython actually scaling CPU-bound
threads, double-checked locking, a hand-built reader-writer lock, deadlock detection via a wait-for
graph, a lock-free single-owner-queue counter,
threading.local(),Future/Taskcancellation,asyncio.wait,TaskGroup, a rate-limited concurrent fetch-and-aggregate, a three-stage pipeline, work-stealing,multiprocessing.shared_memory,asyncio.to_thread, the spurious-wakeup guard, phased-computation barriers, three-way I/O and CPU benchmarks, graceful shutdown, deadlock-free dining philosophers, a race-detecting stress harness, a capstone-preview benchmark, and a self-contained reactive-streams sub-group (Examples 82-87): anObservablewithmap/filterviareactivex(RxPY), hot vs. cold subscriptions, hand-rolled backpressure (buffer vs. latest -- RxPY 4.1.0 ships no built-in backpressure operators), reactive pull viarequest(n), a hand-rolledjava.util.concurrent.Flow-style contract, and an annotated marble diagram of amerge+map+debouncepipeline.
The 33 concepts this topic covers
- co-01 · Concurrency vs. parallelism -- concurrency is dealing with many things by interleaving; parallelism is doing many at once on multiple cores. Examples 1, 81.
- co-02 · Processes vs. threads -- processes have isolated address spaces; threads share one, which is both cheaper and more dangerous. Examples 2, 45, 46.
- co-03 · The GIL -- CPython's global interpreter lock serializes bytecode so only one thread runs Python at a time. Examples 3, 25, 49, 58, 77.
- co-04 · Free-threaded CPython -- the PEP 703/779 no-GIL build (
python3.14t, supported since 3.14) enables true thread parallelism. Examples 4, 58, 59. - co-05 · I/O-bound vs. CPU-bound -- the workload class decides the tool: threads/async win I/O, processes win CPU. Examples 3, 5, 23, 28, 48, 49, 59, 76, 77.
- co-06 · Thread creation and join -- spawn a
threading.Thread,start()it,join()to wait for completion. Examples 5, 6, 7. - co-07 · Shared-mutable-state hazard -- two threads touching one mutable variable is the root of nearly every concurrency bug. Examples 8, 64.
- co-08 · Race condition -- a result that depends on the nondeterministic interleaving of operations. Examples 8, 10, 11, 37, 60, 80.
- co-09 · Data race vs. race condition -- a data race is unsynchronized concurrent access; a race condition is the broader ordering bug. Example 37.
- co-10 · Atomicity -- an operation that completes indivisibly;
x += 1is a non-atomic load-modify-store. Examples 9, 36. - co-11 · Locks and mutexes -- a
threading.Lockmaking a critical section mutually exclusive. Examples 11, 12, 14, 31, 36, 47, 60, 61, 80. - co-12 · Reentrant locks -- a
threading.RLockthe owning thread can re-acquire without self-deadlock. Examples 13, 14. - co-13 · Semaphores -- a counter permitting at most N concurrent holders of a resource. Examples 15, 16, 53, 61, 69.
- co-14 · Condition variables --
wait/notifycoordination of threads around a shared predicate. Examples 19, 41, 74. - co-15 · Events and barriers -- a
threading.Eventone-shot signal and aBarrierN-way rendezvous. Examples 17, 18, 75. - co-16 · Deadlock -- a cyclic wait where each thread holds what another needs (the four Coffman conditions). Examples 29, 30, 31, 34, 62, 79.
- co-17 · Livelock and starvation -- threads active yet making no progress, or one thread perpetually denied its turn. Examples 32, 33.
- co-18 · Lock-ordering discipline -- acquiring locks in one global order to break the circular-wait condition. Examples 30, 62, 79.
- co-19 · Memory visibility -- one thread's writes may not be observed by another without synchronization. Examples 35, 64, 72.
- co-20 · Message passing over shared state -- "communicate by sharing queues, not by sharing memory". Examples 20, 46, 63.
- co-21 · Thread-safe queues --
queue.Queueas a synchronized hand-off channel between threads. Examples 20, 21, 38, 40, 63, 70. - co-22 · Producer-consumer pattern -- decoupling producers from consumers through a bounded buffer. Examples 21, 22, 38, 39, 40, 41, 52, 70, 78.
- co-23 · Thread pools --
concurrent.futures.ThreadPoolExecutorreusing a fixed set of worker threads. Examples 23, 24, 42, 43, 48, 55, 65, 73, 76, 78, 81. - co-24 · Process pools --
ProcessPoolExecutorrunning work in separate processes to sidestep the GIL. Examples 25, 44, 47, 57, 72, 77, 81. - co-25 · Futures and async results -- a
Futureis a placeholder for a result not yet computed. Examples 24, 26, 42, 43, 65. - co-26 · Async/await and the event loop --
asynciosingle-threaded cooperative concurrency driven by an event loop. Examples 27, 28, 50, 51, 52, 53, 54, 66, 67, 68, 69, 76, 81. - co-27 · Cooperative vs. preemptive scheduling --
awaityields voluntarily; OS threads are preempted involuntarily. Examples 54, 55, 67, 73. - co-28 · Parallel decomposition and Amdahl's Law -- splitting map-reduce work across workers; speedup is bounded by the serial fraction. Examples 56, 57, 71, 75.
- co-29 · Reactive streams and backpressure -- the Reactive Streams spec governs async stream
exchange across a boundary with non-blocking backpressure via four interfaces
(Publisher/Subscriber/Subscription/Processor), adopted into the JDK as
java.util.concurrent.Flow(Java 9). Examples 85, 86. - co-30 · Observables and operators -- a ReactiveX
Observableis a push producer (onNext/onError/onCompleted) -- the dual of a pullIterable-- composed by chainable operators (map/filter/take), with marble diagrams visualizing emissions over time. Examples 82, 87. - co-31 · Hot vs. cold streams -- a cold Observable emits only on subscription so every subscriber sees the whole sequence; a hot Observable emits regardless of subscribers, so late subscribers miss earlier items. Example 83.
- co-32 · Backpressure strategies -- when a producer outpaces its consumer the stream applies
buffer / drop / latest / error; the reactive-pull model inverts push by having the subscriber
request(n)its demand so the source emits only what was asked for. Examples 84, 85. - co-33 · Reactive Manifesto vs. FRP -- the Reactive Manifesto (responsive / resilient / elastic / message-driven) is a manifesto, not a spec; Rx-style reactive streams are discrete-event, distinct from original FRP's continuous-time behaviors + discrete events (Elliott & Hudak, 1997). Example 87.
Examples by Level
Beginner (Examples 1-28)
- Example 1: Concurrency vs. Parallelism, Illustrated
- Example 2: Process vs. Thread Address Space
- Example 3: The GIL Serializes CPU-Bound Threads
- Example 4: Detecting a Free-Threaded (No-GIL) Build
- Example 5: I/O-Bound Threads Actually Help
- Example 6: Your First Thread -- start() and join()
- Example 7: Starting Many Threads and Joining Them All
- Example 8: A Shared Counter Without a Lock Loses Updates
- Example 9:
x += 1Is Not One Atomic Step -- Proof viadis - Example 10: A Race's Output Is Nondeterministic Across Runs
- Example 11: A
LockFixes the Racing Counter - Example 12:
with lock:vs Manual acquire()/release() - Example 13:
RLockLets the Owning Thread Re-Acquire - Example 14: A Plain
LockSelf-Deadlocks on Re-Acquire - Example 15: A
Semaphore(2)Limits Concurrent Access - Example 16:
BoundedSemaphoreCatches an Over-Release Bug - Example 17: A
threading.EventSignal - Example 18: A
BarrierRendezvous Point - Example 19:
Condition-- wait() and notify() - Example 20:
queue.Queue-- put() and get() Between Threads - Example 21: A Basic Producer/Consumer Pipeline
- Example 22: A
NoneSentinel Cleanly Shuts Down a Consumer - Example 23:
ThreadPoolExecutor.mapOver I/O Tasks - Example 24:
submit()Returns aFuture;.result()Blocks - Example 25:
ProcessPoolExecutorBeats Threads on CPU Work - Example 26:
add_done_callbackFires on Completion - Example 27: Your First Coroutine --
async defandasyncio.run - Example 28:
asyncio.gatherRunsasyncio.sleepTasks Concurrently
Intermediate (Examples 29-57)
- Example 29: Two Threads, Two Locks, Opposite Order -- A Reproduced Deadlock
- Example 30: A Global Lock Order Fixes the Deadlock
- Example 31:
acquire(timeout=...)+ Back-Off Fixes a Deadlock Differently - Example 32: Livelock -- Both Threads Active, Neither Makes Progress
- Example 33: A Producer Starved by Greedy Consumers
- Example 34: The Four Coffman Conditions -- Present in ex-29, Broken in ex-30
- Example 35: Memory Visibility -- Why a Busy-Wait Flag Is Fragile, Even When It "Works"
- Example 36: Any Read-Modify-Write Needs a Lock, Not Just
+= 1 - Example 37: A Data Race and a Logic Race Fail in DIFFERENT Ways
- Example 38: A Bounded Queue Applies Backpressure to a Fast Producer
- Example 39: Several Producers and Several Consumers, One Shared Queue
- Example 40:
task_done()+Queue.join()-- Waiting for a Full Drain - Example 41: A Hand-Built Bounded Buffer, Using
ConditionDirectly - Example 42:
as_completedYields Results in FINISH Order, Not Submit Order - Example 43: A Worker's Exception Is Stored, Then RE-RAISED by
.result() - Example 44:
ProcessPoolExecutor.mapWith achunksize-- Same Result, Less IPC Overhead - Example 45: A Global Mutated in a Child Process Is Invisible to the Parent
- Example 46:
multiprocessing.Queue-- the Cross-Process Delivery Channel - Example 47: A Shared
multiprocessing.Value, Protected by ITS OWN Built-In Lock - Example 48: A Thread Pool Beats Serial Execution on I/O-Bound Work
- Example 49: Threads Do NOT Speed Up CPU-Bound Work -- the GIL Serializes Them
- Example 50:
asyncio.create_taskSchedules Work CONCURRENTLY, Not Sequentially - Example 51:
asyncio.timeoutCancels a Coroutine That Runs Too Long - Example 52: An
asyncio.QueuePipeline -- Cooperative Producer/Consumer - Example 53:
asyncio.SemaphoreCaps How Many Coroutines Run "In Flight" at Once - Example 54: A Blocking
time.sleepInside a Coroutine Freezes the ENTIRE Event Loop - Example 55:
loop.run_in_executor-- Offloading a TRULY Blocking Call Off the Event Loop - Example 56: Amdahl's Law -- the Theoretical CEILING on Parallel Speedup
- Example 57: Map-Reduce -- Split the Work, Combine the Partial Results
Advanced (Examples 58-87)
- Example 58: On a Free-Threaded Build, CPU-Bound Threads Actually Scale
- Example 59: Benchmarking IDENTICAL Threaded Code -- GIL Build vs
python3.14t - Example 60: Double-Checked Locking -- A Lazily-Built Singleton, Safe Under Contention
- Example 61: A Hand-Built Reader-Writer Lock -- Many Readers, OR One Writer
- Example 62: Detecting a Deadlock -- Finding a Cycle in a Wait-For Graph
- Example 63: A "Lock-Free" Counter -- via a Single-Owner Queue, Not a Lock
- Example 64:
threading.local()-- Per-Thread State That Never Bleeds Across Threads - Example 65: Cancelling a PENDING
Future-- Before It Ever Starts - Example 66:
asyncio.wait(..., timeout=...)-- Returns BOTH the Done AND the Pending Sets - Example 67: Cancelling a Task -- Catching
CancelledErrorto Run Cleanup - Example 68:
asyncio.TaskGroup-- One Failure Cancels ALL Its Siblings - Example 69: Fetch Many "URLs" Concurrently, Rate-Limited, Then Aggregate
- Example 70: A Three-Stage Pipeline -- Read -> Transform -> Write, via Two Queues
- Example 71: Work-Stealing -- an Idle Worker Steals From an Overloaded Peer's Deque
- Example 72:
multiprocessing.shared_memory-- Genuine No-Copy Cross-Process Access - Example 73:
asyncio.to_thread-- the High-Level Shortcut for Offloading Blocking Calls - Example 74:
while not predicate: cond.wait()-- Guarding Against a Spurious/Early Wakeup - Example 75: A
BarrierSynchronizes Phased Parallel Computation - Example 76: I/O-Bound Work, Benchmarked Three Ways -- Serial vs Threads vs
asyncio - Example 77: CPU-Bound Work, Benchmarked Three Ways -- Only Processes Actually Win
- Example 78: Graceful Shutdown -- Draining In-Flight Work Before a Worker Pool Exits
- Example 79: Dining Philosophers -- Deadlock-Free, via a Global Fork-Acquisition Order
- Example 80: A Stress Harness -- Repeated Trials Surface an Intermittent Race
- Example 81: Capstone Preview -- Fetch-and-Aggregate, Three Ways, One Timing Harness
- Example 82: An
Observable,map, andfilter-- Reactive Streams viareactivex(RxPY) - Example 83: Cold Observables Replay in Full; Hot Subjects Drop What Already Happened
- Example 84: Backpressure Strategies -- Buffer-All vs Keep-Latest (Hand-Rolled)
- Example 85: Reactive Pull -- a Subscriber's request(n) Bounds How Much the Producer Emits
- Example 86: A java.util.concurrent.Flow-Style Contract, Hand-Rolled in Python
- Example 87: An Annotated Marble Diagram for merge -> map -> debounce
← Previous: 23 · Functional Programming Drilling · Next: Beginner Examples →
Last updated July 16, 2026