Threads share memory, processes isolate memory, asyncio multiplexes IO in one thread
threading runs multiple threads in one process with shared memory but serialized bytecode because of the GIL. multiprocessing runs multiple interpreter processes with separate memory, giving true CPU parallelism at the cost of serialization and IPC. asyncio runs one thread with a cooperative event loop that switches between coroutines at await points, giving very high IO concurrency with low per-task overhead but requiring non-blocking code end to end. The choice follows the workload: IO-bound with many short tasks favors asyncio or threads; CPU-bound favors processes; mixed workloads often combine them, for example an asyncio service that offloads CPU work to a process pool.
threading: shared state, simple mental model, good for blocking libraries without async support, limited CPU parallelism.
multiprocessing: true parallel CPU work, isolated memory, cost of pickling and process startup, harder to share state.
asyncio: single thread, huge IO concurrency, must avoid blocking calls, requires async-compatible libraries end to end.
Trade-off: asyncio is fastest at IO fan-out but any blocking call stalls the whole loop. Threads tolerate blocking libraries better but scale worse at high concurrency.
Common mistake: mixing blocking libraries into asyncio code and accidentally freezing the loop.
Common mistake: using multiprocessing with large objects and paying a serialization cost larger than the parallel speedup.
Version note: asyncio was stabilized in 3.4+, and asyncio.to_thread was added in 3.9 to offload blocking calls cleanly. Process pools have been in concurrent.futures since 3.2.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience