Skip to content

Global Interpreter Lock#

The Global Interpreter Lock (GIL) is a mutex in CPython that allows only one thread to execute Python bytecode at a time in a single process. It simplifies memory management (reference counting) and C API integration, but limits CPU parallelism for pure Python threads. Understanding when the GIL is held, released, and how to work around it is central to Python concurrency interviews and production architecture.

How to use this page

Read alongside Threading and Reference Counting. This page describes CPython — other implementations (Jython, IronPython) may differ; PyPy has a different threading model.

At a glance
Track Python Advanced → Python Internals
Sections 11 major topics
Outline Use the right-hand TOC to jump

Topics: What the GIL is · Why CPython has a GIL · When the GIL is released · GIL switching interval · Practical impact by workload · multiprocessing vs threading · Releasing the GIL in C extensions · asyncio and the GIL · … (+3 more)

  1. What the GIL is
  2. Why CPython has a GIL
  3. When the GIL is released
  4. GIL switching interval
  5. Practical impact by workload
  6. multiprocessing vs threading
  7. Releasing the GIL in C extensions
  8. asyncio and the GIL
  9. Free-threaded CPython (3.13+)
  10. Diagnosing GIL-bound slowness
  11. Design guidelines

What the GIL is#

Process (CPython)
├── Thread A  ──┐
├── Thread B  ──┼──> GIL (one bytecode executor at a time)
└── Thread C  ──┘

The GIL protects CPython internals — especially reference count updates — from race conditions between threads. Without it, two threads incrementing/decrementing ob_refcnt simultaneously could corrupt memory.

Property Detail
Scope Per process (each interpreter has its own GIL)
Granularity Bytecode instructions (not Python source lines)
Implementation OS mutex (pthread lock on Linux, etc.)
Alternative builds Python 3.13+ optional free-threaded build (--disable-gil)

Why CPython has a GIL#

  1. Reference counting — Most objects use immediate refcounting; inc/dec must be atomic or locked. A process-wide lock was the pragmatic CPython choice in the 1990s.
  2. C extension simplicity — Extension authors can assume single-threaded bytecode unless they explicitly release the GIL.
  3. Single-threaded performance — Fine-grained locking on every object would slow the common case (one thread).

The GIL is a CPython implementation detail, not a Python language feature.


When the GIL is released#

CPython releases the GIL around blocking or long-running operations, allowing other threads to run:

Operation GIL released?
time.sleep() Yes
File I/O (read/write) Yes (during blocking syscall)
Socket I/O Yes
threading.Lock.acquire() (blocking) Yes while waiting
Many C extensions (NumPy, zlib, hashlib) Often yes, during C computation
Pure Python bytecode loop No (switches between threads periodically)
import time
from threading import Thread

def io_bound():
    time.sleep(2)  # GIL released — other threads can run

def cpu_bound():
    total = 0
    for i in range(10_000_000):
        total += i  # Holds GIL — little benefit from threads

GIL switching interval#

CPython periodically checks whether to switch threads (default sys.setswitchinterval, 0.005 seconds in 3.13+). A CPU-bound thread can starve others briefly, but switches still occur.

import sys
print(sys.getswitchinterval())  # seconds between GIL switch checks
sys.setswitchinterval(0.001)    # more frequent switches (not a parallelism fix)

Lowering switch interval does not create CPU parallelism

It only makes thread scheduling fairer for mixed workloads — all CPU-bound Python still runs on one core's bytecode at a time.


Practical impact by workload#

Workload Threads effective? Better alternative
100 HTTP downloads Yes asyncio or ThreadPoolExecutor
Image resize in Pillow (C) Often yes Threads (GIL released in C)
JSON parse pure Python Limited multiprocessing or orjson
ML inference (PyTorch C++) Often yes Threads or dedicated runtime
Parallel for loop crunching No ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor

def compute(n: int) -> int:
    return sum(i * i for i in range(n))

if __name__ == "__main__":
    with ProcessPoolExecutor() as pool:
        results = list(pool.map(compute, [1_000_000] * 8))

Each process has its own interpreter and GIL — true parallel CPU on multiple cores.


multiprocessing vs threading#

Aspect threading multiprocessing
Memory Shared Separate (copy-on-write where possible)
Startup cost Low Higher (spawn interpreter)
IPC Shared objects (careful) Pipes, queues, shared memory
GIL One per process, shared by threads One GIL per process
Pickling Not required Required for spawn (macOS, Windows default)

On Linux, fork start method inherits memory layout; still use explicit queues for communication clarity.


Releasing the GIL in C extensions#

C extension authors wrap long C-only work:

Py_BEGIN_ALLOW_THREADS
/* C code that does not touch Python objects */
Py_END_ALLOW_THREADS

This is why NumPy matrix multiply can parallelize internally while Python threads wait — the heavy work runs without the GIL.


asyncio and the GIL#

asyncio runs coroutines on a single thread — no GIL contention between coroutines because there is no parallel bytecode execution. Concurrency comes from cooperative yielding at await points (non-blocking I/O).

import asyncio
import httpx

async def main():
    async with httpx.AsyncClient() as client:
        r1, r2 = await asyncio.gather(
            client.get("https://a.com"),
            client.get("https://b.com"),
        )

For I/O-heavy services, asyncio often scales better than threads with lower memory overhead. See HTTP Requests.


Free-threaded CPython (3.13+)#

Python 3.13 introduced an optional build with the GIL disabled (python3.13t / sys._is_gil_enabled()):

import sys
if hasattr(sys, "_is_gil_enabled"):
    print(sys._is_gil_enabled())  # False in free-threaded build
Consideration Detail
Status Experimental as of 3.13
Extensions Must be rebuilt for free-threading (Py_GIL_DISABLED)
Refcounting Uses biased refcounting + deferred collection
When to adopt Only after dependency audit — not default production yet

Diagnosing GIL-bound slowness#

import threading
print(threading.active_count())

# If CPU is 100% on one core with many threads — likely GIL-bound Python
# If CPU spread across cores with threads — likely C extensions releasing GIL

Tools: py-spy (sampling profiler), cProfile, perf on Linux. Look for one hot Python frame dominating a single core.


Design guidelines#

  1. I/O-boundasyncio or threading / ThreadPoolExecutor
  2. CPU-bound pure Pythonmultiprocessing / ProcessPoolExecutor
  3. CPU-bound C libraries → threads may work; benchmark
  4. Mixed → split pipeline: async/thread for I/O, process pool for compute
  5. Shared state → prefer message passing (queues) over shared mutable objects

Interview traps (quick reference)#

Trap What goes wrong Safe approach
"Threads bypass the GIL" False for pure Python CPU work Processes or native code
"GIL exists in all Python" Jython/IronPython differ Specify CPython
"More threads = faster CPU loop" Often slower (overhead) multiprocessing
"GIL is bad, always avoid threads" Threads excel at I/O Match tool to workload
"asyncio releases the GIL" Single-threaded event loop Async for concurrent I/O waits
"Removing GIL is trivial" Breaks C extensions, refcount model Free-threading is gradual
setswitchinterval(0) hack Does not parallelize CPU Processes

Mental model checklist#

  1. Why does CPython need a GIL for reference counting?
  2. When is the GIL released during time.sleep()?
  3. Why doesn't ThreadPoolExecutor speed up a pure Python numeric loop?
  4. How does multiprocessing achieve true parallelism?
  5. What is the difference between GIL contention and I/O waiting?
  6. What is the free-threaded build in Python 3.13?

What's next#

Topic Page
Thread API Threading
Refcount internals Reference Counting
HTTP concurrency HTTP Requests
New 3.13 features New Syntax and Features