Concurrency (GIL, Threading, Asyncio)
Why Python threads don't parallelize CPU work, and the two real ways around it: multiprocessing and asyncio.
The GIL
CPython's Global Interpreter Lock allows only one thread to execute Python bytecode at a time, even on a multi-core machine. This means threading doesn't speed up CPU-bound work (the GIL serializes it anyway) — but it does help I/O-bound work, since a thread waiting on network/disk releases the GIL for others.
Multiprocessing
multiprocessing sidesteps the GIL entirely by running separate OS processes, each with its own interpreter and memory space — true parallelism for CPU-bound work, at the cost of higher memory use and needing to explicitly serialize data passed between processes.
Asyncio
asyncio gives cooperative concurrency on a single thread: an async def coroutine voluntarily yields control at each await, letting the event loop run other coroutines while one is waiting on I/O. It doesn't add parallelism — it adds concurrency for I/O-bound work without the overhead of threads or processes.
Choosing among the three
CPU-bound → multiprocessing. I/O-bound with many concurrent connections → asyncio. I/O-bound with simpler, blocking-library code that isn't async-native → threading.
