Concurrency line
Multithreading deep-dive
Two cooks can see the same fridge, and a small crew works a rail of tickets instead of hiring a cook per ticket.
Stops explain each step. The film plays the whole ride.
- Reading or handing a ticket
- Waiting
- A safe update
- Stuck or missing
Both read the same fridge
Both cooks read the same note on the fridge at the same moment.
Next stop: The result depends on who writes last
All stops
The everyday version
One fridge, two cooks, one marker. The crew is a fixed team and a rail of tickets, not a new cook for every ticket.
- Cook Ais the Thread A
Priya. She reads the fridge, takes the marker, and hands tickets to the crew.
- Cook Bis the Thread B
Kamal. He sees the same fridge, and he waits while Priya holds the marker.
- The fridgeis the Shared heap
The shared note. Both cooks can see it, which is the point and the hazard.
- The markeris the Lock
One cook updates while holding it. Then they put it back.
- The crewis the Pool
A few cooks and a rail of tickets. They can all end up waiting on each other.
- One cookis the threading page
A single shift, from the contract to the pass. This page adds the second cook.
Interview questions
Say the short answer first. Go deeper only if they ask.
Does the GIL make Python threads safe?
It protects the interpreter, not your own updates.
x += 1 is not one bytecode instruction, so it is not atomic. The GIL does not make that update safe. You still need a lock, a queue, or no sharing. The locks page shows the lost tally.
“The GIL means I can skip locks.”
What is a race condition versus a data race?
A race condition is a timing bug. A data race is an unsynchronised native write.
Pure Python can have a race condition without a data race, because the GIL serialises bytecode. A data race is several threads, the same memory, at least one write, and no synchronisation, and it happens in native code. The glossary draws that line. The two names are not synonyms.
“They are synonyms.”
What is the default thread-pool size?
Since 3.13, the smaller of 32 and the process CPU count plus 4.
The 3.13 default is min(32, (os.process_cpu_count() or 1) + 4). In 3.8 it was min(32, os.cpu_count() + 4). In 3.5 it was five times the CPU count. The pool reuses idle workers and joins them before the interpreter exits. Do not park a server loop in it. The docs’ deadlock is a worker waiting on its own pool.
“It starts one thread per core, or one thread per task.”
What changes on a free-threaded build?
Several threads can run bytecode at once. Sharing still needs a plan.
dict and list use per-object locks, and a lock-free read can still see a half-updated container. The GIL build stays the default through 3.14. Free-threading does not remove the need for a lock around your own multi-step update.
“Free-threading removes the need for locks.”
Can two correct updates still slow each other down?
Yes. Writes that do not overlap can still bump on one shared line of cache.
False sharing: two threads update different counters that sit on the same cache line, so each write makes the other core refresh that line. The result is correct, and it is still slow. Do not quote a line size from memory. This is not a race.
“Different variables never slow each other down.”
Cheat-sheet
Commands you will actually type.
tally += 1 # several bytecode steps, not atomicThe GIL does not make this one update. Take a lock, or do not share the tally.
with lock:
tally += 1The read and the write stay together. A Lock may be released by any thread. An RLock may be taken again by its owner.
ThreadPoolExecutor() # 3.13: min(32, (os.process_cpu_count() or 1) + 4)The default crew. Idle workers are reused, and they are joined before exit.
executor = ThreadPoolExecutor(max_workers=1)
executor.submit(wait_on_future) # the only worker waits on the same poolThe docs’ deadlock. The worker calls result on a future that needs a free worker.
if key in mapping: # the key can vanish here
return mapping[key]The glossary’s check-then-read race. Lock both steps, or try the read and handle the miss.