Chapter 08 · Week 8

Concurrency, Threads, and Midterm Review

How do many hands work as one without chaos?

Chapter 8 — Concurrency, Threads, and Midterm Review

“Concurrency is not parallelism.” — Rob Pike

“For God is not a God of confusion but of peace.” — 1 Corinthians 14:33


Why This Matters

This is the chapter that closes Phase 1, and it has two jobs.

The first job is concurrency. Every structure you have built so far — the dynamic array, the linked list, the stack and queue, the hash map, the tree, the graph — assumed one thread of execution. One thing happening at a time, in order, where you could trace a single finger down the code and know exactly what state the program was in. That assumption is about to break. The moment two threads touch the same data, the cost model you have spent seven weeks building gets a new and frightening line item: the cost of getting the answer wrong, intermittently, in a way that passes every test you ran. A race condition is not a slow program. It is a sometimes-wrong program. And “sometimes wrong” is the most expensive bug class in this entire book.

So concurrency belongs in Phase 1, with the rest of the cost lessons, because it is a cost lesson. Threads buy you responsiveness and, sometimes, throughput. They charge you in a currency you have not had to spend yet: the discipline of reasoning about which operations are atomic, what state is shared, and who is allowed to touch it when. The architect who reaches for threads without counting that cost ships the worst kind of bug there is.

The second job is the midterm. Half of Coding 3 is behind you. You have built, by hand, every classical data structure on the standard syllabus, measured each one, and learned what it is good and bad at. This week consolidates all of it into a one-page reference — a cost table of every Phase 1 structure — that you will carry into the exam and, more importantly, into your career. Project 8 is the midterm: a 60-minute, closed-AI, closed-internet build of a small concurrent pipeline that uses the structures you own. The pipeline’s backbone is a thread-safe queue, which is exactly what this chapter teaches you to use correctly.

The Christian question for the week sits underneath both jobs: how do many hands work as one without chaos? Paul wrote 1 Corinthians 14 to a church where many people had real gifts and were all trying to use them at once — prophets, tongues-speakers, teachers — and the result was not richness but noise. His answer is not “fewer gifts.” His answer is order. “God is not a God of confusion but of peace” (14:33), and therefore “all things should be done decently and in order” (14:40). That is, almost word for word, the engineering problem of concurrency: many workers, real work, and a discipline of coordination that turns simultaneous effort into one coherent result instead of a corrupted counter. Order is not the enemy of many hands working at once. Order is the only thing that lets many hands work at once.

Let’s count this cost.


8.1 — Why Concurrency at All

A program that does one thing at a time is the easiest program to reason about and, frequently, the wrong tool. Three pressures push you toward doing more than one thing at once.

Responsiveness. A program that is busy computing cannot also answer the user. If your single thread is grinding through a long task, the UI freezes, the server stops accepting connections, the spinner spins forever. Doing the slow work somewhere else keeps the program answering while it works.

Throughput. You have four CPU cores. A single-threaded program uses one of them and leaves three idle. If your work can be split into independent pieces, running them on separate cores can — in the right language, for the right workload — finish in roughly a quarter of the time. More work per unit of wall-clock time is throughput.

Using the waiting time. This is the big one, and the most misunderstood. Most programs spend most of their life waiting — for the disk, for the network, for a database, for the user. A request to a remote API might take 200 milliseconds, during which your CPU does nothing but wait. If you have a hundred such requests to make, doing them one at a time costs 20 seconds of mostly-idle waiting. Overlapping the waits — starting the second request while the first is still in flight — can collapse that 20 seconds to a fraction of it. You are not doing more computation. You are filling the idle time.

Coach’s Note — Memorize this distinction now, because it is the right-tool fork of the entire chapter: are you waiting, or are you computing? “Waiting” work (network, disk, sleep) and “computing” work (math, parsing, crunching) want completely different concurrency tools. Get this wrong and you will write threaded code that is slower than the single-threaded version you started with. We will prove that with a clock in §8.5.


8.2 — Concurrency vs Parallelism

These two words get used interchangeably in casual speech. They are not the same thing, and the difference is the heart of this chapter.

  • Concurrency is a structure: the program is organized as multiple independent tasks that can be in progress at the same time. Concurrency is about dealing with many things at once.
  • Parallelism is an execution fact: multiple tasks are literally running at the same instant, on different physical cores. Parallelism is about doing many things at once.

