Search code examples
pythonconcurrencypython-asyncio

How to do a "real concurrent" corroutines


I'm trying to do this:

"An event loop runs in a thread (typically the main thread) and executes all callbacks and Tasks in its thread. While a Task is running in the event loop, no other Tasks can run in the same thread. When a Task executes an await expression, the running Task gets suspended, and the event loop executes the next Task."

https://docs.python.org/3/library/asyncio-dev.html#concurrency-and-multithreading

And I did this ugly example:

import asyncio

async def print_message(message):
    print(message)

async def int_sum(a, b):
    await print_message('start_sum')
    result = a + b
    await print_message('end_sum')
    return result

async def int_mul(a, b):
    await print_message('start_mul')
    result = a * b
    await print_message('end_mul')
    return result

async def main():
    result = await asyncio.gather(int_sum(4, 3), int_mul(4, 3))
    print(result)

asyncio.run(main())

With "secuential-like" results:

$ python async_test.py

start_sum
end_sum
start_mul
end_mul
[7, 12]

But I want a "corroutine-like" output:

$ python async_test.py

start_sum
start_mul
end_sum
end_mul
[7, 12]

How can I do that?

Note: I'm not looking for a asyncio.sleep(n) example, I'm looking for "When a Task executes an await expression, the running Task gets suspended, and the event loop executes the next Task".


Solution

  • The point is, Tasks only give the control back to the event loop with yield statement. In your example you already have three active tasks(add asyncio.all_tasks() in the first line of int_sum coroutine to confirm1) but for example int_sum is not cooperating. it doesn't give the control back to the event loop. why ? Because you don't have any yield.

    A simple fix to this is to change your print_message to:

    async def print_message(message):
        print(message)
        await asyncio.sleep(0)
    

    if you see the source code of asyncio.sleep:

    async def sleep(delay, result=None):
        """Coroutine that completes after a given time (in seconds)."""
        if delay <= 0:
            await __sleep0()
            return result
    ...
    

    And this is the body of the __sleep0()(right above the sleep):

    @types.coroutine
    def __sleep0():
        """Skip one event loop run cycle.
    
        This is a private helper for 'asyncio.sleep()', used
        when the 'delay' is set to 0.  It uses a bare 'yield'
        expression (which Task.__step knows how to handle)
        instead of creating a Future object.
        """
        yield
    

    Now your output should be:

    start_sum
    start_mul
    end_sum
    end_mul
    [7, 12]
    

    1 Note: you do have three tasks, asyncio.gather does that for you:

    If any awaitable in aws is a coroutine, it is automatically scheduled as a Task.