CodeRigor

FREE LESSON

Bounded async work & cancellation

Concurrent fake requests with bounded in-flight work, ordered results, explicit errors and cancellation propagation.

async def defines a coroutine function; calling it creates a coroutine object. await suspends the current coroutine until an awaitable completes. This allows other tasks to run while waiting for I/O, but it does not automatically make CPU-heavy Python computation parallel. Use bounded concurrency for an adapter that can spend time waiting.

A single semaphore must be shared by every worker that contributes to the concurrency limit.

Two permits, many queued requests
The semaphore bounds in-flight awaits, not the total number of tasks created by gather. Results are assembled in input order.
Two permits, many queued requests
The semaphore bounds in-flight awaits, not the total number of tasks created by gather. Results are assembled in input order.
Python
import asyncio


async def double(x):
    await asyncio.sleep(0)
    return x * 2


async def main():
    print(await asyncio.gather(double(3), double(1)))


asyncio.run(main())

gather returns results in the input order even if completion order differs. A semaphore guards an in-flight section: share one semaphore across workers, and hold its permit through the awaited request. Creating a semaphore per request does not impose a shared limit.

Python
import asyncio


async def main():
    semaphore = asyncio.Semaphore(2)

    async def worker(x):
        async with semaphore:
            await asyncio.sleep(0)
            return x

    print(await asyncio.gather(*(worker(x) for x in range(4))))


asyncio.run(main())

Ordinary request failures should become explicit result records when the evaluation policy needs to continue. Cancellation is control flow: propagate CancelledError so a parent can stop the run. Do not catch BaseException or turn a cancelled experiment into an ordinary failed example. Cancellation of the gather await propagates to its unfinished children.

Arrival order is not result order, and an exception outcome must not remove the original item from the result list.

Ordered outcomes retain failures
For input [2,1,0], return results in that order even if calls finish differently: value 4, ValueError outcome, value 0.
Ordered outcomes retain failures
For input [2,1,0], return results in that order even if calls finish differently: value 4, ValueError outcome, value 0.

The build uses fake workers only, no requests or real sleeps. It returns one ordered outcome per input, with ordinary exception types recorded explicitly. This implementation bounds in-flight work but still creates one task per input; a very large or infinite source needs a bounded queue/worker pool too.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Łukasz Langa’s Thinking in Coroutines explains cooperative scheduling. This older talk uses historical APIs; use the lesson’s current asyncio.run, shared semaphore and cancellation patterns for the build.