Rob Pike’s line — “concurrency is not parallelism” — is the slogan. Here is the picture.

A single chef in a kitchen working on three dishes — chopping for one while a second simmers and a third bakes — is concurrent. There is one chef (one core). At any single instant the chef’s hands are on exactly one task. But three dishes are in progress, because the chef interleaves the work and fills the waiting time (the simmer, the bake) with active work on another dish.

Three chefs, each cooking one dish, at three stations, with three pairs of hands moving at the same instant — that is parallel. Three things are literally happening simultaneously.

ConcurrencyParallelism
What it isA way to structure work as independent tasksTasks literally running at the same instant
How many coresWorks on one coreRequires multiple cores
Buys youResponsiveness, filling idle timeThroughput on divisible work
The chef pictureOne chef, three dishes interleavedThree chefs, three dishes at once

You can have concurrency without parallelism (one core, interleaved tasks — exactly what a single-threaded event loop does, §8.7). You can have parallelism without much concurrency complexity (split a number-crunch across processes). And the central, surprising fact of Python — which the rest of the chapter builds toward — is that Python threads give you concurrency but, for CPU-bound work, not parallelism. Hold that thought.


8.3 — Processes vs Threads

Two ways to get more than one thing happening. They are not interchangeable, and the difference is memory — the thing Coding 1 trained you to see.

A process is a running program with its own private memory — its own address space, its own heap, its own everything. When you run python3 script.py, the operating system creates a process. Two processes are isolated: process A literally cannot read process B’s variables, because B’s memory is in an address space A has no access to. To share data between processes, you have to deliberately ship it across a boundary (a pipe, a queue, a file, a socket) — and that shipping has a cost, because the data must be serialized, copied, and deserialized.

A thread is a separate line of execution inside one process. All threads in a process share the same address space — the same heap, the same global variables, the same objects. A thread has its own little private stack (its current function calls and local variables), but everything on the heap is common ground. Two threads in the same process can both see and modify the same list, the same dict, the same counter — with no shipping at all, because it is the same memory.

ProcessThread
MemoryPrivate address spaceShared address space
What’s sharedNothing (must ship explicitly)The whole heap — all objects, all globals
What’s privateEverythingJust its own call stack
Cost to createHeavy (new interpreter, new memory)Light (one new stack)
Cost to communicateHigh (serialize + copy across boundary)Near zero (it’s the same memory)
Failure isolationStrong (one crash doesn’t kill others)Weak (one bad access can corrupt shared state)
In PythonReal parallelism (own GIL each)Concurrency; parallel only for I/O

Read that table the way Coding 1 taught you to read memory. Threads are cheap and share everything — which is exactly why they are dangerous. The same property that makes a thread fast to create and free to communicate with — shared memory — is the property that lets two threads stomp on each other’s writes. Processes are expensive and isolated — which is exactly why they are safe, and why they are Python’s answer for real CPU parallelism.

Coach’s Note — “Shared, mutable, concurrent — pick at most two.” If state is shared between threads, it had better be either immutable (no one writes it) or guarded (writes are serialized with a lock). Shared and mutable and touched concurrently with no guard is the precise recipe for a race condition. The next section shows you the recipe cooking.


8.4 — Shared Mutable State and Race Conditions

Here is the bug that makes concurrency hard. It is small. Read it slowly.

import threading

counter = 0

def worker():
    global counter
    for _ in range(100_000):
        counter += 1   # looks atomic. is not.

t1 = threading.Thread(target=worker)
t2 = threading.Thread(target=worker)
t1.start(); t2.start()
t1.join();  t2.join()

print(counter)   # you expect 200000. you will often get less.

You expect 200000. Two threads, each adding one a hundred thousand times. The trouble is that counter += 1 is not one operation. It is three:

  1. Read the current value of counter into a register.
  2. Add one to that register.
  3. Write the register back into counter.

Now imagine the operating system pauses thread A between step 1 and step 3. Both threads read counter as, say, 41. Both add one, getting 42. Both write 42 back. Two increments happened; the counter went up by one. One update was lost — silently, with no error, no crash, no exception. The thread that wrote second simply overwrote the work of the thread that wrote first.

