What is the GIL (Global Interpreter Lock), and why does it exist?

7 minadvancedconcurrencygilthreading

Quick Answer

The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time, even on a multi-core machine. It exists because CPython's memory management (reference counting) isn't thread-safe by default, and the GIL was the simplest way to make the interpreter thread-safe without requiring fine-grained locking on every object. The practical consequence: pure-Python CPU-bound code doesn't get faster with more threads. I/O-bound code still benefits, because the GIL is released during blocking I/O.

Detailed Answer

What the GIL actually locks

CPython's memory management relies on reference counting: every object tracks how many references point to it, and is freed when that count hits zero. Incrementing/decrementing a refcount from multiple threads simultaneously, without synchronization, is a data race that could corrupt an object's refcount (leading to premature frees or memory leaks). The GIL solves this crudely but effectively: only one thread runs Python bytecode at a time, so refcount updates are never actually concurrent.

import threading

counter = 0

def increment():
    global counter
    for _ in range(1_000_000):
        counter += 1

threads = [threading.Thread(target=increment) for _ in range(4)]
[t.start() for t in threads]
[t.join() for t in threads]
print(counter)   # 4,000,000 -- correct, thanks to the GIL serializing bytecode execution

Without the GIL (or equivalent fine-grained locking), this kind of shared counter update from multiple threads would risk lost updates.

Why "more threads" doesn't mean "more CPU throughput"

def cpu_bound(n):
    return sum(i * i for i in range(n))

# Running cpu_bound() on 4 threads doesn't run 4x faster --
# only one thread executes Python bytecode at any instant, GIL or not.

For CPU-bound pure-Python work, threads provide concurrency (multiple things making progress, interleaved) but not parallelism (multiple things running simultaneously on separate cores). The GIL serializes bytecode execution regardless of how many OS threads and CPU cores exist.

Why threading still helps for I/O-bound work

import time

def slow_io():
    time.sleep(1)   # releases the GIL while "blocked"

Blocking operations that call into C (file/network I/O, time.sleep, many library calls) release the GIL while waiting, letting other Python threads run bytecode in the meantime. This is why threading/concurrent.futures.ThreadPoolExecutor genuinely speed up I/O-bound workloads (many concurrent HTTP requests, for example) even though the GIL exists — the bottleneck (waiting on the network) isn't CPU work at all.

The real workaround for CPU-bound parallelism: separate processes

Since the GIL is per-interpreter process, multiprocessing sidesteps it entirely by running separate Python processes, each with its own GIL, achieving true multi-core parallelism for CPU-bound work at the cost of inter-process communication overhead (data must be pickled/copied between processes, not shared directly).

PEP 703: free-threaded (no-GIL) Python

Starting with Python 3.13, an experimental free-threaded build (python3.13t) removes the GIL, using more fine-grained locking instead, aiming to give real multi-core parallelism to threaded Python code. As of this writing it's still opt-in and the ecosystem (C extensions especially) is still adapting. The standard GIL-enabled build remains the default.

The GIL is CPython's mutex ensuring only one thread executes Python bytecode at a time, needed because refcount-based memory management isn't otherwise thread-safe. It doesn't prevent threading from helping I/O-bound work (the GIL is released during blocking calls), but it does prevent threads from speeding up CPU-bound pure-Python code. For that, use multiprocessing, or Python 3.13+'s experimental free-threaded build.