4. Concurrency & Parallelism
Choose the right concurrency model, keep event loops responsive, and coordinate tasks safely under failures.
Q1 What is the Python Global Interpreter Lock (GIL), and how can you work around its limitations?
Answer: The Global Interpreter Lock (GIL) is a mutex in CPython that allows only one thread to execute Python bytecode at a time within a single process. This means multi-threaded Python code does not achieve true parallelism for CPU-bound tasks. To work around its limitations for CPU-bound work, you must use multiprocessing, C extensions, or vectorized libraries like NumPy.
Explanation: For I/O-bound tasks (e.g., network requests), the GIL is released by the waiting thread, allowing other threads to run, making threading effective. For CPU-bound tasks, the multiprocessing module is the correct choice, as it sidesteps the GIL by giving each process its own interpreter and memory.
Q2 Compare and contrast threading, multiprocessing, and asyncio.
Answer: threading is for I/O-bound tasks where you manage a small number of operations that spend time waiting. multiprocessing is for CPU-bound tasks where you need to perform parallel computations on multi-core hardware. asyncio is for high-throughput I/O-bound tasks in a single thread, using an event loop and cooperative multitasking.
Explanation: asyncio is the most efficient solution for network I/O, as it can handle thousands of concurrent connections with minimal overhead. However, any blocking, CPU-intensive code will block the entire event loop. Such blocking calls must be offloaded to a separate thread (using asyncio.to_thread) or a process pool to avoid stalling the application.
import asyncio, aiohttp
async def fetch(session, url):
async with session.get(url, timeout=5) as r:
r.raise_for_status()
return await r.text()
async def main(urls):
async with aiohttp.ClientSession() as s:
tasks = [fetch(s, u) for u in urls]
return await asyncio.gather(*tasks, return_exceptions=True)
# asyncio.run(main(["https://example.com"]))
Q3 How do you handle backpressure in an asyncio application?
Answer: Backpressure is handled by limiting the number of concurrent tasks to prevent a system from being overwhelmed. This is typically done using synchronization primitives like asyncio.Semaphore or by using bounded queues.
Explanation: A semaphore initialized with a specific count will only allow that many tasks to acquire it and run concurrently. Any additional tasks will wait until a spot is released. This is a crucial pattern for controlling access to limited resources, like database connections or downstream API rate limits.
sem = asyncio.Semaphore(50)
async def limited_task():
async with sem:
... # This code will only be run by 50 tasks concurrently
Q4 How do you implement timeouts and cancellation correctly in asyncio?
Answer: Wrap awaited operations with asyncio.wait_for or context-specific timeouts and handle asyncio.TimeoutError. Propagate CancelledError and use try/finally for cleanup.
Explanation: Always make cancellation points safe by releasing resources.
async def fetch_with_timeout(session, url, timeout=3):
try:
return await asyncio.wait_for(session.get(url), timeout)
except asyncio.TimeoutError:
return None
Q5 What is structured concurrency (TaskGroup) in Python 3.11?
Answer: asyncio.TaskGroup scopes tasks so they start, fail, and cancel together, simplifying error handling.
Explanation: If one task fails, the group cancels the rest and raises an aggregated error.
async def gather_all(urls):
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch(u)) for u in urls]
return [t.result() for t in tasks]
Q6 How do you avoid blocking the event loop with CPU work?
Answer: Offload to threads/processes using asyncio.to_thread or a process pool.
Explanation: Blocking the loop stalls all I/O and timers.
result = await asyncio.to_thread(cpu_heavy_fn, data)
Q7 What is the status of free-threaded CPython (PEP 703) and how does it change guidance?
Answer: As of 2025, a free-threaded build is experimental and opt-in; production CPython still has the GIL. For CPU-bound parallelism, processes remain the safe default.
Explanation: Expect gradual ecosystem adoption; verify library support before relying on free-threading.