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.
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)")
GIL behavior:
For CPU-bound work:
multiprocessing: separate process, separate GILFor I/O-bound work:
threading: efficient, GIL released during I/OFor high-concurrency I/O:
asyncio or trio: even lighter than threadsAlternatives to threading:
multiprocessing.Pool: pool of worker processesconcurrent.futures.ThreadPoolExecutor: thread pool for I/Oconcurrent.futures.ProcessPoolExecutor: process pool for CPUasyncio: event loop for I/OThread safety without GIL:
x += 1) aren't thread-safe without lockingNumPy and GIL:
GIL internals:
sys.getswitchinterval(): how often to switch threads (default 5ms)sys.setswitchinterval(seconds): adjust (rarely needed)Common misconceptions:
When to use each: