0

I have this code:

import asyncio

async def part1():
    print('1')
    await asyncio.sleep(2)
    print('5')


async def part2():
    print('2')
    await asyncio.sleep(2)
    print('6')


async def main():
    p1 = await part1()
    p2 = await part2()
    print('3')
    print('4')

asyncio.run(main())

When I run it, I would expect to print

1
2
3
4
5
6

The way I understand it the thread should go like this:

  1. Starts main() function
  2. Starts part1() function
  3. Prints 1
  4. While it's waiting 2 seconds, it should continue on the line p2 = await part2()
  5. Starts part2() function
  6. Prints 2
  7. While it's waiting for 2 seconds, it should continue on the line of main() "print('3')", then "print('4')"
  8. part1() function ends its sleep, so it prints 5
  9. part2() function ends its sleep, so it prints 6

However, what it prints is:

1
5
2
6
3
4

And waits the full time for both async.sleep(2)

What am I missing here?

Thanks!

Hiperfly
  • 227
  • 1
  • 10
  • You're still only running in a single thread. when `main` hits `await part1()`, it pauses until `part1()` returns back to it. It can't continue down it's "command list" until then. The way you've designed your `async/await` patterns doesn't really do anything because you haven't created any separate Tasks and/or Threads to run them on. – b_c Feb 26 '20 at 17:57
  • Hmmm the way I understand it is that when I call main() via async.run() it creates a sort of 'wrapper' or object that keeps track of the thread and tries to keep the calulations running when an asynchronous function is idle. Since asyncio module is not parallel, but just concurrent, I understand I don't need to create any other tasks or threads since the whole point is to have it all running in a single thread, isn't it? Please correct me if I'm wrong, thanks for your input! Also, could you please indicate how would you modify the code without using the Threading module, just the asyncio? – Hiperfly Feb 26 '20 at 19:31
  • I started a comment reply, but it began turning into an answer, so I just did that instead :) Referring to "threads" in Python can get pretty murky and I probably use the term incorrectly a lot in regards to Python. I tend to think of a "thread" as a separate "execution space", and the `await` keyword manages which of those spaces has control at a given point in time. – b_c Feb 27 '20 at 15:43

2 Answers2

0
  1. While it's waiting 2 seconds, it should continue

This is a misunderstanding. await means precisely the opposite, that it should not continue (running that particular coroutine) until the result is done. That's the "wait" in "await".

If you want to continue, you can use:

    # spawn part1 and part2 as background tasks
    t1 = asyncio.create_task(part1())
    t2 = asyncio.create_task(part2())
    # and now await them while they run in parallel
    p1 = await t1
    p2 = await t2

A simpler way to achieve the same effect is via the gather utility function:

    p1, p2 = asyncio.gather(part1(), part2())

Note that the code modified like this still won't output 1 2 3 4 5 6 because the final prints will not be executed until the tasks finish. As a result, the actual output will be 1 2 5 6 3 4.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
0

In response to your comment:

... [T]he way I understand it is that when I call main() via asyncio.run() it creates a sort of 'wrapper' or object that keeps track of the thread and tries to keep the calculations running when an asynchronous function is idle

(Small disclaimer - pretty much all my experience with async stuff is in C#, but the await keywords in each seem to match pretty well in behavior)

Your understanding there is more or less correct - asyncio.run(main()) will start main() in a separate (background) "thread".

(Note: I'm ignoring the specifics of the GPL and python's single-threadedness here. For the sake of the explanation, a "separate thread" is sufficient.)

The misunderstanding comes in with how you think await works, how it actually works, and how you've arranged your code. I haven't really been able to find a sufficient description of how Python's await works, other than in the PEP that introduced it:

await, similarly to yield from, suspends execution of read_data coroutine until db.fetch awaitable completes and returns the result data.

On the other hand, C#'s await has a lot more documentation/explanation associated with it:

When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete.

In your case, you've preceded the "middle output" (3 & 4) with 2 awaits, both of which will return control to the asycnio.run(...) until they have a result.

Here's the code I used that gives me the result you're looking for:

import asyncio

async def part1():
    print('1')
    await asyncio.sleep(2)
    print('5')


async def part2():
    print('2')
    await asyncio.sleep(2)
    print('6')


async def part3():
    print('3')
    print('4')

async def main():
    t1 = asyncio.create_task(part1())
    t2 = asyncio.create_task(part2())
    t3 = asyncio.create_task(part3())
    await t1
    await t2
    await t3

asyncio.run(main())

You'll notice I turned your main into my part3 and created a new main. In the new main, I create a separate awaitable Task for each part (1, 2, & 3). Then, I await them in sequence.

When t1 runs, it hits an await after the first print. This pauses part1 at that point until the awaitable completes. Program control will return to the caller (main) until that point, similar to how yield works.

While t1 is "paused" (waiting), main will continue on and start up t2. t2 does the same thing as t1, so startup of t3 will follow shortly after. t3 does no await-ing, so its output occurs immediately.

At this point, main is just waiting for its child Tasks to finish up. t1 was await-ed first, so it will return first, followed shortly by t2. The end result is (where test.py is the script I put this in):

~/.../> py .\test.py
1
2
3
4
5
6
b_c
  • 1,202
  • 13
  • 24
  • _"This is very similar to how a UI (think Tkinter) runs the GUI Event Loop."_ - not really. Tkinter doesn't run the event loop in a separate thread. – Bryan Oakley Feb 27 '20 at 16:53
  • Yeah, bad comparison. I took it out. – b_c Feb 27 '20 at 18:40
  • Sorry for the delay, was busy with other projects and didn´t receive any notification about this answer, but this one is great! Thanks a lot! – Hiperfly Mar 08 '20 at 16:29