TL;DR
- Invoking a coroutine function(
async def
) will NOT run it. It returns a coroutine
object, like generator functions return generator objects.
await
retrieves values from coroutines, i.e. "calls" the coroutine.
eusure_future/create_task
wrap a coroutine and schedule it to run on the event loop
on next iteration, but will not wait for it to finish, it's like a daemon thread.
- By awaiting a coroutine or a task wrapping a coroutine, you can always retrieve the
result returned by the coroutine, the difference is their execution order.
Some code examples
Let's first clear some terms:
- coroutine function, the one you
async def
s;
- coroutine object, what you got when you "call" a coroutine function;
- task, a object wrapped around a coroutine object to run on the event loop.
- awaitable, something that you can
await
, like task, future or plain coroutine object.
The term coroutine
can be both coroutine function and coroutine object depending on
the context, but it should be easy enough for you to tell the differences.
Case 1, await
on a coroutine
We create two coroutines, await
one, and use create_task
to run the other one.
import asyncio
import time
# coroutine function
async def log_time(word):
print(f'{time.time()} - {word}')
async def main():
coro = log_time('plain await')
task = asyncio.create_task(log_time('create_task')) # <- runs in next iteration
await coro # <-- run directly
await task
if __name__ == "__main__":
asyncio.run(main())
You will get results like this, plain coroutine was executed first as expected:
1539486251.7055213 - plain await
1539486251.7055705 - create_task
Because coro
was executed directly, and task
was executed in the next iteration.
Case 2, yielding control to event loop
By calling asyncio.sleep(1)
, the control is yielded back to the loop, we should see a
different result:
async def main():
coro = log_time('plain await')
task = asyncio.create_task(log_time('create_task')) # <- runs in next iteration
await asyncio.sleep(1) # <- loop got control, and runs task
await coro # <-- run directly
await task
You will get results like this, the execution order is reversed:
1539486378.5244057 - create_task
1539486379.5252144 - plain await
When calling asyncio.sleep(1)
, the control was yielded back to the event loop, and the
loop checks for tasks to run, then it runs the task
created by create_task
first.
Although we invoked the coroutine function first, without await
ing it, we just created
a coroutine, it does NOT start automatically. Then, we create a new coroutine and wrap it
by a create_task
call, creat_task
not only wraps the coroutine, but also schedules
the task to run on next iteration. In the result, create_task
is executed before plain await
.
The magic here is to yield control back to the loop, you can use asyncio.sleep(0)
to
achieve the same result.
After all the differences, the same thing is: if you await on a coroutine or a task
wrapping a coroutine, i.e. an awaitable, you can always retrieve the result they return.
Under the hood
asyncio.create_task
calls asyncio.tasks.Task()
, which will call loop.call_soon
.
And loop.call_soon
will put the task in loop._ready
. During each iteration of the loop,
it checks for every callbacks in loop._ready
and runs it.
asyncio.wait
, asyncio.ensure_future
and asyncio.gather
actually call loop.create_task
directly or indirectly.
Also note in the docs:
Callbacks are called in the order in which they are registered. Each callback will be called exactly once.