Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Order of execution in async function. How is it determined?

When I run the following asynchronous code:

from asyncio import create_task, sleep, run

async def print_(delay, x):
    print(f'start: {x}')
    await sleep(delay)
    print(f'end: {x}')

async def main():
    slow_task = create_task(print_(2, 'slow task'))
    fast_task = create_task(print_(1, 'fast task'))

    # The order of execution here is strange:
    print(0)
    await slow_task
    print(1)
    await fast_task

run(main())

I get an unexpected order of execution:

0
start: slow task
start: fast task
end: fast task
end: slow task
1

What exactly is happening?

How can I predict the order of execution?

What I find strange is print(1) is ignored until all tasks are finished. To my understanding the code runs as expected until it reaches any await. Then it creates a task-loop with any other awaitable it finds down the line. Which it prioritizes. Right?

That's what I find surprising. I'd expect it to run print(1) before any task is complete. Why doesn't it?

Is that the standard behavior or does it vary? If so, what does it vary upon?

If you could into detail how the event loop works alongside the rest of the code, that'd be great.

like image 766
user Avatar asked Oct 19 '25 20:10

user


1 Answers

Let's walk through this:

  • You create asynchronous processes for a fast task and a slow task at the same time; even the "fast" one will take a significant amount of time.
  • Immediately after creating them, you print 0, so this becomes your first output.
  • You call await slow_task, passing control to the event loop until slow_task finishes.
    • Because you requested slow_task, it's prioritized, so it starts first, so start: slow_task is printed.
    • Because slow_task contains an await sleep(2), it passes control back to the event loop, which finds fast_task as ready to operate and starts it, so start: fast_task is printed.
    • Because fast_task's await sleep(1) finishes first, fast_task completes, and end: fast_task is printed. Because we're awaiting slow_task, not fast_task, we remain in the event loop.
    • Finally, the slow task finishes, so it prints end: slow task. Because this is what we were awaiting for, control flow is returned to the synchronous process.
  • After the slow task has finished, you print 1, so this becomes your last output.
  • Finally, you wait for the fast task to finish; it already did finish earlier, while you were waiting for the slow task, so this returns immediately.

Everything is exactly as one would expect.


That said, for the more general case, you can't expect order-of-operations in an async program to be reliably deterministic in real-world cases where you're waiting on I/O operations that can take a variable amount of time.

like image 112
Charles Duffy Avatar answered Oct 22 '25 10:10

Charles Duffy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!