Do more than one thing at once. Pick by bottleneck: I/O-bound -> threading/asyncio; CPU-bound -> multiprocessing.
Threading is a team of waiters; multiprocessing is more chefs in separate kitchens; asyncio is one super-fast waiter with many plates spinning.
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
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.
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).
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.
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).
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.
I/O-bound -> ThreadPoolExecutor or asyncio/aiohttp. CPU-bound -> ProcessPoolExecutor. Never call blocking code from async; use loop.run_in_executor if you must.
async def handler() still called requests.get(...) (blocking sync) -> froze event loop.loop.run_in_executor; better, migrate to aiohttp/httpx async.if k not in d: d[k] = 0; d[k] += 1; read-then-write race.