Free — no signup required

The GIL (Global Interpreter Lock)

3 min read

The Single-Lane Highway

Imagine a busy kitchen with multiple chefs (threads) but only one knife (the GIL). No matter how many chefs you hire, only one can cut vegetables at a time. The others wait. Adding more chefs doesn't speed up the cutting — it just adds coordination overhead.

The Global Interpreter Lock (GIL) is a mutex (mutual exclusion lock) built into CPython — the standard Python interpreter you get when you run python3. It prevents more than one thread from executing Python bytecode at the same time, even on a machine with dozens of CPU cores.

This is a deliberate design decision, not an oversight. The GIL simplifies CPython's memory management, particularly its reference-counting garbage collector. Without the GIL, two threads could simultaneously modify an object's reference count, causing memory corruption. The GIL trades parallelism for simplicity and safety.

What This Means in Practice

The impact depends entirely on what your code is doing:

CPU-Bound Work (threads make it worse):
CPU-bound tasks require continuous computation — parsing JSON, compressing data, running cryptographic operations. With threads, you pay the overhead of context switching between threads, but you never get actual parallel execution. The result is often slower than single-threaded code.

import threading
import time

def crunch_numbers():
    # Simulate CPU-intensive work
    total = 0
    for i in range(10_000_000):
        total += i
    return total

# Two threads — you'd expect 2x speedup. You won't get it.
start = time.time()
t1 = threading.Thread(target=crunch_numbers)
t2 = threading.Thread(target=crunch_numbers)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Threaded: {time.time() - start:.2f}s")  # Often SLOWER than sequential

I/O-Bound Work (threads work well):
When a thread is waiting for a network response or disk read, it releases the GIL. Other threads can run during that wait. This is why threading works well for scraping APIs, querying databases, or reading many files concurrently — the threads spend most of their time waiting, not computing.

import threading
import urllib.request

urls = [
    "http://example.com",
    "http://example.org",
    "http://example.net",
]

def fetch(url):
    urllib.request.urlopen(url)
    print(f"Fetched {url}")

# These threads genuinely run concurrently because they spend time waiting
threads = [threading.Thread(target=fetch, args=(u,)) for u in urls]
for t in threads: t.start()
for t in threads: t.join()

The Real Fix: Multiprocessing

To use multiple CPU cores for CPU-bound work, you must use separate processes instead of threads. Each process has its own Python interpreter and its own GIL, so they truly run in parallel.

from multiprocessing import Pool

def crunch_numbers(_):
    total = 0
    for i in range(10_000_000):
        total += i
    return total

# Spawns 4 separate Python processes — genuinely parallel
with Pool(processes=4) as pool:
    results = pool.map(crunch_numbers, range(4))

The trade-off: processes are heavier than threads. They don't share memory, so passing data between them requires serialization (pickling). For large datasets, this overhead can dominate. The concurrent.futures module provides a clean unified API (ThreadPoolExecutor for I/O, ProcessPoolExecutor for CPU) that lets you swap strategies with one line change.

Interview Tip

A classic interview question: "You wrote a Python script using threads to process files in parallel but it's not faster. Why?" The expected answer names the GIL, distinguishes CPU-bound vs I/O-bound work, and proposes multiprocessing or ProcessPoolExecutor as the fix. Bonus points for mentioning that asyncio is a third option for I/O-bound work that avoids thread overhead entirely by using cooperative multitasking in a single thread.

Key Point: The GIL is a CPython implementation detail, not a Python language requirement. Jython (Python on the JVM) and PyPy-STM don't have it. But for standard CPython: use threads for I/O-bound concurrency, use multiprocessing for CPU-bound parallelism, and use asyncio for high-volume I/O with low overhead.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free