Asyncio and cooperative concurrency
Await nonblocking work and bound concurrent tasks
Coroutines and tasks
async def defines a coroutine function. Calling it creates a coroutine object; awaiting or scheduling it runs the body. At a suspending await, other tasks on the event loop can progress.
Awaiting operations one after another is sequential. Schedule independent tasks before collecting their results to allow overlap:
import asyncio
async def label(value):
await asyncio.sleep(0)
return f"item-{value}"
async def main():
tasks = [asyncio.create_task(label(n)) for n in (2, 1)]
print(await asyncio.gather(*tasks)) # ['item-2', 'item-1']
if __name__ == "__main__":
asyncio.run(main())gather preserves input order, regardless of completion order. sleep(0) yields control; it is not a network-performance model. Use asyncio.run in a standalone script; inside an existing notebook or browser event loop, use await main() instead.
Capacity
Hold a semaphore around the actual awaited operation with async with semaphore:. Protecting only task creation does not bound active operations.
A semaphore limits active work, not the number or memory of waiting tasks. Finite lists may use one task per item; large streams need bounded submission or queue-consuming workers. Keep coordination objects within their intended event loop's lifetime.
Failure and cancellation
Own task references and observe their results. Cancellation is cooperative, usually delivered at a suspension point; cleanup should normally propagate CancelledError:
import asyncio
async def worker(started, events):
try:
started.set()
await asyncio.Event().wait()
finally:
events.append("released")
async def main():
started, events = asyncio.Event(), []
task = asyncio.create_task(worker(started, events))
await started.wait()
task.cancel()
try:
await task
except asyncio.CancelledError:
print(events, task.cancelled()) # ['released'] True
if __name__ == "__main__":
asyncio.run(main())Requesting cancellation is not proof of termination: await the task so cleanup completes. Suppressing cancellation can break timeout and task-group behavior. Cancelling an await on offloaded blocking work does not forcibly stop its thread.
Default gather can propagate one failure while siblings continue. Python 3.11+ TaskGroup cancels remaining siblings on ordinary child failure, waits for cleanup, and can report an exception group.
Gotchas
async defdoes not make CPU work parallel. Blocking calls, includingtime.sleep(), block the event loop.- An
awaitcan complete immediately without yielding. A suspending await can expose shared state to other tasks; protect invariants spanning it. - Unowned background tasks can outlive their caller or leave exceptions unobserved. Use explicit completion and cancellation policies.
Use synchronous code when there is no independent waiting to overlap.
Put it into practice
Implement the exercise in a full-screen workspace, then run its tests.