Search code examples
pythonpython-asynciocoroutine

Asyncio future, running Future objects


I have a code:

import asyncio as aio


async def coro(future: aio.Future):
    print('Coro start')
    await aio.sleep(3)
    print('Coro finish')
    future.set_result('coro result')


async def main():
    future = aio.Future()
    aio.create_task(coro(future))
    await future
    coro_result = future.result()
    print(coro_result)


aio.run(main())

In main() I create an empty aio.Future object, then I create a task with aio.create_task(coro(future)) using coroutine which takes aio.Future object. Then I 'run' the empty future with await future. Somehow this line runs the task instead of running the empty coroutine! I don't understand how it works and why it goes like this, because I expect the line await future to run the empty future, not task!

If I reorganize my main() like this:

import asyncio as aio


async def coro(future: aio.Future):
    print('Coro start')
    await aio.sleep(3)
    print('Coro finish')
    future.set_result('coro result')


async def main():
    future = aio.Future()
    await aio.create_task(coro(future))
    # await future
    coro_result = future.result()
    print(coro_result)


aio.run(main())

I get the same result but the code behaviour becomes much more explicit for me.


Solution

  • First, let's clear up some terminology. You said, "Then I 'run' the empty future with await future ..." A future is not "run". A future represents a value that will be set in the future. If you await the future, there has to be some other task that calls set_result on the future before your await is satisfied.

    Then you said, "Somehow this line (await future) runs the task instead of running the empty coroutine!" I don't know what you mean by an "empty coroutine". Let's see what is actually happening:

    In main you create a task with aio.create_task(coro(future)). First, you should ideally assign the task instance that was created to some variable so that a reference to the task exists preventing the task from being prematurely garbage collected (and thus terminated). For example,

    task = aio.create_task(coro(future))
    

    Now that you have created a task, it will potentially execute (depending on what other tasks exist) as soon as main either executes an await statement or returns. Thus the mere fact that you execute await future is sufficient to cause function coro to start running. coro sets a result in the future and when it issues an await or returns, then another task gets a chance to run. In this case coro returns and the await issued on the future by main completes.

    Your second example is less than ideal. main wants to wait for the future to be set with a value. This setting is being done by coro so clearly if you wait for coro to complete you will discover that your future has been set. But what if coro is a very long running task and sets a value in the future long before it terminates? In this case main will be waiting an unnecessarily long period of time since the future it is interested in was set long before coro ever terminated. Your code should therefore be:

    import asyncio as aio
    
    async def coro(future: aio.Future):
        print('Coro start')
        # For demo purposes we set the future right away:
        future.set_result('coro result')
        await aio.sleep(3)
        print('Coro finish')
    
    
    async def main():
        future = aio.Future()
        task = aio.create_task(coro(future))
        # We are interested in examining the future as soon
        # as it gets a result, which may be before coro terminates:
        await future
        # Now we can call `result` on the future even though coro will
        # not terminate for 3 more seconds:
        coro_result = future.result()
        print(coro_result)
        await task  # Give coro a chance to finish
    
    aio.run(main())
    

    Prints:

    Coro start
    coro result
    Coro finish