Warming up the neural circuits...
By the end of this chapter you will:
Most performance discussions around Python fail because they ask the wrong first question.
The right question is: "Are we waiting on I/O or burning CPU?"
Once workload type is clear, choosing threads, asyncio, processes, or plain sync becomes straightforward.
In standard CPython, the Global Interpreter Lock allows only one thread to execute Python bytecode at a time within a process.
Important nuance:
from concurrent.futures import ThreadPoolExecutor
import time
def fetch(i: int) -> str:
time.sleep(0.2)
return f"ok-{i}"
with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(fetch, range(20)))
print(results[:3])Great for HTTP requests with blocking clients, file I/O, and SDK calls.
from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(n: int) -> int:
total = 0
for i in range(n):
total += i * i
return total
with ProcessPoolExecutor() as
Processes avoid the GIL but introduce inter-process communication overhead.
import threading
counter = 0
lock = threading.Lock()
def inc() -> None:
global counter
for _ in range(10_000):
with lock:
counter += 1Without synchronization, shared updates can be lost.
Free-threading changes are evolving and ecosystem readiness varies.
Practical stance today:
I/O-bound -> threads or . CPU-bound -> processes (or native acceleration). This single rule prevents most concurrency misdesigns.
| Workload | Best first choice |
|---|---|
| Blocking network/file I/O with sync libraries | ThreadPoolExecutor |
| CPU-heavy pure Python loops | ProcessPoolExecutor / multiprocessing |
| High-scale async-native I/O | asyncio |
| Small low-latency script | synchronous code |
| Mistake | Symptom | Fix |
|---|---|---|
| Using threads for CPU-bound loops and expecting linear speedup | Little gain | Use processes or native acceleration |
| Sharing mutable globals across threads unsafely | Flaky nondeterministic bugs | Use locks/queues or immutable message passing |
| Using too many worker threads/processes | Thrashing and overhead | Benchmark and cap worker counts |
| Large objects passed repeatedly to processes | IPC bottleneck | Minimize payloads and batch work |
| Ignoring startup method differences across OS | Platform-specific failures | Test process code on target OS and guard entry point |
sleep-based taskFaster than sequential executionShared global counterDeterministic final countList of large N valuesParallel execution across coresThree different workloadsRational model choice per workloadCounter updates without lockRoot cause, fix, and prevention checklistBeginner:
"Why can threads still help Python programs despite the GIL?"
They improve throughput for I/O-bound workloads because threads can progress while others wait on network/disk.
"When do you choose multiprocessing?"
For CPU-bound tasks needing true parallel execution across cores.
Senior:
"How do you evaluate thread vs process pool in production?"
Profile workload type, benchmark throughput/latency, include memory and overhead in measurement.
"What are common failure modes in concurrent Python systems?"
Race conditions, deadlocks, starvation, unbounded queues, and hidden serialization bottlenecks.
I/O-bound? -> threads or async
CPU-bound? -> processes
Shared state? -> lock or redesign with message passing
Need confidence? -> benchmark, do not assumeFor which workload do threads usually help most in CPython?