How can python bypass my prior code to run later codes first?

Viewed 60
import asyncio
import time


async def func():
    print('task start')
    await asyncio.sleep(10)
    print('task end')


async def main():
    task1 = asyncio.create_task(func())
    task2 = asyncio.create_task(func())
    task3 = asyncio.create_task(func())
    task4 = asyncio.create_task(func())
    s = time.monotonic()
    print('main start', time.monotonic() - s)
    await task1
    print('main continue', time.monotonic() - s)
    await task2
    print('main continue', time.monotonic() - s)
    await task3
    print('main continue', time.monotonic() - s)
    await task4
    print('main end', time.monotonic() - s)


asyncio.run(main())

This code gives results as below:

main start 0.0
task start
task start
task start
task start
task end
task end
task end
task end
main continue 10.0
main continue 10.0
main continue 10.0
main end

But how is this possible ,python bypassed my prior print calls , run the awaitables first and then go back to make print calls , how shall I make sense of this ?

2 Answers

All your tasks are sleeping 10 seconds, then continue and finish almost instantly. So all the await calls will unlock at the same time since when task 1 if finished, all tasks will be finished as well.

You are right in the sense that you technically could have intertwined prints between task end and main continue but I guess this is an implementation detail that everything appears grouped.

I think you can have a better understanding of what is going on using this tweaked script:

import asyncio
import time


async def func(task_nb, wait):
    print('[%s] task start' % task_nb)
    await asyncio.sleep(wait)
    print('[%s] task end' % task_nb)


async def main():
    task1 = asyncio.create_task(func(1, 1))
    task2 = asyncio.create_task(func(2, 5))
    task3 = asyncio.create_task(func(3, 7))
    task4 = asyncio.create_task(func(4, 2))
    s = time.monotonic()
    print('main start', time.monotonic() - s)
    await task1
    print('main continue', time.monotonic() - s)
    await task2
    print('main continue', time.monotonic() - s)
    await task3
    print('main continue', time.monotonic() - s)
    await task4
    print('main end', time.monotonic() - s)


asyncio.run(main())

You will have a more interesting await behaviour:

main start 1.81000359589234e-07
[1] task start
[2] task start
[3] task start
[4] task start
[1] task end
main continue 1.0019499360005284
[4] task end
[2] task end
main continue 5.001785704000213
[3] task end
main continue 7.003587035000237
main end 7.003632674000073

Your code does what it is supposed to do according to the specification of asyncio, but perhaps you are misunderstanding what a "task" is.

From the docs:

asyncio.create_task(coro, *, name=None)

Wrap the coro coroutine into a Task and schedule its execution. Return the Task object.

This means that as you've created 4 tasks at the beginning of your main, they are all scheduled to execute starting at the same time. And so they all print task start together, then print task end together, at which point, all of your awaits instantly fall through as all tasks have completed at the same time (10 seconds later). So finally you see main continue 10.0 3 times.

Try this code, I believe it will have the behavior you are expecting.

async def main():
    task1 = func()
    task2 = func()
    task3 = func()
    task4 = func()
    ...
Related