Skip to Content

Concurrency: Threading / Multiprocessing / asyncio

When to use

Do more than one thing at once. Pick by bottleneck: I/O-bound -> threading/asyncio; CPU-bound -> multiprocessing.

Analogy

Threading is a team of waiters; multiprocessing is more chefs in separate kitchens; asyncio is one super-fast waiter with many plates spinning.

Data-flow diagram

   threading       multiprocessing       asyncio
 1 proc, N          N procs, each        1 proc, 1 event
 OS threads;        owns its GIL;        loop; thousands
 good for I/O;       good for CPU;        of I/O awaits
 shared state       pickled IPC          (no CPU parallelism)
 gotchas: GIL       overhead             

Deep explanation

CPython has the GIL - only one thread runs Python bytecode at a time, so threading does NOT parallelize CPU-bound Python. Threading DOES help when threads spend time waiting on I/O (DNS, network, disk). Multiprocessing sidesteps the GIL by spawning separate interpreters; state crosses via pickle. asyncio: a single thread runs an event loop; await pauses; thousands of tasks fit in one thread.

Examples

Example 1

from concurrent.futures import ThreadPoolExecutor
import urllib.request
def fetch(u): return urllib.request.urlopen(u).read()
with ThreadPoolExecutor(max_workers=8) as ex:
    pages = list(ex.map(fetch, ['https://a','https://b']))

ThreadPoolExecutor fans out I/O; concurrent fetches finish in max(latencies).

Example 2

import asyncio, aiohttp
async def main():
    async with aiohttp.ClientSession() as s:
        async def fetch(u):
            r = await s.get(u); return await r.text()
        return await asyncio.gather(*(fetch(u) for u in urls))
asyncio.run(main())

asyncio.gather schedules all coroutines on one event loop; thousands of HTTPS.

Example 3

from concurrent.futures import ProcessPoolExecutor
import math
with ProcessPoolExecutor() as ex:
    results = list(ex.map(math.factorial, [100000]*8))

ProcessPoolExecutor gets real CPU parallelism (each process has its own GIL).

Common mistake

Using threading for CPU work (it does not help due to GIL). Shared mutable state without sync. Calling blocking I/O (requests.get) inside an async coroutine - freezes the event loop.

Key takeaway

I/O-bound -> ThreadPoolExecutor or asyncio/aiohttp. CPU-bound -> ProcessPoolExecutor. Never call blocking code from async; use loop.run_in_executor if you must.

Production Failure Playbook

Failure scenario 1: blocking-driver-in-asyncio

Failure scenario 2: thread-race-on-shared-dict