FTG
FTG

Reputation: 75

one await and tasks in the loop in python

In Python, does an await makes execute all the possible next tasks if they have the possibility to do so?

Example:

import asyncio
import time
async def say_after(delay, what):
    print(f"Task {what} started at {time.strftime('%X')}")
    #await asyncio.sleep(delay) // commented on purpose
    print(f"Task {what} finished at {time.strftime('%X')}")   

async def main():
    task1 = asyncio.create_task(say_after(1, 'task1'))
    task2 = asyncio.create_task(say_after(2, 'task2'))
    task3 = asyncio.create_task(say_after(2, 'task3'))
    
    print(f"started at {time.strftime('%X')}")  
    await task1
    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

Output:

started at 09:39:19
Task task1 started at 09:39:19
Task task1 finished at 09:39:19
Task task2 started at 09:39:19 
Task task2 finished at 09:39:19
Task task3 started at 09:39:19 
Task task3 finished at 09:39:19
finished at 09:39:19

Here task2 and task3 are started although I don't have any await in the say_after coro.

Does it mean that if I had an infinite task in the loop it would be launched before main is resumed whatever the 'time slot'? Intuitively, I was expecting only next task in the loop (eg task1) would be executed, and then the door is closed until another await is met. Or maybe the loop is a sequence, once it is 'opened' all tasks in it are executed until we are back to the first one?

Upvotes: 2

Views: 373

Answers (2)

Paul Cornelius
Paul Cornelius

Reputation: 10936

Asyncio is a cooperative multitasking system. Switching between tasks occurs only at await expressions.

The asyncio.run() statement in your program creates a task (main) and starts to run it. Main then creates three more tasks (task1, task2, task3). Only one task can run at a time, so those three tasks are "pending." The event loop keeps track of them, in order, in a list. When main() executes this line:

await task1

the await expression causes main() to be suspended. Main(), in effect, goes to the bottom of the task list. The event loop runs all the tasks in its list, starting with task1. Since task1 contains no await expression, it runs all the way to completion. Then task2 gets a turn and after that task3.

Only after all that happens does main() get another turn. Look again at this line of code:

await task1

This means that the main() task will not continue until task1 is finished - either by successfully executing all the way to a return statement, raising an Exception, or being cancelled. In this case, task1 is finished, so main() resumes where it left off. The final print statement is executed and the program ends.

What is important to realize is that this is all managed by the event loop, and await expressions are the only points at which task switching occurs. At each await, the event loop regains control and runs all the pending tasks in a round-robin fashion. When you said, "Or maybe the loop is a sequence, once it is 'opened' all tasks in it are executed until we are back to the first one" you were on exactly the right track.

If you removed the comment mark from the await expression inside of say_after, the tasks would all interleave. Each task would yield back to the loop, and at each yield it would take its place at the bottom of the task list. But main() would not actually be resumed until task1 was finished.

This mechanism results in programs that are, in general, easier to write and understand than threading. Multithreading is "pre-emptive" rather than co-operative; thread switches can occur at any time and are not under the programmer's control. Asycnio programs, on the other hand, switch tasks only at points that are clearly indicated in the code. They are also deterministic (although they may be complicated): the script you presented will produce exactly the same results on any computer, at any speed, on any OS. That is not necessarily the case with multithreading.

Upvotes: 3

el_oso
el_oso

Reputation: 1061

create_task() will schedule the task to run by adding it to the event loop. It will then run for an indeterministic amount of time swapping in with any other tasks in an unpredictable manner.

await simply tells the python interpreter to not continue past this line of code until whatever has been running has finished running.

import asyncio
import time
async def say_after(delay, what):
    print(f"Task {what} started at {time.strftime('%X')}")
    #await asyncio.sleep(delay) // commented on purpose
    print(f"Task {what} finished at {time.strftime('%X')}")   

async def main():
    task1 = asyncio.create_task(say_after(1, 'task1'))
    task2 = asyncio.create_task(say_after(2, 'task2'))
    task3 = asyncio.create_task(say_after(2, 'task3'))
    
    print(f"started at {time.strftime('%X')}")  
    await task1
    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

in this example, it's extremely unlikely that task 2 and 3 will ever complete before task1 returns, but they will start.

import asyncio
import time
async def say_after(delay, what):
    print(f"Task {what} started at {time.strftime('%X')}")
    #await asyncio.sleep(delay) // commented on purpose
    print(f"Task {what} finished at {time.strftime('%X')}")   

async def main():
    task1 = asyncio.create_task(say_after(5, 'task1'))
    task2 = asyncio.create_task(say_after(2, 'task2'))
    task3 = asyncio.create_task(say_after(2, 'task3'))
    
    print(f"started at {time.strftime('%X')}")  
    await task1
    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

in this example however, task1 runs for longer than task2 and task3. When task1 pauses because it's waiting for sleep to finish, it will continue running task2 and task3. If they finish, great, if not, python won't wait for them, it only cares about task1 finishing.

Upvotes: 2

Related Questions