A sequence of operations whose correctness depends on the timing of how threads interleave is a race condition. The “race” is the two threads racing to read-modify-write, and whoever loses gets clobbered. The output is nondeterministic — different on different runs, on different machines, under different loads. It will pass on your laptop and fail in production. This is the most expensive bug class in software because the test that would catch it has to lose the race too, and it usually doesn’t.

The code/race_demo.py file in this chapter runs exactly this experiment with the read-modify-write split out explicitly (and the interpreter tuned to switch threads aggressively, so the race is reliably visible). On a typical run you will see the unsafe counter come out at a fraction of the expected total — hundreds of thousands of updates lost.

The fix: a lock

A lock (also called a mutex, for “mutual exclusion”) is an object only one thread can hold at a time. To touch the shared state, a thread must acquire the lock; when it’s done, it releases it. Any other thread that tries to acquire a held lock blocks — waits — until the holder releases. The lock turns the three-step read-modify-write into one indivisible step that no other thread can interrupt. That “no one can interrupt me here” region is called a critical section.

import threading

counter = 0
lock = threading.Lock()

def worker():
    global counter
    for _ in range(100_000):
        with lock:        # acquire on entry, release on exit (even on error)
            counter += 1  # critical section: exactly one thread at a time

# ... start and join the threads as before ...
# counter is now exactly 200000, every single run.

The with lock: form is the right way to use a lock in Python. It acquires the lock when the block begins and guarantees the lock is released when the block ends — even if an exception is thrown inside. (This is the same with you saw for files in Phase 1’s file work: a context manager cleaning up after you. The same discipline, a different resource.) Run code/race_demo.py and you will see the locked version produce exactly the right total, every time. The race is gone because the read-modify-write can no longer be split.

Coach’s Note — The lock has a cost, and you should be able to name it. Acquiring and releasing has overhead, and — worse — every thread waiting on a held lock is a thread doing nothing. A lock held too long, or too coarsely, serializes your program: you bought threads for concurrency and then locked them into single-file. The art is the smallest critical section that is still correct. Guard the shared write, not the whole loop. Cost, again — there is no free coordination.

Deadlock, briefly

Locks introduce their own failure mode. Suppose thread A holds lock 1 and is waiting for lock 2, while thread B holds lock 2 and is waiting for lock 1. Neither will ever release, because each is waiting for the other. The program hangs forever. That is deadlock — mutual, permanent waiting.

The classic prevention rule: always acquire multiple locks in the same global order. If every thread that needs both lock 1 and lock 2 always grabs lock 1 first, the circular wait can’t form. You will not need multi-lock code for the midterm — queue.Queue (§8.6) hides all of this — but you must be able to define deadlock and name the prevention rule. It’s a fair exam question and a real production failure.


8.5 — The GIL: The Right-Tool Lesson of the Week

Here is the fact that surprises every programmer the first time, and the single most important right-tool lesson in this chapter.

CPython has a Global Interpreter Lock — the GIL — and it means only one thread executes Python bytecode at a time.

Not one thread per core. One thread, period. The interpreter holds a single global lock, and a thread must hold that lock to run Python code. So even on an 8-core machine, eight Python threads do not run eight pieces of Python at the same instant. They take turns holding the GIL. The CPU’s seven other cores sit idle as far as your Python computation is concerned.

Read that again, because the consequence is sharp:

For CPU-bound work, Python threads give you no parallelism — and therefore essentially no speedup.

If your threads are all doing pure-Python computation — looping, arithmetic, parsing, crunching — they spend their lives fighting over the one GIL. Adding more threads adds overhead (the cost of switching between them) without adding throughput. The threaded version comes out the same speed as the single-threaded version, or a hair slower.

So why does anyone use Python threads at all? Because of the other half of the fact:

The GIL is released during I/O. So for I/O-bound work, Python threads effectively run concurrently — and give a real speedup.

When a thread calls something that waitstime.sleep, a socket read, a disk read, a database query — CPython releases the GIL before it blocks. While that thread waits, another thread can grab the GIL and run. The waits overlap. You are filling the idle time (§8.1) — exactly the workload threads are good at. The thread isn’t computing while it waits; it’s just waiting, so handing the GIL to a sibling costs nothing.

