The GIL (Global Interpreter Lock)

RoadmapsPython

Scenario

You spin up 4 threads on your 4-core server to process a massive dataset, expecting it to be 4x faster. But it actually takes slightly longer than using 1 thread. Why?

Mental model

The GIL is a speaking baton. Even if there are 10 people in the room (threads), only the person holding the baton is allowed to speak (execute Python code).

The Global Interpreter Lock (GIL) is a mutex in CPython that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This means no matter how many threads or CPU cores you have, only one thread can execute Python code at any given time.

Deep dive

Python's memory management uses reference counting. If two threads increase a variable's reference count at the exact same nanosecond, the count might only go up by 1 instead of 2. The variable would eventually be deleted while a thread was still using it, causing a crash. The GIL was introduced as a simple, foolproof lock to prevent these race conditions at the interpreter level.

The GIL exists to make Python's memory management (reference counting) thread-safe.

The massive consequence: Python threads CANNOT run in parallel. A multi-threaded Python program is strictly concurrent. If you have CPU-bound work (like crunching numbers), threads will fight over the GIL, and the overhead of switching the lock will actually make the program slower than a single thread.

The GIL prevents true parallelism for Python threads.

However, the GIL is released when a thread does I/O (like network requests, reading a file, or waiting for a database). During that time, another thread can acquire the GIL and execute Python code. This means threading IS useful in Python, but only for I/O-bound tasks.

The GIL is dropped during I/O, allowing other threads to run.

Code examples

CPU-Bound: Threads fight for the GIL (Slower)

import threading

def count_down(n):
    while n > 0:
        n -= 1

# Using 2 threads to do CPU-heavy math
t1 = threading.Thread(target=count_down, args=(50_000_000,))
t2 = threading.Thread(target=count_down, args=(50_000_000,))

t1.start(); t2.start()
t1.join(); t2.join()

# This takes LONGER than running count_down(100_000_000) sequentially!
# Both threads are fighting for the single GIL.

Because of the GIL, `t1` and `t2` cannot execute their `while` loops simultaneously. They rapidly pass the GIL back and forth, and that context-switching overhead makes it slower than doing the math in one thread.

I/O-Bound: The GIL is released (Faster)

import threading
import time

def simulated_network_call():
    # The GIL is RELEASED during time.sleep() (or any I/O)
    time.sleep(1)

t1 = threading.Thread(target=simulated_network_call)
t2 = threading.Thread(target=simulated_network_call)

start = time.time()
t1.start(); t2.start()
t1.join(); t2.join()

# This takes ~1 second, not 2 seconds.
print(f"Took {time.time() - start:.2f}s")

When `t1` hits the blocking I/O (simulated by `sleep`), it drops the GIL. `t2` grabs the GIL and also starts waiting. They wait concurrently, finishing in 1 second total.

Common mistakes

Recall questions

Questions & answers

What is the GIL and how does it affect threads?

The Global Interpreter Lock ensures only one thread executes Python code at a time. It prevents CPU-bound threads from running in parallel, making them useless for performance gains. However, I/O-bound threads drop the GIL while waiting, so multithreading is still highly effective for network or disk operations.

Approach: Define it, explain WHY it restricts parallelism (CPU-bound bottleneck), and ALWAYS mention the I/O-bound exception where it's released.

If you have a 16-core machine, how can you bypass the GIL to fully utilise the CPU for a heavy data-processing script?

You must use multiprocessing instead of threading. The `multiprocessing` module spawns separate OS processes, each with its own memory space and its own Python interpreter/GIL, allowing them to run truly in parallel across the 16 cores.

Approach: State that threading cannot bypass the GIL, but multiprocessing can because it isolates the interpreters.

Continue learning

Previous: Concurrency vs Parallelism

Next: Threading for I/O-bound work

Next: Threading vs Multiprocessing

Related: Threading vs Multiprocessing

Return to Python Roadmap