Home/Python/Asyncio and cooperative concurrency

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:

Python
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:

Python
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 def does not make CPU work parallel. Blocking calls, including time.sleep(), block the event loop.
  • An await can 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.

Open practice
Next lessonMemory and garbage collection