This is the whole game. Threads for waiting; processes for computing. The code/gil_io_vs_cpu.py file proves it with a clock. A representative run:

I/O-BOUND (4 x 0.25s sleep):
  serial      :  1.019s
  threads     :  0.255s   (3.99x vs serial)

CPU-BOUND (4 x heavy loop):
  serial      :  0.551s
  threads     :  0.520s   (1.06x vs serial)
  processes   :  0.201s   (2.74x vs serial)

Read those numbers like an architect. Four 0.25-second sleeps run serially in ~1 second; with four threads they finish in ~0.25 seconds — a 4x speedup, because all four waits overlap. That is threads doing exactly what they’re for. The CPU-bound loop, run with four threads, gets 1.06x — no speedup at all, just the same work plus switching overhead, because the GIL let only one thread compute at a time. The same CPU work across four processes gets 2.74x, because each process has its own interpreter and its own GIL, free to run on its own core. (Why not a clean 4x? Because spawning processes and shipping data across the boundary costs real time — §8.3’s communication cost, showing up in the measurement. Asymptotics describe the work; constant factors bill you for it.)

Processes for CPU-bound parallelism

When the work is genuinely CPU-bound and you need real parallelism in Python, you reach past threads to the multiprocessing module. It runs your function in separate processes, each with its own interpreter and GIL, on separate cores. The Pool makes this almost as easy as a loop:

import multiprocessing

def heavy(n):
    return sum(i * i for i in range(n))

if __name__ == "__main__":                 # required guard on every platform
    with multiprocessing.Pool(processes=4) as pool:
        results = pool.map(heavy, [5_000_000] * 4)
    print(results)

pool.map ships each argument to a worker process, runs heavy there, and ships the result back. The if __name__ == "__main__": guard is not optional — without it, child processes re-import your module and re-run the top-level code, spawning processes recursively. The cost you are paying for this parallelism is the process-creation and the serialization of arguments and results across the process boundary (§8.3). For a big enough computation, that cost is dwarfed by the parallel speedup. For a tiny one, the overhead can cost more than you save — measure, don’t assume. That is the P1 lesson, returning.

Coach’s Note — The GIL is the cleanest “right tool for the job” lesson in the whole book, because the wrong choice is not slightly worse — it is no better, sometimes worse, and more code. A junior throws threads at a CPU-bound loop, watches it not speed up, and concludes “concurrency is hard.” The architect asks one question first — am I waiting or computing? — and picks threads or processes accordingly, before writing a line. The question comes before the code. That sentence is the whole book.


8.6 — queue.Queue: The Thread-Safe Tool

You built a queue by hand in Chapter 4 — FIFO, enqueue, dequeue, the works. Python’s standard library ships a thread-safe one in the queue module, and it is the single most useful concurrency tool you will touch this week, because it is the backbone of the midterm.

queue.Queue is a FIFO queue where put and get are atomic and safe across threads — the locking is done for you, inside. You do not write a single Lock; the queue’s internals handle all coordination. This is the right-tool move in miniature: instead of hand-rolling shared state plus a lock plus careful critical sections, you reach for the structure that has already solved the coordination problem correctly.

It is the natural shape for a producer/consumer pipeline: one or more producer threads create work and put it on the queue; one or more consumer threads get work off and process it. The queue is the safe hand-off point between them.

import queue
import threading

work = queue.Queue()

def producer():
    for i in range(1000):
        work.put(i)        # thread-safe; no lock needed

def consumer():
    while True:
        item = work.get()  # blocks until an item is available
        if item is None:   # poison pill: a signal to stop
            return
        process(item)
        work.task_done()

Two features make this the right backbone:

  • Blocking. get() on an empty queue blocks — the consumer waits, doing nothing, until a producer puts something. put() on a full bounded queue (Queue(maxsize=N)) blocks the producer until a consumer makes room. This blocking is exactly the coordination you want and would otherwise have to build by hand.
  • The poison pill. To tell consumers to stop, the producer puts one sentinel value (commonly None) per consumer after the real work. Each consumer that pulls a sentinel exits. This is how you shut a pipeline down cleanly, with no consumer left blocked forever on an empty queue.

