10

I'm trying my hand at asyncio in Python 3.6 and having a hard time figuring out why this piece of code is behaving the way it is.

Example code:

import asyncio

async def compute_sum(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(5)
    print("Returning sum")
    return x + y

async def compute_product(x, y):
    print("Compute %s x %s ..." % (x, y))
    print("Returning product")
    return x * y

async def print_computation(x, y):
    result_sum = await compute_sum(x, y)
    result_product = await compute_product(x, y)
    print("%s + %s = %s" % (x, y, result_sum))
    print("%s * %s = %s" % (x, y, result_product))

loop = asyncio.get_event_loop()
loop.run_until_complete(print_computation(1, 2))

Output:

Compute 1 + 2 ...
Returning sum
Compute 1 x 2 ...
Returning product
1 + 2 = 3
1 * 2 = 2

Expected Output:

Compute 1 + 2 ...
Compute 1 x 2 ...
Returning product
Returning sum
1 + 2 = 3
1 * 2 = 2

My reasoning for expected output:

While the compute_sum coroutine is correctly called before the compute_product coroutine, my understanding was that once we hit await asyncio.sleep(5), the control would be passed back to the event loop which would start the execution of the compute_product coroutine. Why is "Returning sum" being executed before we hit the print statement in the compute_product coroutine?

Gaurav Keswani
  • 431
  • 4
  • 14
  • 3
    This is a very good question for people looking to understand how to use `await`, and I think it could be turned into a _great_ question… but I'm not sure how to do that. Anyway, certainly good enough for a +1 as it is, but hopefully someone can suggest something to make it even more searchable/generally-applicable. – abarnert Mar 12 '18 at 02:42

3 Answers3

14

You're right about how the coroutines work; your problem is in how you're calling them. In particular:

result_sum = await compute_sum(x, y)

This calls the coroutine compute_sum and then waits until it finishes.

So, compute_sum does indeed yield to the scheduler in that await asyncio.sleep(5), but there's nobody else to wake up. Your print_computation coro is already awaiting compute_sum. And nobody's even started compute_product yet, so it certainly can't run.

If you want to spin up multiple coroutines and have them run concurrently, don't await each one; you need to await the whole lot of them together. For example:

async def print_computation(x, y):
    awaitable_sum = compute_sum(x, y)
    awaitable_product = compute_product(x, y)        
    result_sum, result_product = await asyncio.gather(awaitable_sum, awaitable_product)
    print("%s + %s = %s" % (x, y, result_sum))
    print("%s * %s = %s" % (x, y, result_product))

(It doesn't matter whether awaitable_sum is a bare coroutine, a Future object, or something else that can be awaited; gather works either way.)

Or, maybe more simply:

async def print_computation(x, y):
    result_sum, result_product = await asyncio.gather(
        compute_sum(x, y), compute_product(x, y))
    print("%s + %s = %s" % (x, y, result_sum))
    print("%s * %s = %s" % (x, y, result_product))

See Parallel execution of tasks in the examples section.

abarnert
  • 354,177
  • 51
  • 601
  • 671
2

Expanding on the accepted answer, what asyncio.gather() does behind the scenes is that it wraps each coroutine in a Task, which represents work being done in the background.

You can think of Task objects as Future objects, which represent the execution of a callable in a different Thread, except that coroutines are not an abstraction over threading.

And in the same way Future instances are created by ThreadPoolExecutor.submit(fn), a Task can be created using asyncio.ensure_future(coro()).

By scheduling all coroutines as tasks before awaiting them, your example works as expected:

async def print_computation(x, y): 
    task_sum = asyncio.ensure_future(compute_sum(x, y)) 
    task_product = asyncio.ensure_future(compute_product(x, y)) 
    result_sum = await task_sum 
    result_product = await task_product 
    print("%s + %s = %s" % (x, y, result_sum)) 
    print("%s * %s = %s" % (x, y, result_product))

Output:

Compute 1 + 2 ...
Compute 1 x 2 ...
Returning product
Returning sum
1 + 2 = 3
1 * 2 = 2
carver
  • 2,229
  • 12
  • 28
-1

Here's how it works. Lets use a main thread for primary reference...

The main thread handles events and work from various locations. If there are 3 events fired at once from other threads the main thread can only handle one at a time. If the main thread is processing your loop it will continue processing it until the method (or function) is returned before handling other work.

This means that 'other work' is placed in a queue to be ran on the main thread.

When you use 'async await' you write 'async' to let it be known that the method will (or can be) broken into it's own set of queues. Then when you say 'await' it should be doing work on another thread. When it does the main thread is allowed to process the other events and work that is stored in queue instead of just waiting there.

So when the await work is complete it places the remaining portion of the method in the queue on the main thread as well.

So in these methods it doesn't continue processing but places the remaining work in a queue to be accomplished when the await is complete. Therefore it's in order. await compute_sum(x, y) gives control back to the main thread to do other work and when it's complete the rest is added to the queue to be worked. So await compute_product(x, y) is queued after the former is complete.

Michael Puckett II
  • 6,586
  • 5
  • 26
  • 46
  • This makes it sound like `asycio` tasks are implemented with explicit continuation objects rather than simple coroutines. Also, it explains what happens under the covers, but not what that means on the level of the user's actual code—and nobody's going to figure out how to fix their code without looking at that higher level first. – abarnert Mar 12 '18 at 02:31
  • @abarnert I was submitting my answer as you were and wasn't trying to trump on yours. I do believe understand async await is just as if not more important than just solving the problem. If he understands what and why it's doing it this way then he can learn from it and make better decisions about the results. – Michael Puckett II Mar 12 '18 at 02:34
  • I don’t think he’s actually confused about what coroutines do (and if he is, I don’t know that explaining them in terms of a mechanism that Python could conceivably have used, but doesn’t use, is going to help). But I could be wrong. Let’s wait and see what the OP thinks. – abarnert Mar 12 '18 at 02:39
  • Thanks for the answer. abarnert's answer definitely answered my question directly and helped me understand where I was going wrong and what I could do to correct it since it came with some basic code. That being said, I don't mean that your answer wasn't right. It's just that I am just starting off with asyncio and don't have enough understanding to make complete sense of it as of today :) – Gaurav Keswani Mar 12 '18 at 03:07
  • Another problem with this answer is that it is not correct. "When you use 'async await' you write 'async' to let it be known that the method will (or can be) broken into it's own set of queues" - there is but a single queue entry (task) for each async function, and it holds the running coroutine object. "Then when you say 'await' it should be doing work on another thread" - the whole idea behind asyncio is that everything is done in the same thread. – user4815162342 Mar 12 '18 at 06:56
  • @user4815162342 That's not correct.. The whole idea is that you're able to develop and write the code inline and marshal the variables back and forth between threads more like you're on a single thread. But behind the scenes it is in fact breaking it up into queues and performing the work on other threads. – Michael Puckett II Mar 13 '18 at 14:54
  • [The documentation](https://docs.python.org/3/library/asyncio-dev.html#concurrency-and-multithreading) is clear that "An event loop runs in a thread and executes all callbacks and tasks in the same thread." You might be referring to something like `run_in_executor`, which allows legacy blocking code to execute in a different thread and a coroutine to await it. But coroutines defined with `async def` are always run in the same thread, with potentially blocking calls converted to coroutine suspensions. – user4815162342 Mar 13 '18 at 15:20
  • @user4815162342 "All callbacks and tasks" not the actual work being performed. You are very confused. A thread can only operate inline. If it was literally all in the same thread then there would be absolutely no reason to use async await. Simply writing it inline would make the most sense and is always the most sense when you want to stay on the same thread. – Michael Puckett II Mar 13 '18 at 15:29
  • In asyncio, the work done by coroutines _is_ "in callbacks and tasks", because a task drives a coroutine. Coroutines run by asyncio are literally all in the same thread. Blocking work can be run in different threads, but only through specialized APIs such as `loop.run_in_executor()`. *If it was literally all in the same thread then there would be absolutely no reason to use async await* async/await allows coroutines to suspend instead of blocking, precisely so that they can all run in the same thread. – user4815162342 Mar 13 '18 at 16:20
  • @user4815162342' Palm to forehead' I think you really need to get into the underlying code and then come back to this thread. – Michael Puckett II Mar 14 '18 at 19:39
  • I'm well acquainted with the asyncio code, thank you very much. :) If the official documentation doesn't convince you, then feel free to find the place in asyncio that executes coroutines in different threads. – user4815162342 Mar 14 '18 at 20:32