Concurrency Highlights Summary
Every highlight bullet from the 10 pages in this section, gathered on one page and grouped by the page it came from.
- Every concurrency tool in Python picks a point on one spectrum: how much memory is shared, and what protects it
- The GIL is a single lock over one interpreter's shared memory - it's a safety mechanism, not a performance feature
- Multiprocessing trades shared memory for true parallelism by giving each worker its own isolated interpreter
- Subinterpreters and free-threaded builds are two different ways of relaxing that one shared lock without giving up a single process
- Use threads for I/O-bound tasks and processes for CPU work to avoid GIL limits
- Concurrent increments on shared counters race without locks, yielding wrong results
- asyncio efficiently handles many concurrent I/O operations on a single thread
- Queue provides thread-safe coordination for producer/consumer task pipelines
- Protect shared mutable state with threading.Lock to prevent race conditions
- concurrent.futures offers high-level pool APIs for spawning thread/process work
- CPU-bound threads hit GIL bottleneck; use ProcessPoolExecutor instead
- Free-threaded Python 3.13+ removes GIL but requires extension testing
- I/O operations release GIL so network threads still scale efficiently
- Asyncio cooperatively schedules one thread; does not parallel CPU work
- Single-thread performance regresses without GIL; benchmark both builds
- Check if Python is free-threaded using sys._is_gil_enabled on 3.13+
- Use threads for I/O-bound overlap, not CPU work because GIL limits parallel bytecode
- Guard shared mutable state with Lock, RLock, Event, or Condition to prevent races
- Avoid deadlock by always acquiring locks in the same consistent order
- Pass results between threads via Queue without manual locks on shared lists
- Non-daemon threads finish before process exits; use daemon threads for background tasks
- Size thread pool near concurrent I/O waits, not CPU core count, to minimize context switches
- Bypass Python's GIL for CPU-bound work by using separate processes instead of threads
- Use queues, pipes, or shared memory to pass data between worker processes
- Guard process spawn with if name == 'main' to prevent recursive spawning
- Choose spawn start method on macOS and Windows to avoid fork-related threading deadlocks
- Size process pools by os.cpu_count(), lowering if tasks require large memory allocations
- Ensure worker target functions are picklable; use module-level functions, not closures
- ThreadPoolExecutor overlaps I/O, ProcessPoolExecutor parallelizes CPU
- Call result() with timeout to avoid blocking shutdown
- Worker exceptions stored on Future, raised at result()
- Batch small tasks to avoid process spawn overhead
- Thread pool GIL limits CPU speedup - use ProcessPoolExecutor
- map keeps order like input, as_completed yields on task finish
- Bounded queue blocks producers when full, smoothing bursts and backpressure
- Forgotten task_done hangs join forever; wrap dequeue in try/finally
- One STOP sentinel per consumer; multiple sentinels silently leave workers running
- Select queue type by concurrency model: threads, processes, or async coroutines
- Unbounded queues cause memory exhaustion when producers outrun consumers
- Pickle large objects slowly over multiprocessing queues; use shared memory or file paths
- Subinterpreters split CPU work across isolated interpreters with lower overhead than multiprocessing
- Subinterpreters remain experimental in 3.14; default to multiprocessing for production CPU isolation
- Many C extensions assume one interpreter and won't work reliably with subinterpreters
- Gate experimental APIs behind version checks; API surface is unstable across Python minors
- Use for plugin sandboxes, CPU parallel tasks needing shared process memory, free-threaded futures
- Threads have lowest overhead, processes highest isolation, subinterpreters split the difference
- Profile your workload with py-spy or cProfile before choosing a model
- More than 60% I/O wait time with async libraries means pick asyncio
- Thread pools won't speed up pure Python CPU work due to the GIL
- Split mixed workloads: asyncio for I/O, process pool for CPU tasks
- If you have native async clients, asyncio is strongly preferred
- Measure p50 and p99 latencies under realistic load to validate choice
- Profile before picking asyncio vs threads vs processes
- Keep CPU work off the asyncio event loop thread
- Size thread pools by I/O concurrency, not by CPU count
- Prefer queues over shared mutable structures for clarity
- Acquire locks in fixed global order to prevent deadlocks
- Call task_done for every queue.get or join hangs
Reviewed by Chris St. John·Last updated Jul 31, 2026