The code/producer_consumer.py file is a complete, correct producer/consumer pipeline with multiple producers and consumers, a bounded queue, and a correctness proof: it counts consumed items under a lock and asserts at the end that the count equals exactly what was produced — no lost items, no duplicates. Study it. It is, almost exactly, the Normal tier of Project 8.

Coach’s Note — “Every item produced is consumed exactly once” is the correctness property the midterm grades. Not “the program ran.” Not “no exceptions.” Exactly once. Lost items mean a race in your hand-off; duplicate processing means a race in your tracking. The queue prevents the first for free. The second is on you — guard your result-recording with a lock, or record into the queue’s own machinery. Prove it with an assertion, the way the demo does.


8.7 — A Contrasting Model: Node’s Event Loop

Everything above is the threads answer to concurrency. There is a completely different answer, and you will build on it in Week 9, so meet it now as a contrast.

Node.js is single-threaded. One thread. No Lock. No GIL to fight. And yet a Node server routinely handles thousands of simultaneous connections. How?

The event loop. Instead of one thread per task, Node has one thread that never blocks. When Node starts an I/O operation — a database query, a file read, a network request — it does not wait for it. It registers a callback (“when this finishes, run this function”) and immediately moves on to the next ready piece of work. When the I/O completes, the result is placed on a queue, and the single thread picks the callback up and runs it. The one thread is never idle waiting — it is always either running a short piece of work or starting an I/O operation and moving on. This is non-blocking I/O driven by an event loop.

Think back to the single chef from §8.2. The event loop is the one concurrent chef: it never stands waiting for the pot to boil. It starts the pot (the I/O), turns to chop vegetables (the next callback), and comes back to the pot only when it signals done. One pair of hands, many dishes in progress, zero idle time.

The consequences are worth a table, because this is a right-tool fork you will make for real in Phase 2:

Threads (Python)Event loop (Node)
Concurrency modelMany threads, OS-scheduledOne thread, cooperatively scheduled
Shared-state racesYes — you need locksNo — one thread, nothing to race
Best atI/O-bound (GIL released on I/O)Massive numbers of I/O-bound connections
CPU-bound workUse processes insteadBlocks the whole loop — a real weakness
The dangerDeadlock, lost updatesOne slow synchronous call freezes everything

The event loop’s strength is its weakness. Because there is only one thread, there are no locks and no races — beautiful. But that same one thread means a single long synchronous computation (a CPU-bound loop, a blocking call) freezes every connection at once, because there’s no other thread to pick up the slack. Node is superb for many-connections, I/O-bound servers and a poor fit for heavy CPU work — the mirror image of where you’d reach for Python’s multiprocessing.

Which gives us the right-tool table for the whole chapter — the one to internalize:

WorkloadRight toolWhy
I/O-bound, moderate concurrencyThreads (Python)GIL released during I/O; waits overlap; simple
CPU-bound, divisible workProcesses (multiprocessing)Each process its own GIL → real parallel cores
Many simultaneous connections, I/O-boundEvent loop (Node, async)One thread, no locks, scales to thousands of waits
CPU-bound on an event loop(wrong tool)One long computation freezes the whole loop

Coach’s Note — Notice there is no “best” concurrency model on that table — only a best fit for a workload. This is the thesis of the entire book wearing concurrency’s clothes: the right tool is a decision you make before you write code, driven by the constraints of the problem — here, the single constraint “am I waiting or computing, and for how many things at once?” Week 9 puts the event loop in your hands. This week, just know it exists and why it’s different.


Part B — Midterm Review

8.8 — The One-Page Cost Table (Take This Into the Exam)

Seven chapters of Phase 1, every structure you built by hand, compressed into one reference. This is the page you re-read the morning of the exam and keep beside you during it. n is the number of elements. “Amortized” means averaged over many operations (a few are expensive, most are cheap). “Average” means expected under a good hash / balanced tree; the worst case is noted where it bites.

