The GIL makes Python concurrency confusing. IO-bound and CPU-bound workloads demand different models - pick wrong and performance worsens.
Three Models and Code
ThreadPool submits blocking calls; GIL releases on IO waits (10-50 workers). ProcessPool uses subprocesses to evade GIL for CPU work (serialization overhead). asyncio schedules coroutines on one thread for massive light IO concurrency.
import asyncio, time, requests, httpx
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
from typing import List
URLS = [f"https://jsonplaceholder.typicode.com/posts/{i}" for i in range(1, 51)]
def fetch_sync(url: str) -> dict:
return requests.get(url, timeout=10).json()
async def fetch_async(client, url: str) -> dict:
r = await client.get(url, timeout=10.0)
return r.json()
def run_threadpool(urls, maxw=20):
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=maxw) as ex:
fs = [ex.submit(fetch_sync, u) for u in urls]
results = [f.result() for f in as_completed(fs)]
print(f"[ThreadPool] {len(results)} tasks, {time.perf_counter()-t0:.2f}s")
def cpu_heavy(n):
sieve = [True]*(n+1); sieve[0]=sieve[1]=False
for i in range(2, int(n**0.5)+1):
if sieve[i]: sieve[i*i::i] = [False]*len(sieve[i*i::i])
return sum(sieve)
def run_processpool(args):
t0 = time.perf_counter()
with ProcessPoolExecutor() as ex:
results = list(ex.map(cpu_heavy, args))
print(f"[ProcessPool] sum={sum(results)}, {time.perf_counter()-t0:.2f}s")
async def run_asyncio(urls):
t0 = time.perf_counter()
async with httpx.AsyncClient() as client:
tasks = [fetch_async(client, u) for u in urls]
results = await asyncio.gather(*tasks)
print(f"[asyncio] {len(results)} tasks, {time.perf_counter()-t0:.2f}s")
if __name__ == "__main__":
run_threadpool(URLS)
run_processpool([100000]*8)
asyncio.run(run_asyncio(URLS))
Decision Framework and Benchmarks
Pure CPU math → ProcessPool. Network/file IO → asyncio or ThreadPool. Mixed → asyncio.run_in_executor offloads CPU to pool while IO stays in coroutines.
| Dimension | ThreadPool | ProcessPool | asyncio |
|---|---|---|---|
| Principle | Multi-thread shared GIL | Multi-process, own GIL | Single-thread loop + coroutines |
| GIL impact | Released on IO / useless on CPU | No GIL at all | No contention |
| 50 HTTP reqs | ~5-8s | ~40s (fork slow) | ~1.5-3s |
| 8 CPU primes | ~45s (serial) | ~7s (multi-core) | ~40s (single) |
| Max concurrency | Hundreds | Cores × 2 | Thousands+ |
| Migration cost | Low | Medium (pickle) | High (full async chain) |
Best Practices
Prefer asyncio for IO first; fall to ThreadPool if dependencies synchronous. Use ProcessPool for CPU tasks and cap workers at 1-2 × core count.