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)
- What the GIL is
- Why CPython has a GIL
- When the GIL is released
- GIL switching interval
- Practical impact by workload
multiprocessingvsthreading- Releasing the GIL in C extensions
asyncioand the GIL- Free-threaded CPython (3.13+)
- Diagnosing GIL-bound slowness
- 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#
- 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.
- C extension simplicity — Extension authors can assume single-threaded bytecode unless they explicitly release the GIL.
- 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:
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#
- I/O-bound →
asyncioorthreading/ThreadPoolExecutor - CPU-bound pure Python →
multiprocessing/ProcessPoolExecutor - CPU-bound C libraries → threads may work; benchmark
- Mixed → split pipeline: async/thread for I/O, process pool for compute
- 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#
- Why does CPython need a GIL for reference counting?
- When is the GIL released during
time.sleep()? - Why doesn't
ThreadPoolExecutorspeed up a pure Python numeric loop? - How does
multiprocessingachieve true parallelism? - What is the difference between GIL contention and I/O waiting?
- 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 |