StructureAccess by indexSearch (by value/key)InsertDeleteSpaceRight tool when…
Static arrayO(1)O(n)— (fixed)— (fixed)O(n), tightSize known up front; need raw indexed speed
Dynamic array (Py list)O(1)O(n)O(1) amortized at end; O(n) middleO(n) (shift)O(n), some slackThe default sequence; append-heavy, index-heavy
Singly linked listO(n)O(n)O(1) at front / at known nodeO(1) at known node; O(n) to findO(n) + pointer overheadCheap front insert; you hold the node already
Doubly linked listO(n)O(n)O(1) at either end / known nodeO(1) at known nodeO(n) + 2 pointers/nodeO(1) delete of a held node; deque internals
Stack (LIFO)O(1) pushO(1) popO(n)Last-in-first-out: undo, recursion, bracket match
Queue (FIFO)O(1) enqueueO(1) dequeueO(n)First-in-first-out: scheduling, BFS, pipelines
Deque (ring buffer)O(1) endsO(n)O(1) both endsO(1) both endsO(n)Add/remove at both ends; sliding windows
Hash table (Py dict/set)O(1) average, O(n) worstO(1) averageO(1) averageO(n), load-factor slackMembership / lookup by key, no ordering needed
Binary search tree (balanced)O(log n) avg, O(n) if unbalancedO(log n)O(log n)O(n) + 2 pointers/nodeOrdered lookup: range queries, sorted iteration, min/max, predecessor/successor
Graph (adjacency list)O(V+E) traversalO(1) add edgeO(degree)O(V+E)Relationships/dependencies/routes; sparse graphs
Graph (adjacency matrix)O(1) edge checkO(V²) traversalO(1)O(1)O(V²)Dense graphs; constant-time “is there an edge?”

Three judgments to carry alongside the table, because the costs only matter in service of a choice:

  • Hash table vs BST. Both give fast lookup. Choose the hash table for raw membership and key lookup with no ordering. Choose the BST the moment you need order — sorted iteration, range queries (keys_between), min/max, predecessor/successor. The hash table has no concept of “next key”; the tree is built on it. (This is exactly the choice a database makes between a hash index and a B-tree index — a forward-link to Phase 2.)
  • Array vs linked list. Asymptotics say linked-list front-insert is O(1) vs the array’s O(n). Your measurements in P3 said the array usually wins anyway, because contiguous memory is cache-friendly and pointer-chasing is not. Choose the linked list only when you genuinely insert/delete at a node you already hold, a lot. Otherwise the array’s constant factors win the footrace. Asymptotics describe; constant factors bill.
  • Adjacency list vs matrix. List costs O(V+E) space — cheap for sparse graphs (most real graphs). Matrix costs O(V²) — wasteful when sparse, but gives O(1) edge lookup and is fine when the graph is dense. The rule of thumb: sparse → list, dense or edge-check-heavy → matrix.

Coach’s Note — If you can reproduce this table from memory — not perfectly, but the shape of it: which operation each structure makes cheap, and the one workload each is for — you have the cost intuition Phase 1 was built to give you. The numbers serve the choice. An architect who knows the costs cold chooses the right structure before writing code; everyone else writes code and discovers the cost in production.


8.9 — The Cumulative Bug List

