Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does await give back control to the event loop during coroutine chaining?

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?

like image 822
Gaurav Keswani Avatar asked Mar 12 '18 02:03

Gaurav Keswani


People also ask

What is event loop in Asyncio?

The event loop is the core of every asyncio application. Event loops run asynchronous tasks and callbacks, perform network IO operations, and run subprocesses. Application developers should typically use the high-level asyncio functions, such as asyncio.

Is await a blocking call Python?

In Python, we need an await keyword before each coroutine object to have it called by the event loop. But when we put await , it makes the call blocking. It follows that we end up doing the same thing as we do in the blocking fashion.

Does await block event loop?

It'll block event loop at a point.

Does await yield Python?

Correct OP answer: No, await (per se) does not yield to the event loop, yield yields to the event loop, hence for the case given: "(2) We jump directly into send_message ".


2 Answers

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.

like image 88
abarnert Avatar answered Nov 13 '22 05:11

abarnert


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
like image 20
Yassine Marzougui Avatar answered Nov 13 '22 05:11

Yassine Marzougui