Table of Contents

Python GIL

Python GIL (Global Interpreter Lock) is a mutex that prevents multiple threads from executing Python bytecode simultaneously in CPython. Only one thread can hold the GIL at a time, making CPU-bound multithreading ineffective. The GIL exists because CPython's memory management isn't thread-safe; removing it hurts single-threaded performance.

Use multiprocessing for CPU-bound parallelism, threading for I/O-bound concurrency, or async for high-concurrency I/O.

Example

This example demonstrates GIL limitations and workarounds.

# run: python3 gil.py
# description: GIL effects on threading and multiprocessing
 
import threading
import time
import multiprocessing
 
def cpu_bound_task(n):
    """Pure CPU work (no I/O)."""
    total = 0
    for i in range(n):
        total += i
    return total
 
# Threading: GIL prevents parallelism
print("Threading (GIL limited):")
start = time.time()
 
def thread_worker():
    cpu_bound_task(100_000_000)
 
threads = []
for _ in range(2):
    t = threading.Thread(target=thread_worker)
    threads.append(t)
    t.start()
 
for t in threads:
    t.join()
 
thread_elapsed = time.time() - start
print(f"  2 threads: {thread_elapsed:.3f}s")
 
# Multiprocessing: separate processes, no GIL
print("\nMultiprocessing (no GIL):")
start = time.time()
 
processes = []
for _ in range(2):
    p = multiprocessing.Process(target=cpu_bound_task, args=(100_000_000,))
    processes.append(p)
    p.start()
 
for p in processes:
    p.join()
 
process_elapsed = time.time() - start
print(f"  2 processes: {process_elapsed:.3f}s")
 
print(f"\nSpeedup: {thread_elapsed / process_elapsed:.1f}x")
 
# I/O-bound: threading is fine
print("\n" + "="*60)
print("I/O-bound threading (GIL released during I/O):")
 
def io_bound_task():
    """I/O work (releases GIL)."""
    time.sleep(1)  # GIL released during sleep
 
start = time.time()
threads = []
for _ in range(3):
    t = threading.Thread(target=io_bound_task)
    threads.append(t)
    t.start()
 
for t in threads:
    t.join()
 
io_elapsed = time.time() - start
print(f"  3 I/O tasks in threads: {io_elapsed:.3f}s")
print(f"  (should be ~1s, not ~3s)")
 
# GIL release points
print("\n" + "="*60)
print("GIL is released during:")
print("  - I/O operations (read, write, network)")
print("  - time.sleep()")
print("  - C extensions (NumPy, etc.) often release GIL")
print("  - threading.Lock/RLock acquisition")
 
# sys.getswitchinterval() and GIL
import sys
print(f"\nGIL switch interval: {sys.getswitchinterval()}s")
print("  (how often to switch between threads)")

Common patterns

GIL behavior:

For CPU-bound work:

For I/O-bound work:

For high-concurrency I/O:

Alternatives to threading:

Thread safety without GIL:

NumPy and GIL:

GIL internals:

Common misconceptions:

When to use each: