The Global Interpreter Lock (GIL)
Python · Concurrency · Run with python3
Explanation
The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time, even on multi-core CPUs. It exists because CPython's memory management (reference counting) is not thread-safe. The GIL makes single-threaded programs faster (no locking overhead) and C extension writing simpler. The consequence: CPU-bound multi-threaded Python programs don't scale across cores. I/O-bound workloads are largely unaffected — the GIL is released while waiting for I/O. Python 3.13 introduced an optional free-threaded build (`python3.13t`) that removes the GIL.
Real-World Scenario
Two threads computing prime numbers simultaneously on a 16-core machine run at the same total speed as one thread — the GIL serializes them. The same two threads making HTTP requests run twice as fast — each thread releases the GIL while waiting for the network, so they truly overlap. ```python import threading import time def cpu_bound(n: int) -> int: """Count-down loop — pure Python, GIL-bound.""" while n > 0: n -= 1 return n N = 10_000_000 # Single-threaded baseline start = time.perf_counter() cpu_bound(N) cpu_bound(N) single = time.perf_counter() - start # Multi-threaded — GIL prevents true parallelism t1 = threading.Thread(target=cpu_bound, args=(N,)) t2 = threading.Thread(target=cpu_bound, args=(N,)) start = time.perf_counter() t1.start(); t2.start() t1.join(); t2.join() multi = time.perf_counter() - start print(f"Single: {single:.2f}s") print(f"Multi: {multi:.2f}s (should be ~same or slower due to GIL)") ```
Quick Snippet
import threading
import time
def cpu_bound(n: int) -> int:
"""Count-down loop — pure Python, GIL-bound."""
while n > 0:
n -= 1
return n
N = 10_000_000
# Single-threaded baseline
start = time.perf_counter()
cpu_bound(N)
cpu_bound(N)
single = time.perf_counter() - start
# Multi-threaded — GIL prevents true parallelism
t1 = threading.Thread(target=cpu_bound, args=(N,))
t2 = threading.Thread(target=cpu_bound, args=(N,))
start = time.perf_counter()
t1.start(); t2.start()
t1.join(); t2.join()
multi = time.perf_counter() - start
print(f"Single: {single:.2f}s")
print(f"Multi: {multi:.2f}s (should be ~same or slower due to GIL)")Full Example
# Compile: python3 gil_demo.py
# Output:
# Single and multi-threaded times printed (multi ~= single due to GIL)
import threading
import time
def cpu_bound(n: int) -> int:
while n > 0:
n -= 1
return n
N = 2_000_000
start = time.perf_counter()
cpu_bound(N)
cpu_bound(N)
single = time.perf_counter() - start
t1 = threading.Thread(target=cpu_bound, args=(N,))
t2 = threading.Thread(target=cpu_bound, args=(N,))
start = time.perf_counter()
t1.start(); t2.start()
t1.join(); t2.join()
multi = time.perf_counter() - start
print(f"Single-threaded: {single:.3f}s")
print(f"Multi-threaded: {multi:.3f}s")
print(f"Ratio: {multi/single:.2f}x (expect ~1.0 due to GIL)")
print("For CPU-bound work, use multiprocessing instead.")
python3 gil_demo.py