4. Concurrency & Parallelism
Choose the right concurrency model, keep event loops responsive, and coordinate tasks safely under failures.
Question: 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.
Question: Compare and contrast
threading
,multiprocessing
, andasyncio
.
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"]))
Question: 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
Question: 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
Question: 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]
Question: 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)
Question: 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.