Question presented to candidate: "You have a Python service that needs to do a lot of work at once. How do you decide between threading, multiprocessing and asyncio? Give me the rule, and then the cases where the rule is not enough."
What a strong answer should cover:
- First classify the work: I/O-bound (waiting) vs CPU-bound (computing). On a standard CPython build the GIL lets only one thread run Python bytecode at a time.
- CPU-bound pure Python: processes (
multiprocessing,ProcessPoolExecutor), each with its own interpreter and GIL; mind pickling cost and start-up time. - I/O-bound with blocking libraries or a modest number of tasks: threads (
ThreadPoolExecutor); blocking calls release the GIL. - I/O-bound with very many concurrent connections and async-capable libraries:
asyncioβ one thread, cooperative switching atawait, cheap tasks; but one blocking call stalls the whole loop. - They combine:
asyncio.to_thread/run_in_executorpush blocking or CPU work off the loop; C extensions that release the GIL (NumPy, hashlib, compression) can make threads useful for CPU work. - Version note: the free-threaded build (experimental in 3.13, supported but optional in 3.14) makes threads a candidate for CPU-bound work too.
Clarifying questions expected:
- Is the work mostly waiting on the network or disk, or mostly computing?
- How many tasks at once β tens, or tens of thousands of connections?
- Are the libraries involved blocking (requests, most DB drivers) or async-native?
Code / implementation expected: Optional β a short sketch with ThreadPoolExecutor, ProcessPoolExecutor or asyncio.gather, or a decision table.