Explanation
What just happened
Each job waits on the network, then crunches numbers in pure Python. With threads, both fetches waited at once: a thread waiting on I/O lets go of the GIL. Crunching needs the GIL, so the threads took turns on one core.
Try the what-if switch
- multiprocessing: two worker processes, each a separate Python with its own GIL. The fetches overlap and so does the CPU work, on two cores.
- asyncio: one thread runs both tasks. The fetches overlap because
awaithands the thread back.crunch()has noawait, so task b waits until task a is done. Its output order never changes.
Which one to pick
| Waiting on I/O | Python CPU work | |
|---|---|---|
| threading | overlaps | takes turns (GIL) |
| multiprocessing | overlaps | runs in parallel |
| asyncio | overlaps | one task at a time |
Mostly waiting: asyncio or threads. Mostly crunching: multiprocessing, or a free-threaded build.
Common questions
When should I use threading, multiprocessing or asyncio?
Threads or asyncio when the work is mostly waiting on I/O: both overlap the waiting. multiprocessing for pure-Python CPU work: each process has its own GIL.
Do Python threads run in parallel?
Not on the standard build. Only the thread holding the GIL runs Python code, so two CPU-bound threads share one core. Waiting on I/O releases the GIL, so waits overlap.
Is asyncio faster than threading?
Neither speeds up CPU work. asyncio runs on one thread, and its tasks switch only at an await: the order is predictable, but a blocking call stalls every task.
Verified: this timeline matches a real program, checked in CI on Python 3.14, 3.15 and free-threaded 3.15t. How we verify. Unclear or wrong? Tell us