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

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.


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