Concurrency Patterns
asyncio.gather vs create_task, semaphores, mixing blocking code, and asyncio vs threading vs multiprocessing
Free tier: read the explanation here. Upgrade to Pro for Drills, Speed challenges & Mastery badges.
UpgradeKnowledge Debt detected
You can study this freely — but your score may plateau if these foundations have gaps. The Mastery badge requires them to be solid.
Explanation
Real interviews probe concurrency beyond basic async/await: how to limit concurrency, mix blocking calls with async code, and choose between asyncio/threading/multiprocessing.
asyncio.gather vs asyncio.create_task:
python import asyncio async def main(): # gather: schedule + wait for all, results in order results = await asyncio.gather(fetch(1), fetch(2), fetch(3)) # create_task: schedule now, await later — useful when you need # to do other work between starting and finishing task1 = asyncio.create_task(fetch(1)) task2 = asyncio.create_task(fetch(2)) result1 = await task1 result2 = await task2
Limiting concurrency with a Semaphore:
python import asyncio async def fetch_limited(sem: asyncio.Semaphore, url: str): async with sem: return await fetch(url) async def main(urls): sem = asyncio.Semaphore(5) # max 5 concurrent return await asyncio.gather(*(fetch_limited(sem, u) for u in urls)) Without a semaphore, gather()` over hundreds of URLs fires them all at once — likely to hit rate limits or overwhelm the target server.
Mixing blocking code with async — asyncio.to_thread:
python import asyncio def blocking_db_call(): return slow_sync_library.query() async def main(): result = await asyncio.to_thread(blocking_db_call) Calling a blocking function directly inside async def freezes the entire event loop — nothing else can run until it returns. asyncio.to_thread` (3.9+) runs it in a thread pool instead.
asyncio vs threading vs multiprocessing:
- asyncio — many I/O-bound tasks, single thread, cooperative — best for thousands of concurrent network calls
- threading — I/O-bound work with blocking libraries that don't support async — the GIL still limits CPU parallelism
- multiprocessing — CPU-bound work — separate processes bypass the GIL entirely
Timeouts:
python try: result = await asyncio.wait_for(fetch(url), timeout=5.0) except asyncio.TimeoutError: result = None
return_exceptions in gather:
python results = await asyncio.gather(*tasks, return_exceptions=True) # results may contain Exception objects instead of raising immediately Without return_exceptions=True, the first exception propagates immediately from gather()`.
Examples
Rate-limited concurrent fetches
Semaphore caps concurrency at 10, and return_exceptions=True keeps one failed request from crashing the whole batch
import asyncio
import aiohttp
async def fetch(session, sem, url):
async with sem: # only N requests in flight at once
async with session.get(url) as response:
return await response.json()
async def fetch_all(urls, max_concurrent=10):
sem = asyncio.Semaphore(max_concurrent)
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, sem, url) for url in urls]
return await asyncio.gather(*tasks, return_exceptions=True)
urls = [f"https://api.example.com/items/{i}" for i in range(200)]
results = asyncio.run(fetch_all(urls))Calling a blocking library from an async function
asyncio.to_thread offloads a blocking call so it doesn't freeze the event loop — common when wrapping legacy sync code in FastAPI
import asyncio
def slow_sync_report(report_id: int) -> dict:
# e.g. a sync ORM call or PDF library with no async support
time.sleep(2)
return {"id": report_id, "status": "done"}
async def get_report(report_id: int) -> dict:
# Runs slow_sync_report in a thread pool —
# the event loop keeps serving other requests
return await asyncio.to_thread(slow_sync_report, report_id)How well did you understand this?