← Back to Additional Topics

asyncio vs threading vs multiprocessing

Additional TopicsMidconcurrencypython

The Question

What is the difference between asyncio, threading, and multiprocessing in Python?

What a Strong Answer Covers

  • All three with correct use case. GIL impact on threading. "Cooperative vs preemptive.

Senior-Level Answer

Python offers three concurrency models, each suited to a different problem class. Choosing wrong is a common source of performance bugs.

**asyncio** is cooperative, single-threaded concurrency built on an event loop and coroutines. A coroutine yields control at `await` expressions, allowing the event loop to run other coroutines while it waits. Because everything runs on one thread, there is no lock contention or thread-safety concern for pure Python data structures. asyncio is ideal for I/O-bound workloads with many concurrent operations—HTTP clients, database drivers, WebSocket servers. The concurrency limit scales to thousands of coroutines with minimal overhead. The constraint: all code in the event loop must be non-blocking. A single blocking call (e.g., `time.sleep`, a synchronous file read) freezes the entire event loop. Use `asyncio.to_thread()` to offload blocking calls.

**threading** creates OS-level threads managed by the Python runtime. Threads share memory space, making data sharing easy but requiring locks for correctness. Python's GIL (Global Interpreter Lock) allows only one thread to execute Python bytecode at a time, so threading does not achieve CPU parallelism for Python code. Threads release the GIL during I/O and when calling C extensions, so threading does give concurrency benefits for I/O-bound workloads. It's simpler to retrofit into existing synchronous code than asyncio (just wrap blocking calls in threads). Overhead: each thread has an OS-level stack (~1–8 MB), so thousands of threads are impractical.

**multiprocessing** spawns separate OS processes, each with its own Python interpreter and GIL. This achieves true CPU parallelism—work is distributed across all available cores. The cost: inter-process communication (IPC) requires serialization (pickle), making it expensive to share large data. Process startup is heavy (~100ms). It's the correct tool for CPU-bound tasks: image processing, data transformation, ML preprocessing. `ProcessPoolExecutor` from `concurrent.futures` provides a clean high-level API.

Decision heuristic: I/O-bound with async-compatible libraries → asyncio. I/O-bound with legacy synchronous code → threading. CPU-bound → multiprocessing. For mixed workloads, combine: use asyncio for I/O and `loop.run_in_executor()` with a `ProcessPoolExecutor` for CPU-intensive tasks.

Key Differences

Aspectasynciothreadingmultiprocessing
Execution modelSingle thread, event loopOS threads, shared memorySeparate processes
GIL impactN/A (one thread)Limits CPU parallelismBypasses GIL completely
I/O-bound performanceExcellentGoodOverhead per process
CPU-bound performancePoor (blocks event loop)Poor (GIL)Excellent
Memory overheadVery low (coroutines)~1-8 MB per threadHigh (full process copy)
Data sharingShared (same thread)Shared (needs locks)Requires IPC/pickle
Best use caseHigh-concurrency async I/OLegacy sync I/O workloadsCPU-intensive computation

What Separates a 2/3 from a 3/3

2/3 — Passing but Incomplete

Correctly identifies asyncio as event-loop/coroutine-based, explains GIL limitation on threading, states multiprocessing bypasses GIL for CPU parallelism, gives an appropriate use case for each.

3/3 — Strong Answer

Explains cooperative vs. preemptive scheduling, quantifies overhead differences, explains asyncio.to_thread() for blocking calls in event loops, describes run_in_executor() for mixing models, and gives a nuanced recommendation for mixed workloads.

Common Mistakes

  • Claiming threading achieves CPU parallelism in Python — the GIL prevents this for Python bytecode
  • Not knowing asyncio.to_thread() exists for running blocking code in the event loop
  • Saying multiprocessing is always better — the IPC serialization overhead makes it wrong for I/O-bound work
  • Forgetting that asyncio requires all called code to be async-compatible — you can't just add async to existing sync code

Follow-Up Questions

  • You have a FastAPI server that needs to call a CPU-intensive function. How do you avoid blocking the event loop? — Use loop.run_in_executor() with a ProcessPoolExecutor to offload the CPU work to a separate process. This keeps the event loop free to handle other requests.
  • Why doesn't releasing the GIL during I/O make threading equivalent to asyncio for high-concurrency I/O? — Thread overhead: each thread needs an OS stack (1-8 MB), scheduling is preemptive with context-switch cost, and locks are needed for shared state. asyncio coroutines are much lighter and have no preemptive scheduling overhead.
  • What is the difference between concurrent.futures.ThreadPoolExecutor and ProcessPoolExecutor? — ThreadPoolExecutor uses OS threads — suitable for I/O-bound tasks. ProcessPoolExecutor uses separate processes — suitable for CPU-bound tasks. Both provide the same Future-based interface.
  • When would you use asyncio.gather() vs. asyncio.create_task()? — Both run coroutines concurrently. gather() awaits all at once and returns results in order — use for a fixed set of tasks you need results from. create_task() schedules immediately and returns a Task handle — use when you need independent lifecycle control or fire-and-forget semantics.

Related Questions

  • CAP theorem
  • SQL vs NoSQL
  • Big O basics
  • N+1 problem
  • AI tools in workflow

Can You Explain This Cold?

Reading the answer is step one. Explaining it unprompted — under interview pressure — is what actually matters. Get AI-graded feedback on your answer with follow-up probes on your weak points.

Get Graded — Free Assessment
GrindQuestionsAITechnical interview assessment
TermsPrivacyAbout