Concurrency (GIL, Threading, Asyncio)
Why Python threads don't parallelize CPU work, and the two real ways around it: multiprocessing and asyncio.
Try answering in your head first, then click a question to check the model answer.
Q1.Why doesn't Python's `threading` module give you true parallelism for CPU-bound work?(show answer)
The GIL allows only one thread to execute Python bytecode at a time, regardless of how many CPU cores are available — so CPU-bound threads take turns rather than running simultaneously. For CPU-bound parallelism, multiprocessing is needed instead, since each process gets its own Python interpreter and GIL.
Q2.When is `threading` still genuinely useful in Python, given the GIL?(show answer)
For I/O-bound work — network requests, file/database I/O — where a thread spends most of its time waiting rather than executing Python bytecode. The GIL is released during blocking I/O calls, so other threads can run during that wait, giving real concurrency benefit even though CPU-bound code can't run in true parallel.
Q3.How does `asyncio` achieve concurrency differently from `threading`?(show answer)
asyncio uses a single thread and an event loop that cooperatively switches between tasks at await points — a task voluntarily yields control while waiting on I/O, letting the loop run other tasks in the meantime. This avoids thread-switching overhead and race conditions from preemptive context switches, but it only works if the code actually awaits non-blocking operations; a blocking call in async code stalls the entire event loop.
Q4.For a task that's both I/O-bound and CPU-bound in parts, how would you combine these concurrency models?(show answer)
Use asyncio (or threads) for the I/O-bound parts to overlap waiting time, and offload the CPU-bound parts to a multiprocessing pool (or loop.run_in_executor with a process pool from async code) so the heavy computation runs on separate cores in parallel rather than blocking the event loop or being serialized by the GIL.
