1

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.

2
  • 2
    It makes more sense if you think of await as meaning "wait for", like it does in the dictionary, instead of meaning "run". Commented Jan 20 at 16:27
  • 1
    await aio.create_task(coro(future)) waits for corutine to finish, await future waits for future to get assigned final result. You need to have at the least one await to avoid error Commented Jan 20 at 19:30

3 Answers 3

3

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
Sign up to request clarification or add additional context in comments.

5 Comments

"... task, it will potentially execute ... as soon as main either executes an await statement or returns." - is it implementation specific of guarantied?
@Serge I am saying that in general there are multiple tasks waiting to run and one task that is running. It is only when that running task completes or issues an await that another task could be dispatched to run. If there are other tasks that are ready to run and the thing that the running task is awaiting has already completed, then it's not clear to me who will run next. Quite possibly the task that has been running will continue to run. Maybe not. One would have to look at the implementation, which could change tomorrow.
@Serge I did a little test, which is not completely conclusive. But the currently running task, worker2, awaits a completed future and continues to run despite another task worker1 being ready to run. There is an Eager Task factory that can be used so that when a task is created, it runs immediately and the task that created it is suspended. But that is not the default behavior.
@Booboo many thanks for your answer!!! After posting the question I preceeded with my investigation and came to understanding what you write in the first part. My understanding is- await in main() 'starts' not exactly the object which follows await but all the objects in event loop which already includes task with coro. So thats why await future actually 'runs' the task. But await future will wait for result set for future without waiting for task results. Thats the second part of your answer and you cleared that up for me. So thanks a lot again:)
Just to clarify your last comment: The event loop only runs one task at a time and may start or resume execution of a single ready task (i.e. with no unsatisfied await statement) when the currently running task issues an await or returns. So await does not, as you say, "(await) starts not exactly the object which follows await but all the objects in event loop which already includes task with coro." And tasks are created implicitly when you pass a coroutine to asyncio.run or asyncio.gather or explicitly with a call to asyncio.create_task.
1

I feel like there is a misconception of what a future is and how it works, so simply a future is an object that has a result which is not available yet and will be set in the future (obvious by the name), and it is mainly used inside of Asnycio through other functions and such but we can use futures outside of it as well.

Its pretty much a placeholder which we can set either a result/value to or an exception, and in the situation where you are creating a task and it "fills" up the future that's the point of it, so that the future is populated.

The following will explain the concept better in a code manner:

async def main():

    future = aio.Future()

    # Create a task that will set the result of 'future'
    aio.create_task(coro(future))

    # This will "block" until 'future' gets a result or exception
    await future

    # Retrieves the result of 'future' once it's set
    coro_result = future.result()

    print(coro_result)

Hope this helps :)

Comments

1

To keep it simple, aio.create_task(coro(future)) already schedules (launches) the coro coroutine . Yet main is not suspended right away, it continues to execute (unless one or another await gives task a chance to execute). await future suspends the main coroutine and waits until future gets assigned the final result. It does not start the task per se, the task is already launched, just gives its opportunity to run and ensures completion.

await aio.create_task(coro(future)) in the main co-routine not only creates the task, but awaits until the coro task is finished.

Either way you need at least one await to make sure the result is ready

3 Comments

I dont completely understand what you mean by <aio.create_task(coro(future)) already launches a co-routine>, the task is run by await future, isn't it?
Not necessarily future. Strictly speaking, create_task creates and schedules a task to be ready to run. Normally, its actual execution can begin when the main coroutine is suspended by an await (of whatever: future, past or present) or, perhaps, a return, because await suspends the current coroutine. But if you like to execute a task asap, create eager tasks, but this is more advanced subject
tldr just any await will give coro a chance to get control and execute. But await future guaranties the main will be suspended until future gets assigned the results.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.