The Global Interpreter Lock (GIL) is a mutex inside the standard CPython interpreter that allows only one thread to execute Python bytecode at a time, even on a multi-core machine. This is one of the most commonly misunderstood facts about Python performance:
import threading
def cpu_bound_work():
total = 0
for i in range(10_000_000):
total += i
return total
# Two threads running cpu_bound_work do NOT run twice as fast on a multi-core
# machine — the GIL means only one is actually executing Python bytecode at
# any given instant, so this gains essentially nothing over running it twice
# sequentially (and loses a bit to thread-switching overhead).
| Workload type | Best tool | Why |
|---|---|---|
| CPU-bound (heavy computation) | multiprocessing | Separate processes each get their own GIL and their own interpreter — genuine parallelism across cores |
| I/O-bound (network calls, file/disk waits) | threading or asyncio | The GIL is released while waiting on I/O, so other threads/coroutines can run during that wait |
Threads in Python are genuinely useful for I/O-bound work — a thread blocked on requests.get() or a database query releases the GIL during the wait, letting other threads make progress — the GIL is a bottleneck specifically for CPU-bound work spread across threads, not for I/O-bound concurrency.
asyncio basicsimport asyncio
async def fetch_data(delay):
print("start")
await asyncio.sleep(delay) # yields control back to the event loop during the wait
print("done")
return "data"
async def main():
results = await asyncio.gather(fetch_data(1), fetch_data(2)) # runs concurrently
print(results)
asyncio.run(main())
async/await is cooperative concurrency on a single thread: await explicitly yields control back to the event loop, letting other coroutines run during a wait, rather than the OS preemptively switching between threads. asyncio.gather runs the two fetch_data calls concurrently — the total time is roughly max(1, 2) seconds, not 1 + 2, because both sleeps are in flight at once. A common mistake is calling an async def function without await-ing it (or passing it to asyncio.run/gather) — that only creates a coroutine object and never actually runs its body, silently doing nothing.