Skip to content

Async & Concurrency

Python gives you three distinct concurrency models, each suited to a different class of problem:

ModelBest forMechanism
asyncioI/O-bound work (network, files, DB)Single thread, cooperative multitasking
threadingI/O-bound work that uses blocking libsOS threads, limited by the GIL
multiprocessingCPU-bound work (computation, parsing)Separate processes, true parallelism

Choosing the wrong model is the most common source of performance bugs in Python. A blocking database call inside a threading.Thread may look fine, but that same call inside an asyncio coroutine will freeze the entire event loop.

Most server-side Python work is I/O-bound: waiting for a database, an HTTP API, a file read. asyncio handles thousands of simultaneous waits inside a single OS thread by running an event loop that yields control between coroutines whenever one is blocked on I/O.

import asyncio
async def fetch(url: str) -> str:
await asyncio.sleep(0.1) # simulates a network delay
return f"Response from {url}"
async def main():
results = await asyncio.gather(
fetch("https://api.example.com/a"),
fetch("https://api.example.com/b"),
)
for r in results:
print(r)
asyncio.run(main())

Both fetches run concurrently — neither blocks the other. Total time is roughly 0.1 s, not 0.2 s.

CPython holds a lock called the Global Interpreter Lock (GIL) that allows only one thread to execute Python bytecode at a time. For I/O-bound work this does not matter — threads release the GIL while waiting for the OS — but for CPU-bound work (tight loops, number crunching), adding threads provides zero speedup.

import threading
def count_down(n: int) -> None:
while n > 0:
n -= 1
# Two threads — still runs on one core due to the GIL
t1 = threading.Thread(target=count_down, args=(5_000_000,))
t2 = threading.Thread(target=count_down, args=(5_000_000,))
t1.start(); t2.start()
t1.join(); t2.join()

multiprocessing spawns separate OS processes, each with its own Python interpreter and GIL. CPU-bound work can therefore use all available cores.

from multiprocessing import Pool
def square(x: int) -> int:
return x * x
with Pool(processes=4) as pool:
results = pool.map(square, range(10))
print(results)
LessonCore concept
asyncio BasicsEvent loop, async def, await, asyncio.run, asyncio.gather
Async / AwaitTasks, create_task, sequential vs concurrent execution
GIL & ThreadsThe GIL, threading, ThreadPoolExecutor, I/O vs CPU-bound
MultiprocessingProcesses vs threads, ProcessPoolExecutor, Pool.map

Each lesson is self-contained. Start with asyncio if you are building network services; jump to multiprocessing if you need to parallelize CPU-bound computation.

Which Python concurrency model is best suited for CPU-bound tasks like numerical computation?
Why does asyncio handle thousands of concurrent I/O waits efficiently in a single thread?
What is the Global Interpreter Lock (GIL)?
Which statement about threading and CPU-bound Python work is correct?