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, 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"]))

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.