Concurrency: Traps, Failure Modes & Interview Q&A
Master concurrency failure modes: the GIL Convoy Effect, event loop starvation, fork deadlocks, concurrency selection matrices, and senior interview flashcards.
Building scalable concurrent systems requires anticipating subtle failure modes across operating system thread schedulers, process boundaries, and asynchronous event loops.
This lesson explores edge cases like the GIL Convoy Effect, provides a definitive concurrency selection matrix, and walks through high-frequency senior technical interview flashcards.
Introduced in Python 3.11 (PEP 654) to eliminate orphaned asynchronous background tasks.
1. Advanced Concurrency Failure Modes#
Trap 1: The GIL Convoy Effect#
On multi-core architectures, running a mix of CPU-bound threads and I/O-bound threads can trigger the GIL Convoy Effect:
When an I/O thread finishes waiting for a packet and requests the GIL, the CPU thread is often already in a tight loop attempting to re-acquire the lock. The resulting contention forces operating system thread context switches, cache line invalidations, and introduces severe latency spikes for network requests.
Trap 2: Event Loop Starvation#
Because asyncio is single-threaded, any synchronous operation running for more than a few milliseconds starves all other tasks on the loop:
1234567891011121314151617import asyncio import time # Antipattern: Synchronous blocking call inside async function! async def handle_request(request_id: int): print(f"Handling request {request_id}") # BUG: time.sleep() freezes the ENTIRE event loop! # No other request can be received or processed for 2 seconds! time.sleep(2.0) return "Done" # Idiomatic Solution: Async non-blocking await async def handle_request_clean(request_id: int): print(f"Handling request {request_id}") # Non-blocking: yields control back to the loop! await asyncio.sleep(2.0) return "Done"
Trap 3: Modern Structured Concurrency via TaskGroup (Python 3.11+)#
In earlier versions of Python, managing multiple concurrent tasks using asyncio.gather() often leaked background tasks if one task raised an unhandled exception.
PEP 654 introduced asyncio.TaskGroup, which guarantees Structured Concurrency: if any task inside the group fails, all remaining child tasks are cancelled automatically:
123456789101112131415161718192021import asyncio async def fetch_profile(user_id: int): await asyncio.sleep(0.1) return {"id": user_id, "name": "Alice"} async def fetch_orders(user_id: int): await asyncio.sleep(0.15) return [{"order_id": 101, "total": 49.99}] async def main(): # Exception-safe structured concurrency block async with asyncio.TaskGroup() as tg: t1 = tg.create_task(fetch_profile(1)) t2 = tg.create_task(fetch_orders(1)) # Execution reaches here ONLY when all tasks finish cleanly! print(f"Profile: {t1.result()}") print(f"Orders : {t2.result()}") asyncio.run(main())
2. "When to Use What" Concurrency Decision Matrix#
| Workload Characteristic | Optimal Paradigm | Scaling Mechanism | Key Architectural Trade-off |
|---|---|---|---|
| High-concurrency network I/O (10k+ WebSockets) | asyncio Event Loop | Single thread reactor (epoll/kqueue) | Ultra-low RAM footprint; requires non-blocking asynchronous libraries. |
| CPU-bound heavy computation (ML, Video Encoding) | multiprocessing / ProcessPool | Multiple OS processes | Bypasses GIL; high memory footprint and IPC serialization cost. |
| Parallel processing of large numeric arrays | multiprocessing.shared_memory | Shared RAM block | Bypasses GIL with zero-copy access; requires manual buffer coordination. |
| Legacy synchronous blocking I/O | concurrent.futures.ThreadPoolExecutor | Thread pool worker threads | Compatible with standard libraries; limited to single CPU core for bytecode. |
3. Senior Technical Interview Flashcards#
4. Concept Verification#
Test your understanding of concurrency trade-offs:
What causes the GIL Convoy Effect on multi-core systems?
What is the key guarantee provided by asyncio.TaskGroup (introduced in Python 3.11) over asyncio.gather()?