The GIL is a mutex (a lock) in CPython that allows only one thread to execute Python bytecode at a time. Even on a 16-core machine, only one thread runs Python code at any given moment.
This is one of the most misunderstood parts of Python. Let’s break it down.
Why Does the GIL Exist?
Remember reference counting from the previous note? Every object has a reference count. If two threads modify that count simultaneously without a lock, we get a race condition — the count could go wrong, leading to memory leaks or crashes.
The GIL is the simplest solution: one big lock around the entire interpreter. Only one thread can touch Python objects at a time, so reference counting stays safe.
CPU-Bound vs I/O-Bound
The GIL’s impact depends on what kind of work we’re doing:
CPU-bound (number crunching, image processing) — the GIL hurts. Threads can’t run in parallel. Adding more threads might even make things slower due to GIL contention.
import threading, time
def count():
total = 0
for i in range(50_000_000):
total += 1
# Single-threaded
start = time.time()
count()
count()
print(f"Sequential: {time.time() - start:.2f}s")
# Multi-threaded — NOT faster because of GIL!
start = time.time()
t1 = threading.Thread(target=count)
t2 = threading.Thread(target=count)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Threaded: {time.time() - start:.2f}s") # about the same or slower
I/O-bound (network requests, file reads, database queries) — the GIL is fine. Python releases the GIL while waiting for I/O, so other threads can run.
import threading, time, urllib.request
def fetch(url):
urllib.request.urlopen(url)
# Threads help here — GIL released during network wait
urls = ["https://python.org"] * 5
threads = [threading.Thread(target=fetch, args=(u,)) for u in urls]
for t in threads: t.start()
for t in threads: t.join()
Workarounds for CPU-Bound Work
1. multiprocessing — Separate Processes
Each process gets its own Python interpreter and GIL. True parallelism.
from multiprocessing import Pool
def heavy_work(n):
return sum(range(n))
with Pool(4) as pool: # 4 separate processes
results = pool.map(heavy_work, [10**7] * 4)
2. C Extensions
Libraries like NumPy release the GIL when doing heavy computation in C. That’s why NumPy operations are fast even with threads.
3. asyncio for I/O
For I/O-bound work, asyncio is often better than threads — lighter weight, no GIL worries.
Python 3.13: Free-Threaded Mode
Python 3.13 introduced an experimental free-threaded mode (PEP 703) that removes the GIL entirely. It’s opt-in and not production-ready yet, but it’s a big step toward true multithreading in Python.
# Build CPython with --disable-gil (experimental)
python3.13t # the free-threaded build
This is still evolving, but it signals that the GIL’s days may be numbered.
In simple language, the GIL is a lock that prevents multiple threads from running Python code simultaneously. It exists to keep reference counting safe. For I/O-bound work, it’s not a problem. For CPU-bound work, use multiprocessing to sidestep it entirely.