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.
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.
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.
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.