The bugs most likely to cost you points on the midterm — Phase-1, Python edition. Read all of them the morning of the exam.

  1. Mutable default argument. def f(x, acc=[]) — the list is created once and shared across every call. Use acc=None and if acc is None: acc = [] inside.
  2. is vs ==. == compares value; is compares identity. Use == for values; reserve is for None (if x is None). a == b true does not imply a is b.
  3. Integer vs float division. / always gives a float (7 / 2 == 3.5); // floors (7 // 2 == 3). Reaching for the wrong one is a classic off-by-a-type bug.
  4. Off-by-one in a range. range(n) is 0..n-1. range(1, n) skips 0 and stops before n. Trace the endpoints with a tiny input every time.
  5. Modifying a list while iterating it. Deleting from a list inside for x in list skips elements. Iterate a copy (for x in list[:]) or build a new list with a comprehension.
  6. KeyError on a dict. d[k] raises if k is absent. Use d.get(k) (returns None) or d.get(k, default) when absence is normal.
  7. Shallow vs deep copy. b = a aliases the same list; b = a[:] copies one level; nested objects are still shared. Mutating b[0] may surprise you. Know which copy you have.
  8. Hash-table iteration order is undefined in principle. (CPython dict preserves insertion order as an implementation detail; do not write code that depends on a set’s order at all.)
  9. RecursionError from a missing/wrong base case. Every recursion needs a base case and a strictly-shrinking argument. Trace from the smallest input.
  10. Race condition: a shared counter/list mutated by threads with no lock. Lost updates, nondeterministic totals. Guard the shared write with a Lock, or hand off via queue.Queue. (This week’s headline bug.)
  11. Threads on CPU-bound work, expecting a speedup. The GIL means no speedup — sometimes a slowdown. Use multiprocessing for CPU-bound parallelism.
  12. Forgetting the poison pill / join. A consumer blocked on an empty queue never exits; a main that doesn’t join exits before the work is done. Send one sentinel per consumer; join every thread.
  13. multiprocessing with no if __name__ == "__main__": guard. Children re-import the module and spawn recursively. Always guard the entry point.
  14. A “test” that never asserts. A test function with no assert always “passes.” Every test must end in an assertion that can fail.
  15. Catching and silently swallowing an exception. except Exception: pass hides the bug. At minimum, report it; usually, re-raise.

Fifteen items. The first nine are general Phase-1 Python; the rest are this week’s concurrency landmines. Read all fifteen before you walk in.


8.10 — How to Study This Week

You have one week. Spend it like this.

Day 1 — Re-do the hardest reps. Open the exercises for Chapters 2–7. Pick the two reps per chapter you found hardest the first time. Re-do them from memory, AI off. You will be surprised what cemented and what didn’t.

Day 2 — Build each structure’s core from scratch, on a clock. A dynamic array’s append with doubling. A hash map’s put/get with chaining. A BST’s insert and in-order traversal. BFS on an adjacency list. Fifteen minutes each, no notes. If you can produce the core of each cold, the table in §8.8 is yours, not memorized.

Day 3 — Drill concurrency. Type race_demo.py from scratch and watch it lose updates, then fix it. Type producer_consumer.py from scratch and make the assertion pass. Run gil_io_vs_cpu.py and be able to explain every number.

Day 4 — One full timed mock. Re-read this chapter’s the exercises capstone, then do the full Project 8 Normal tier in 60 minutes, closed everything. Then compare against producer_consumer.py.

Day 5 — Diagnose and gap-fill. Where did the mock diverge from clean? Drill that — the two weakest spots only. Resist “review everything”; it produces a thin layer of nothing.

Day 6 — Light review. Re-read §8.8 (the cost table) and §8.9 (the bugs). Run the three demos once more. Stop early.

Day 7 (exam day) — No new material. Re-read §8.9. Skim §8.8. Eat. Show up early. Cramming new material the morning of an exam is self-harm; you either own the muscle or you don’t, and the cram only makes you anxious. Trust the reps.

Compressed 3-day fallback (you started late — it happens): Day 1, re-do one hard rep per chapter and read §8.9 twice. Day 2, do all of this chapter’s reps and the three demos end to end. Day 3 (exam), re-read §8.9, skim §8.8, eat, show up. The compressed plan covers Normal comfortably and earns partial Medium; Hard is unlikely — be honest about scope.


8.11 — Common Bugs (Concurrency Edition)

Bug: A shared counter or list mutated by multiple threads comes out wrong, differently on each run. Example: counter += 1 in two threads loses updates because read-modify-write isn’t atomic. Fix: Guard every shared write with the smallest possible with lock: critical section, or hand the work off through a queue.Queue instead of sharing the variable.


Bug: You added threads to a CPU-bound loop and it didn’t get faster (or got slower). Example: Four threads summing big ranges run no faster than one — the GIL serializes them. Fix: For CPU-bound work, use multiprocessing (separate interpreters, separate GILs, real cores). Threads are for I/O-bound work, where the GIL is released on the wait.


Bug: The program hangs and never exits. Example: A consumer is blocked on queue.get() forever because no poison pill was sent; or main never joined the threads and they’re still running (or were never told to stop). Fix: Put exactly one sentinel per consumer after all real work, and join every thread before reading results.


