Gyaan

Global Interpreter Lock (GIL)

advanced GIL threading CPython concurrency

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.

GIL Thread Switching Timeline
Thread A
running
waiting
running
...
Thread B
waiting
running
waiting
...
GIL
held by A
held by B
held by A
B
Threads take turns — only one executes Python bytecode at a time

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.