1

So I am running an asyncio example:

import asyncio, time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    task1 = asyncio.create_task(say_after(1, 'hello'))

    task2 = asyncio.create_task(say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

This piece of code works correctly with output:

started at 14:36:06
hello
world
finished at 14:36:08

The 2 coroutines is running asynchronously, finally took 2 seconds, which has no problem. However, when I combine the lines together and directly await the Task object, like this:

import asyncio, time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():

    print(f"started at {time.strftime('%X')}")

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await asyncio.create_task(say_after(1, 'hello'))
    await asyncio.create_task(say_after(2, 'world'))

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

This result becomes:

started at 14:37:12
hello
world
finished at 14:37:15

which took 3 seconds, indicating that coroutine running incorrectly.

How can I make the latter code working properly? or is there something idk resulting in this difference?

P.S. The example acturally comes from python doc: https://docs.python.org/3.8/library/asyncio-task.html#coroutines

2
  • 1
    The strange thing about await asyncio.create_task(say_after(1, 'hello')) is that you create a task object but then immediately wait on it. Its the same thing as await say_after(1, 'hello'). Commented Dec 11, 2020 at 7:24
  • This answer also covers this question in some detail. Commented Dec 11, 2020 at 8:45

3 Answers 3

3

await makes it so that code 'stops' and continues after the awaited coroutine is completed, so when you write

await asyncio.create_task(say_after(1, 'hello'))
await asyncio.create_task(say_after(2, 'world'))

the second task is created and run after the first coroutine was completed, therefore it takes 3 seconds total. As a solution, consider using a function like gather or wait. For example:

    import asyncio, time
    
    async def say_after(delay, what):
        await asyncio.sleep(delay)
        print(what)
    
    async def main():
        print(f"started at {time.strftime('%X')}")

        # Wait until both tasks are completed (should take
        # around 2 seconds.)
        await asyncio.gather(say_after(1, 'hello'), say_after(2, 'world'))

        print(f"finished at {time.strftime('%X')}")

    asyncio.run(main())

Output:

started at 08:10:04
hello
world
finished at 08:10:06
Sign up to request clarification or add additional context in comments.

4 Comments

Yeah I know I could use asyncio.gather / asyncio.wait, and the coroutines would work perfectly...
However, as you've explained the second task is created and run after the first coroutine was completed, why the former code can work properly?
It's just how the await statement is designed, your first example is also fine. Just make sure you create the tasks before you await them, because create_task allows them to get scheduled.
@SimZhou Your first example works because both tasks are already scheduled through create_task when the first await is called.
1

From the docs Await expression:

Suspend the execution of coroutine on an awaitable object. Can only be used inside a coroutine function.

Whenever you await, the routine is suspended until the waited task completes. In the first example, both coroutines start and the 2 second sleep in the second overlaps the first. By the time you start running after the first await, 1 second has already elapsed in the second timer.

In the second example, the second await asyncio.create_task(say_after(2, 'world')) isn't scheduled until after the first completes and main continues running. That's when the 2 second sleep for the second task begins.

I've combined the examples to show the progression. Instead of the original prints, I print a start message before say_after awaits and a finish message just after main's await. You can see the time difference in the results.

import asyncio, time

async def say_after(delay, what):
    print(f"start {what} at {time.strftime('%X')}")
    await asyncio.sleep(delay)
    print(what)

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

async def main2():
    await asyncio.create_task(say_after(1, 'hello'))
    print(f"Finished hello at {time.strftime('%X')}")
    await asyncio.create_task(say_after(2, 'world'))
    print(f"Finished world at {time.strftime('%X')}")

print("========== Test 1 ============")
asyncio.run(main())

print("========== Test 2 ============")
asyncio.run(main2())

The results of the second test show that the second say_after isn't called until the first completes.

========== Test 1 ============
start hello at 00:51:42
start world at 00:51:42
hello
Finished hello at 00:51:43
world
Finished world at 00:51:44
========== Test 2 ============
start hello at 00:51:44
hello
Finished hello at 00:51:45
start world at 00:51:45
world
Finished world at 00:51:47

In main, tasks are created to run asyncio.sleep, but those tasks aren't actually run until main returns to the even loop. If we add a time.sleep(3) we might expect these two overlapped async sleeps to already be complete, but in fact say_after isn't even run until the first await that lets the event loop continue.

import asyncio, time

async def say_after(delay, what):
    print(f"starting {what} at {time.time()-start}")
    await asyncio.sleep(delay)
    print(what)

async def main():
    global start
    print('time asyncio.sleep with intermedite time.sleep')
    start = time.time()
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))

    # similate working for 3 seconds with non asyncio sleep
    time.sleep(3)
    print(f'expect 3 got {time.time()-start}')
    await task1  # <== where the 2 `say_after` tasks start
    print(f'expect 3 got {time.time()-start}')
    await task2
    print(f'expect 3 got {time.time()-start}')

asyncio.run(main())

Produces

time asyncio.sleep with intermedite time.sleep
expect 3 got 3.0034446716308594
starting hello at 3.003699541091919
starting world at 3.0038907527923584
hello
expect 3 got 4.005880355834961
world
expect 3 got 5.00671124458313

Adding an asyncio.sleep(0) to main after setting up the tasks allows them to run and do their own overlapped sleeps and the code works as we want.

import asyncio, time

async def say_after(delay, what):
    print(f"starting {what} at {time.time()-start}")
    await asyncio.sleep(delay)
    print(what)

async def main():
    global start
    print('time asyncio.sleep with event loop poll and intermedite time.sleep')
    start = time.time()
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))

    # let the `say_after` tasks (and anything else pending) run
    await asyncio.sleep(0)

    # similate working for 3 seconds with non asyncio sleep
    time.sleep(3)
    print(f'expect 3 got {time.time()-start}')
    await task1  # <== where the 2 `say_after` tasks start
    print(f'expect 3 got {time.time()-start}')
    await task2
    print(f'expect 3 got {time.time()-start}')

asyncio.run(main())

6 Comments

Hey thanks for you effort but I don't think you are right on By the time you start running after the first await, 1 second has already elapsed! To prove this, try add a line of time.sleep(3) after task2 = asyncio.create_task(say_after(2, 'world')). So if you were right, the two coroutines should finish at the same time. However it is not!
@YihuaZhou time.sleep() confounds the issue because it stops the whole event loop and therefore all coroutines. You should never call time.sleep() when using or testing asyncio; await asyncio.sleep() instead.
@user4815162342 - I get your point, but I tried the sleep(3) and it did add a 3 second delay, which I find very puzzling. All asyncio.sleep does is schedule a very boring completion routine in the future. time.sleep(3) is a standin for anything that eats CPU - say, a pandas transformation. I can't figure out why the asyncio.sleep delays until the scheduling coroutine does something to go back to the event loop.
I'm not sure I understood where you added sleep(3) which added the 3-second delay, could you be more specific? Also, does sleep(3) mean that you used await asyncio.sleep(3) or that you used time.sleep(3)? As for pandas and such, if you need to call CPU-bound code from asyncio, you should use run_in_executor() to avoid the event loop being hatled.
@user4815162342 I added time.sleep(3) just before await task1 on the first example. asyncio.sleep creates a future and does call_later with the time delta. I assumed that timeout should start counting down right then, but it seems to wait until await task1. Seems I have some studying to do.
|
0

I kinda understand the question now...

await makes the process blocked at that line.

So in the main function, if you want to do parrellel tasks, better use asyncio.wait/gather...

I think it is just the design style of Asyncio which makes the former code working well...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.