If you've done any research into Python and multithreading, you've run into the Global Interpreter Lock (GIL). It's often blamed for Python being "slow" at concurrency — but the reality is more nuanced.
What Is the GIL?
The GIL is a mutex in CPython (the standard Python implementation) that ensures only one thread executes Python bytecode at a time, even on multi-core machines. Only one thread can hold the GIL at any given moment.
import threading
def count(n):
while n > 0:
n -= 1
t1 = threading.Thread(target=count, args=(50_000_000,))
t2 = threading.Thread(target=count, args=(50_000_000,))
t1.start(); t2.start()
t1.join(); t2.join()
You might expect this to run twice as fast using two cores. It won't — the GIL forces the threads to take turns.
Why Does It Exist?
CPython manages memory using reference counting. Every object tracks how many references point to it, and when that count hits zero, the object is deallocated. Without a lock, two threads could increment/decrement a reference count simultaneously, causing race conditions, memory corruption, or crashes.
The GIL was a simple, effective solution: only let one thread touch Python objects at a time. It also made single-threaded code fast, since there's no need for fine-grained locking everywhere.
When the GIL Doesn't Matter
The GIL is only a bottleneck for CPU-bound work. For I/O-bound work — network calls, file reads, database queries — threads release the GIL while waiting, so threading still helps a lot:
import threading
import requests
urls = ["https://example.com"] * 10
def fetch(url):
requests.get(url)
threads = [threading.Thread(target=fetch, args=(u,)) for u in urls]
for t in threads: t.start()
for t in threads: t.join()
This runs far faster than fetching sequentially, because while one thread waits on the network, another can run.
Working Around the GIL
For CPU-bound parallelism, you have a few real options:
- multiprocessing: spins up separate processes, each with its own interpreter and GIL, achieving true parallelism.
- asyncio: single-threaded concurrency for I/O-bound tasks, avoiding threading overhead entirely.
- C extensions / NumPy: many numerical libraries release the GIL during heavy computation, so operations like matrix multiplication genuinely run in parallel under the hood.
from multiprocessing import Pool
def square(x):
return x * x
with Pool(4) as p:
results = p.map(square, range(1_000_000))
The Future: Free-Threaded Python
Recent CPython releases have introduced an experimental "free-threaded" build (PEP 703) that removes the GIL entirely, aiming to enable true multithreaded parallelism. It's still maturing and not yet the default, but it signals a real shift in how Python handles concurrency long-term.
Key Takeaway
The GIL isn't a sign that Python "can't do concurrency" — it's a design tradeoff that keeps memory management simple and single-threaded code fast. Know when your workload is I/O-bound (threads work great) versus CPU-bound (reach for multiprocessing or GIL-releasing libraries instead).
Comments (0)
No comments yet
Be the first to share your thoughts!