Concurrency & GIL
The word "concurrency" in Python hides three different machines — OS threads, separate processes, and a single-threaded event loop — and the choice between them is decided not by taste but by where the program spends its time. On top of all three sits the GIL, the most-repeated and least-understood mechanism in the language. The precise statement is that the GIL is an implementation detail of CPython, not a property of the Python language: Jython and IronPython have none at all. It protects the interpreter's internal state, above all reference counts, and serialises bytecode execution only. Around blocking I/O and inside many C extensions it is released — which is exactly why threads speed up network work beautifully and do nothing for a pure-Python loop.
Say the version-dependence out loud too, because "Python always has a GIL" is already false. Python 3.12 (PEP 684) gave each subinterpreter its own GIL, and Python 3.13 (PEP 703) introduced an optional free-threaded build where the GIL is switched off entirely. The classic one-shared-GIL model is still the default answer, but call it version-dependent rather than eternal. Then come the execution traps — the race on counter += 1, a deadlock on two Lock objects, a blocking call stalling the event loop, and the cost of pickle at the process boundary.
Topic map
- What the GIL actually protects — the interpreter mutex, reference counts, what is serialised, when it is released, and version-dependence.
- I/O-bound vs CPU-bound — classifying a workload by where its time goes; that classification picks the model.
- Threads — threading — real OS threads, races on non-atomic operations,
Lockand deadlocks. - Thread vs process — a shared address space against isolation, plus the cost of creation and of exchanging data.
- Processes — multiprocessing — one interpreter and one
GILper process, thefork/forkserver/spawnstart methods, the picklable requirement. - Coroutines — a subroutine with multiple entry points, and a coroutine object that does nothing on its own.
- async/await and the event loop — cooperative multitasking on one thread, and the single blocking call that breaks it.
- Green threads —
greenlet/gevent, implicit switching via monkey-patching versus an explicitawait. - Choosing a concurrency model — the decision table and the million-request design questions.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
"The GIL is part of the Python language" | Jython and IronPython have none; it is a CPython implementation constraint, and since 3.13 a build can disable it |
"The GIL makes code thread-safe" | It guarantees atomicity of a single bytecode, not of a compound operation — counter += 1 loses updates |
"The GIL stops threads from speeding anything up" | It is released on I/O and inside C extensions — three threads calling time.sleep(1) finish in 1 s, not 3 |
| Adding threads to CPU work "just in case" | There is no speedup, and the GIL handoff overhead makes the program several times slower than single-threaded |
Treating asyncio as multithreaded | The event loop lives on one thread; any synchronous blocking call stalls every coroutine |
Calling an async def function and never awaiting it | You get a coroutine object, the body never runs, and garbage collection emits a never-awaited RuntimeWarning |
Relying on the parent's memory after fork | The default start method depends on platform and version; with spawn/forkserver the module is re-imported and runtime state is lost |
| Firing an unbounded number of simultaneous requests | Sockets and descriptors run out long before memory or CPU — you need an asyncio.Semaphore or a pool |
What interviews check
The topic is mandatory at any Python interview from middle upward and almost always opens with "what is the GIL". A passing answer has four elements — it is a CPython mutex, it protects interpreter internals and reference counts, it serialises bytecode only, and it is released on blocking I/O. Answering "the GIL forbids multithreading" turns the rest of the conversation into a review of your mistakes — the next question is about three threads waiting on the network. The second mandatory block is workload classification: you are shown code and asked whether threads speed it up.
Then execution is probed. The classic code question is a threaded counter that loses increments, where the interviewer waits for "read-modify-write" and "not atomic at the bytecode level", not merely "add a Lock". The second is two deadlocking locks, diagnosed as lock-ordering inversion. The third is a blocking call inside a coroutine, where asyncio.to_thread is the expected fix. At senior level come the limits questions — can threads make CPU work slower than a single thread (yes, from GIL handoff), and which resource runs out first across a million requests (sockets and descriptors, not CPU and not memory). The typical mistake at every level is the same — naming a model before classifying the workload.