GIL, Threading, Multiprocessing and the Memory Model
This is the deeper companion to the short section in Python Tips & Tricks. It covers why the GIL exists, what it does and doesn't protect, and how memory is shared (or not) under threads versus processes.
Why the GIL Exists
CPython's memory management uses non-atomic reference counting. If two threads changed a refcount simultaneously the count could be corrupted, leading to leaks or use-after-free. The Global Interpreter Lock is one mutex that only lets one thread execute Python bytecode at a time, making refcounts and interpreter internals safe without fine-grained locks — and keeping single-threaded code and C extensions simple and fast.
Trade-off: no CPU parallelism across threads in one process.
When the GIL Is Released
- Blocking I/O: sockets, file reads,
time.sleep. - Many C extensions during heavy work (NumPy, hashing, compression,
rein some cases). - The interpreter forces a switch periodically (
sys.getswitchinterval(), 5 ms by default) so a CPU-bound thread can't starve others forever.
So threads help for I/O-bound work and for C code that releases the lock, but not for pure-Python CPU-bound loops.
The GIL Does Not Make Your Code Thread-Safe
A single bytecode is atomic, but a statement usually isn't:
counter = 0
def work():
global counter
for _ in range(100_000):
counter += 1 # load, add, store -> a switch can happen between them
Run in several threads, the final counter can fall short. Use threading.Lock, or
better, queue.Queue and immutable data. Things like list.append or dict[key] = v are
atomic in CPython, but relying on that is fragile — compound "check then act" sequences
still race.
Threads vs. Processes
threading |
multiprocessing |
|
|---|---|---|
| Memory | Shared address space | Separate memory per process |
| GIL | One shared | One per process |
| CPU-bound speedup | No (pure Python) | Yes |
| Startup / overhead | Cheap | Heavy (fork/spawn) |
| Data exchange | Direct object access | Pickled through pipes/queues or shared memory |
| Failure isolation | A crash can take down all | Process dies alone |
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=16) as pool: # I/O-bound
pages = list(pool.map(fetch, urls))
with ProcessPoolExecutor() as pool: # CPU-bound
results = list(pool.map(crunch, chunks))
concurrent.futures gives both a single interface; prefer it over raw Thread/Process
unless you need fine control.
Multiprocessing Memory Model
- Each child has its own copy of memory. Arguments and return values are pickled, so they must be picklable and large payloads are expensive.
- Start method matters:
fork(Linux default in older versions) copies the parent copy-on-write — fast, but unsafe with threads or held locks;spawn(macOS/Windows default) starts a fresh interpreter and re-imports your module, so guard entry points withif __name__ == "__main__":. - To share data without copying, use
multiprocessing.shared_memory,Value/Array, or a manager; otherwise pass messages. - Note that reference counting writes to objects, so "copy-on-write" pages get copied anyway as children touch shared objects.
Choosing a Model
- Waiting on network/disk, moderate concurrency → threads.
- Massive number of concurrent connections → asyncio.
- Heavy pure-Python computation → processes (or push work into NumPy/C/Rust).
- Mixed → asyncio or threads for I/O,
run_in_executorwith a process pool for CPU.
The GIL's Future
Python 3.13 introduced an experimental free-threaded build (PEP 703) that can run without the GIL, and later releases continue to develop it. It requires extension compatibility and has some single-thread overhead, so the default builds still have the GIL. Even without it, shared mutable state still needs locks.
Key Takeaways
- The GIL protects the interpreter's internals, not your data.
- Threads for I/O, processes for CPU; asyncio for many connections.
- Processes don't share memory: pay for pickling or use explicit shared memory.
- Always protect compound operations on shared state with locks or queues.