A task boundary is a decision. Each boundary costs bookkeeping, and too many boundaries hide the failure inside a task nobody waits on.
What you will see
caught
About 2 min of reading
Quick check
Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.
Try It
Check what a boundary does.
1
Sum gathered.
import asyncio
async def fetch(n):
return n
async def main():
r = await asyncio.gather(*[fetch(i) for i in range(3)])
print(sum(r))
asyncio.run(main())
Before you run it, say out loud what it will print. Then open the answer.
What it prints
3
2
Stages chained.
import asyncio
async def stage(n):
return n + 1
async def main():
a = await stage(1)
b = await stage(a)
print(b)
asyncio.run(main())
Before you run it, say out loud what it will print. Then open the answer.
What it prints
3
3
Group sized.
import asyncio
async def one(n):
return n
async def main():
tasks = [asyncio.ensure_future(one(i)) for i in range(3)]
r = await asyncio.gather(*tasks)
print(len(r))
asyncio.run(main())
Before you run it, say out loud what it will print. Then open the answer.
Before you run it, say out loud what it will print. Then open the answer.
What it prints
caught
Put it in order
Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.
Data Flow
Tasks are built, one await collects them, a failure surfaces at that await, and the boundary count is kept small.
1build the tasks
↓
2await them once
↓
3a failure surfaces there
↓
4keep the count small
Line by line
Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.
import asynciosets nothing on screen
async def fetch(n):sets nothing on screen
return nsets nothing on screen
async def main():sets nothing on screen
r = await asyncio.gather(*[fetch(i) for i in range(3)])sets nothing on screen
print(sum(r))3
asyncio.run(main())sets nothing on screen
Why this worksBoundaries cost
Each task is an object, a schedule slot, and a place an error can land. Fewer boundaries with the same overlap is the cheaper shape.
Errors land at awaits
A failure inside a task surfaces where it is awaited. Put the handling at the boundary and the trace stays readable.
Examples
One boundary
import asyncio
asyncdeffetch(n):
await asyncio.sleep(0)
return n
asyncdefmain():
r = await asyncio.gather(*[fetch(i) for i inrange(3)])
print(sum(r))
asyncio.run(main())
One boundary per item, awaited by one gather. The failure path is a single place.
A boundary per stage
import asyncio
asyncdefstage(n):
await asyncio.sleep(0)
return n + 1asyncdefmain():
a = awaitstage(1)
b = awaitstage(a)
print(b)
asyncio.run(main())
A stage that needs the last answer cannot overlap. The boundary here is a wall, not a task.
A task nobody awaits is a task whose errors go nowhere. Keep the handle or await it at the end.
A group awaited once
import asyncio
asyncdefone(n):
return n
asyncdefmain():
tasks = [asyncio.ensure_future(one(i)) for i inrange(3)]
r = await asyncio.gather(*tasks)
print(len(r))
asyncio.run(main())
Build the list of tasks first, then one await. That order is what makes the work overlap.