Skip to the solution
MediumDSA12 min read

When do you use threading, multiprocessing, or asyncio?

739 views

The problem

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 at await, cheap tasks; but one blocking call stalls the whole loop.
  • They combine: asyncio.to_thread / run_in_executor push 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.

  • concurrency

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

12 min read Β· 12 sections

Run the code

Edit the code and run it here. Each console line with an expected value becomes a test.

PythonA toy event loop with a fake clock: why awaits overlap and blocking calls hurt
PythonThe three tools on real CPython: threads, processes, asyncio and the bridges (not in the browser)
# The three tools side by side on real CPython.
# Not runnable in the browser: asyncio.run, threads and processes.
import asyncio
import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

def blocking_io(i):
    time.sleep(0.2)          # stands in for requests.get(), a DB driver call...
    return i

def cpu_heavy(n):
    return sum(i * i for i in range(n))

async def nice_io(i):
    await asyncio.sleep(0.2)  # stands in for an async HTTP or DB call
    return i

async def bad_io(i):
    time.sleep(0.2)          # BLOCKS the event loop thread
    return i


async def main():
    loop = asyncio.get_running_loop()

    async def gather(coro_fn):
        return await asyncio.gather(*(coro_fn(i) for i in range(5)))

    start = time.perf_counter()
    res = await gather(nice_io)
    print("asyncio, 5 awaits:", res, "overlapped:", time.perf_counter() - start < 0.5)

    start = time.perf_counter()
    res = await gather(bad_io)
    print("asyncio, 5 time.sleep calls:", res, "one after another:", time.perf_counter() - start >= 1.0)

    # Bridge: push the blocking call to a worker thread
    start = time.perf_counter()
    res = await asyncio.gather(*(asyncio.to_thread(blocking_io, i) for i in range(5)))
    print("asyncio + to_thread:", res, "overlapped:", time.perf_counter() - start < 0.5)

    # Bridge: push CPU work to a process pool
    with ProcessPoolExecutor(2) as pool:
        res = await asyncio.gather(*(loop.run_in_executor(pool, cpu_heavy, 10) for _ in range(2)))
    print("asyncio + process pool:", res)

if __name__ == "__main__":
    start = time.perf_counter()
    with ThreadPoolExecutor(5) as pool:
        res = list(pool.map(blocking_io, range(5)))
    print("threads, 5 blocking calls:", res, "overlapped:", time.perf_counter() - start < 0.5)
    with ProcessPoolExecutor(2) as pool:
        print("processes, CPU work:", list(pool.map(cpu_heavy, [10, 20])))
    asyncio.run(main())

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 68 of 139 in Python

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Python questions
Up nextMedium

What are dataclasses?

Continue