Python Concurrency
Choose between threads, processes, and async for CPU- and I/O-bound work
Overview
Python offers three concurrency models: threads (threading), processes (multiprocessing), and cooperative coroutines (asyncio). The Global Interpreter Lock (GIL) means threads do not run Python bytecode in parallel, so threads help I/O-bound work while processes are needed for CPU-bound parallelism. Choosing the right model is the single most important concurrency decision.
Syntax / Usage
concurrent.futures gives one unified API over both thread and process pools.
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def io_task(url: str) -> int:
# Simulated network fetch (I/O-bound) -> use threads
return len(url)
def cpu_task(n: int) -> int:
# Heavy computation (CPU-bound) -> use processes
return sum(i * i for i in range(n))
with ThreadPoolExecutor(max_workers=4) as pool:
print(list(pool.map(io_task, ["a", "bb", "ccc"])))
if __name__ == "__main__":
with ProcessPoolExecutor() as pool:
print(list(pool.map(cpu_task, [10_000, 20_000])))
Process pools sidestep the GIL by running separate interpreters, at the cost of pickling arguments and results.
Examples
Guard shared mutable state with a Lock to avoid race conditions between threads:
import threading
counter = 0
lock = threading.Lock()
def increment() -> None:
global counter
for _ in range(100_000):
with lock: # atomic critical section
counter += 1
threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # 400000 — correct because of the lock
Submit work and collect results as they finish with as_completed:
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def work(n: int) -> int:
time.sleep(0.1)
return n * 2
with ThreadPoolExecutor(max_workers=3) as pool:
futures = [pool.submit(work, i) for i in range(5)]
for fut in as_completed(futures):
print(fut.result())
Common Mistakes
- Using threads for CPU-bound work and seeing no speedup because of the GIL
- Forgetting the
if __name__ == "__main__":guard, which breaksmultiprocessingon Windows/macOS spawn - Sharing mutable state across threads without a lock, causing intermittent race conditions
- Assuming
multiprocessingshares memory; arguments and results are pickled and copied - Creating far more threads than needed instead of bounding a pool, exhausting resources
See Also
python-async-await python-iterators-protocol python-context-managers