Bug: multiprocessing code spawns processes endlessly or errors on import. Example: Top-level Pool(...) code with no if __name__ == "__main__": guard — each child re-imports the module and re-runs it. Fix: Put all process-spawning code inside if __name__ == "__main__":.


Bug: Holding a lock for far too long makes the threaded version no faster than serial. Example: with lock: wrapped around the entire worker loop, not just the shared write — every thread runs single-file. Fix: Shrink the critical section to the smallest region that is still correct. Guard the shared write; do the independent work outside the lock.


Bug: Two threads deadlock, each holding a lock the other needs. Example: Thread A holds lock1, waits for lock2; thread B holds lock2, waits for lock1. Both hang forever. Fix: Acquire multiple locks in one consistent global order everywhere. Better, for a pipeline, avoid multiple locks entirely by using queue.Queue.


8.12 — Reps

Open the exercises for the full set. This week’s reps split into concurrency drills and midterm-consolidation drills — you need both. A preview:

  • Rep 1 — Reproduce the lost-update race by hand, then fix it with a lock.
  • Rep 4 — Time threads on an I/O-bound vs a CPU-bound workload; explain the GIL from your own numbers.
  • Rep 6 — Build a correct producer/consumer with queue.Queue and prove exactly-once consumption.
  • Rep 10 — Reproduce the §8.8 cost table from memory.
  • Capstone — A timed cold build of the producer/consumer pipeline, the midterm dress rehearsal.

Do every one. Phase 1 has no AI to lean on, and the midterm has none either. The muscle is the deliverable.


8.13 — This Week’s Project (The Midterm)

Project 8 — Concurrent Data Pipeline is the midterm, and it is in Project 8.

It is a 60-minute, live, closed-AI, closed-internet build. Open textbook only. You will build a correct producer/consumer pipeline with a thread-safe queue (Normal); show with timing that threads speed up I/O-bound but not CPU-bound work, and explain why with the GIL (Medium); and solve the CPU-bound case with multiprocessing plus write the threads-vs-processes-vs-event-loop decision memo (Hard).

The exam constraints are explicit and they are the point: closed-book except this textbook, no AI, no internet, 60 minutes on the clock. This is the first Coding 3 test where you cannot reach for the agent. If you have typed the reps and built the structures with your own hands, the discomfort of the first ten minutes will pass and your hands will start working. If you haven’t, the midterm will tell you the truth — and you’ll have eight weeks to fix it before the final. Read Project 8 now, then come back for the final word.


8.14 — Coach’s Final Word for Week 8

Half of Coding 3 is behind you.

You can now state the time and space cost of every classical data structure and choose among them deliberately — not because you memorized a table, but because you built each one by hand, measured it, and watched it win and lose footraces. That is the cost intuition the whole first half was for. And you can now reason about concurrency: what a thread shares, what a race condition is and how a lock kills it, why the GIL makes Python threads the right tool for waiting and the wrong tool for computing, and why Node’s single thread is a third model entirely. You can look at a workload and ask the one question that picks the tool — am I waiting, or computing, and for how many things at once? — before you write a line.

Next week, Phase 2 begins, and the whole game changes. You leave the single program behind and build outward into the internet: your first server, listening at a port, answering requests. The agent comes on. The right-tool question stops being about structures inside one process and starts being about whole systems — server, database, front end, and the seams between them. The cost intuition you built in Phase 1 is exactly what makes those bigger decisions good ones.

But that is the back half. The front half — the sharpening — gets tested this week. Take the midterm seriously. Show up rested. Trust your training. Submit honest work, whatever the score, and use it as the diagnostic it is.

“For God is not a God of confusion but of peace.” The whole discipline of this chapter — locks, queues, ordered hand-offs, the careful question of who may touch what when — is, in the small, the same work Paul commended to a noisy church: not fewer hands, but ordered hands; not less effort, but coordinated effort; many workers and one coherent result. That is concurrency done right. It is also a picture of how a body is meant to work.

See you in the exam room. Phase 2 starts the day after.


Up next: Read the exercises and complete every rep — type every line, run everything, AI off. Then open Project 8 for the midterm spec and study with §8.8 and §8.9. (Coming from last week? Chapter 7 — graphs.) After the midterm, Chapter 9 — your first server, and the start of Phase 2: the right tool, the real system.