Python offers different concurrency models for handling I/O-bound and CPU-bound workloads. Choosing the right approach depends on whether tasks spend time waiting or performing computations.
An asynchronous application can handle many I/O operations concurrently on a single thread. Multiprocessing can execute CPU-intensive tasks in parallel across CPU cores.
Threading runs multiple threads within one process. Threads share memory and are useful for workloads that spend time waiting for network requests, file operations, or other blocking I/O.
from concurrent.futures import ThreadPoolExecutorimport timedef fetch_data(item: int) -> str:time.sleep(1)return f"Fetched {item}"with ThreadPoolExecutor(max_workers=3) as executor:results = list(executor.map(fetch_data, [1, 2, 3]))print(results)
The tasks can overlap their waiting periods instead of executing sequentially.
Multiprocessing runs tasks in separate processes, each with its own memory space. It is useful for CPU-intensive computations that benefit from parallel execution.
from concurrent.futures import ProcessPoolExecutordef square(number: int) -> int:return number * numberif __name__ == "__main__":with ProcessPoolExecutor() as executor:results = list(executor.map(square, range(5)))print(results)
The __main__ guard is important for portable multiprocessing code.
In conventional CPython builds, the GIL limits multiple threads from executing Python bytecode simultaneously within the same interpreter.
| Workload | Typical approach |
|---|---|
| Many asynchronous HTTP requests | asyncio |
| Blocking I/O with synchronous libraries | Threading |
| CPU-intensive Python computations | Multiprocessing |
For blocking I/O inside asynchronous code, asyncio.to_thread() can move the operation to a worker thread. It is generally not a solution for speeding up pure Python CPU-bound work under the conventional GIL.