Skillversitas

Choosing the Right Concurrency Model

Python gives you three overlapping concurrency models, and the wrong choice costs you either speed (threads on CPU work) or complexity (an event loop for two background jobs). This chapter is the decision guide: classify the workload, match it to a model, and know the hybrid patterns when a program needs more than one.

The Decision Tree

  1. Is the work CPU-bound? (tight Python loops, heavy arithmetic, parsing). Yes -> multiprocessing / ProcessPoolExecutor. Threads will not help because of the GIL.
  2. Is the work I/O-bound with modest concurrency? (a few parallel downloads, background refresh, UI responsiveness). Yes -> ThreadPoolExecutor. Simple, robust, enough up to hundreds of tasks.
  3. Is the work I/O-bound with high concurrency? (thousands of open connections, a web server, a scraper with many pages). Yes -> asyncio. Memory per task is tiny and the loop scales far beyond a thread pool.

Model Comparison

ThreadsProcessesasyncio
ParallelismI/O only (GIL)True CPU parallelismSingle-thread, many tasks
Unit cost~1 MB stack, cheap~10s MB, expensive~KB per task
SharingDirect memoryPickling / shared memoryDirect memory, one thread
Failure blast radiusWhole processSingle processHandled per task
DebuggingUsual toolsHarder (separate interpreters)Async-aware tools needed

Hybrid Patterns

Real systems mix models. The standard combinations:

  • Async I/O + thread pool for sync libraries: an aiohttp app that must call a synchronous database driver offloads it with await asyncio.to_thread(sync_query, ...) so the loop is never blocked.
  • Processes + queues for CPU pipeline: a reader process fetches, a Queue feeds ProcessPoolExecutor workers that crunch, and a writer process persists results.
  • Threads for the shell, asyncio inside: a CLI keeps its UI on the main thread and runs asyncio.run for the network phase.

Anti-Decisions

  • Do not reach for asyncio because it looks modern - if you have five tasks and a synchronous library, a thread pool is less code and easier to debug.
  • Do not use processes to "escape" a slow C library that already releases the GIL - threads are cheaper and work fine.
  • Do not mix blocking and async code in the same loop without to_thread; one time.sleep in a handler can idle your whole server.

Summary

CPU-bound -> processes; I/O-bound with modest fan-out -> thread pools; I/O-bound at scale -> asyncio. Most production code is a hybrid, bridged with to_thread and queues. Pick the simplest model that fits the bottleneck, and let the benchmark settle disagreements.