Concurrency, from first principles.
Threads, the GIL, race conditions, locks, deadlock, queues, the event loop, limits and cancellation. Each chapter explains one idea in depth, then hands you a real bug in a busy kitchen to fix. Otto, the octopus line cook, checks your work.
- real bugs to fix
- 8
- real bugs to fix
- packages to install
- 0
- packages to install
- start to finish
- 2.5 h
- start to finish
Eight chapters, eight bugs.
Read the idea, run the case, watch it break, fix it, and let the checker run it many times. Every symptom below is real output from the lab.
- 01Waiting is not workingDinner for twenty takes two secondsthreads, processes, the GIL20 min
- 02Race conditionsTen croissants, fifty soldcheck-then-act, locks20 min
- 03Hold locks brieflyThe lock that closed the kitchencontention, critical sections15 min
- 04DeadlockKnife, then boarddeadlock, lock ordering20 min
- 05Queues: producers and consumersThe orders nobody cookedqueues, sentinels, backpressure20 min
- 06The event loopThe waiter who stood stillasyncio, blocking the loop20 min
- 07Limits and backpressureFifty orders at oncesemaphores, fan-out limits15 min
- 08Timeouts and cancellationThe burners that never came backtimeouts, cancellation, cleanup20 min
Lose an update yourself.
Two cooks each run plated += 1, which Python runs as three steps. You are the scheduler: choose who goes next. Then turn on the lock and try to break it again.
Try A, B, A, B, A, B: both cooks load 0 before either one stores. That is chapter 02 in six clicks.
Cook A
- local = plated
- local = local + 1
- plated = local
local = 0
Cook B
- local = plated
- local = local + 1
- plated = local
local = 0
Step the cooks in any order. Can you make plated end at 1?
Run it. Fix it. Check it many times.
- Step 1 · run
Watch the bug happen on your own machine.
$ kitchen run 2
croissants sold: 50
stock left: -40
- Step 2 · fix
Change the case file in
lab/cases/. The chapter explains the idea; the fix is yours to write.with self.lock:
if self.stock > 0:
...
- Step 3 · check
The checker runs the case many times. A race has to lose every time.
$ kitchen check 2
PASS in 20 rushes, exactly 10 croissants are sold every time
PASS the stock never goes below zero
PASS the till still takes time (keep the sleep)
GIL on, GIL off.
The same code on Python 3.13 and on the free-threaded Python 3.14, on one laptop (16 CPUs, Windows 11). You run both in chapters 01 and 02.
Four CPU-heavy jobs. Lower is faster.
| Ran on | GIL on | GIL off |
|---|---|---|
| one thread | 0.98 s | 1.10 s |
| 4 threads | 1.07 s | 0.27 s |
| 4 processes | 0.31 s | 0.29 s |
Four threads each add 1, a million times, with no lock.
- expected
- 4,000,000
- GIL on
- 4,000,000
- GIL off
- 1,098,299
The GIL hid the race. It never prevented it.
Ten questions before you ship.
- 01Is the slow part waiting or working? Threads and asyncio for waiting, processes for working.
- 02List every piece of data more than one thread or task touches.
- 03Hand data over with a queue before you share it with a lock.
- 04A check and the action that depends on it go in one critical section.
- 05Hold locks briefly. Never wait on the network while holding one.
- 06Take locks in one agreed order, written down.
- 07Never block the event loop. Every async def awaits its waits.
- 08Cap concurrency. Unlimited fan-out is a bug waiting for traffic.
- 09Put a timeout on every wait, and clean up in finally.
- 10Test it many times. A race that passes once has proven nothing.
Ready in a minute.
You need uv and git. uv installs Python for you, and the lab uses the standard library only.
git clone https://github.com/jars-demo/concurrency-demo.git cd concurrency-demo/lab uv sync uv run python -m kitchen run 1