1

There is something I can't understand in this code

import asyncio


async def fetch_data(param):
    print(f"Do something with {param}...")
    await asyncio.sleep(param)
    print(f"Done with {param}")
    return f"Result of {param}"


async def main():
    task1 = asyncio.create_task(fetch_data(1))
    task2 = asyncio.create_task(fetch_data(2))
    result2 = await task2
    print("Task 2 fully completed")
    result1 = await task1
    print("Task 1 fully completed")
    return [result1, result2]


results = asyncio.run(main())
print(results)

The output is

Do something with 1...
Do something with 2...
Done with 1
Done with 2
Task 2 fully completed
Task 1 fully completed
['Result of 1', 'Result of 2']

I expected to see

Do something with 2...

In the first line, but it outputs Do something with 1... first. It seems just creating tasks will run the coroutine, while from what I read and saw, it only registers it in the event loop. The flow should be

  • From results = asyncio.run(main()) the main is registered into event loop
  • Event loop runs the main
  • Two tasks 1,2 are created and registered into the event loop, with status ready
  • By result2 = await task2, the main is suspended fetch_data(2) is run

From this flow, I expect to see Do something with 2... in the first line. Why does this output the Do something with 1...? first?

To verify, I run this

import asyncio


async def fetch_data(param):
    print(f"Do something with {param}...")
    await asyncio.sleep(param)
    print(f"Done with {param}")
    return f"Result of {param}"


async def main():
    task1 = asyncio.create_task(fetch_data(1))
    task2 = asyncio.create_task(fetch_data(2))

results = asyncio.run(main())
print(results)

and the output is

Do something with 1...
Do something with 2...
None

Why are the couroutines running even without awaiting the tasks? Why does print(f"Done with {param}") not run in this version?

8
  • create_task() starts running task. If you don't want start it then use only await fetch_data(2). Commented Sep 4 at 9:56
  • 1
    no - await is NOT starting if you used create_task() - it only waits for finishing it. Commented Sep 4 at 9:59
  • 1
    there are two things: coroutine and task. fetch_data is a coroutine, task1 is a task. When you runs await coroutine then it converts coroutine to task, starts it and wait for results. When you runs await task then it is already running and it has to only wait for result Commented Sep 4 at 10:05
  • 1
    it doesn't runs concorenlty because await is blocking code. Commented Sep 4 at 10:06
  • 1
    if you want run concurently then you have to use create_task(coroutine1) or gather([coroutine1, coroutine2]) Commented Sep 4 at 10:09

4 Answers 4

4

Let's insert a new line in main following the creation of task2:

import asyncio

async def fetch_data(param):
    print(f"Do something with {param}...")
    await asyncio.sleep(param)
    print(f"Done with {param}")
    return f"Result of {param}"


async def main():
    task1 = asyncio.create_task(fetch_data(1))
    task2 = asyncio.create_task(fetch_data(2))
    print('tasks created')  # New statement
    result2 = await task2
    print("Task 2 fully completed")
    result1 = await task1
    print("Task 1 fully completed")
    return [result1, result2]


results = asyncio.run(main())
print(results)

The first line of output will be:

tasks created

By default a call such as asyncio.create_task(fetch_data(1)) creates the task and it can be now scheduled to run -- but it is not automatically started. The current task (main) continues to run and any other task that has been created cannot run until main either issues an await statement or completes. Thus, the newly created tasks cannot start running until main issues the call to await task2. As a result of this await call, other tasks can now run one at a time. Since task1 was created before task2 it is the next task to run. So the task for coro fetch_data(1) will start executing and continue executing until it issues its first await statement. The next task to run will be task2 (coro fetch_data(2)) since the main task is blocking until task2 completes. Thus the complete output will be:

tasks created
Do something with 1...
Do something with 2...
Done with 1
Done with 2
Task 2 fully completed
Task 1 fully completed
['Result of 1', 'Result of 2']

Update

If you are running Python >= 3.12, then tasks can be created eagerly, in which case they start running immediately:

import asyncio

async def fetch_data(param):
    print(f"Do something with {param}...")
    await asyncio.sleep(param)
    print(f"Done with {param}")
    return f"Result of {param}"


async def main():
    loop = asyncio.get_running_loop()
    loop.set_task_factory(asyncio.eager_task_factory)

    task1 = asyncio.create_task(fetch_data(1))
    task2 = asyncio.create_task(fetch_data(2))
    print('tasks created')
    result2 = await task2
    print("Task 2 fully completed")
    result1 = await task1
    print("Task 1 fully completed")
    return [result1, result2]


results = asyncio.run(main())
print(results)

Prints:

Do something with 1...
Do something with 2...
tasks created
Done with 1
Done with 2
Task 2 fully completed
Task 1 fully completed
['Result of 1', 'Result of 2']
Sign up to request clarification or add additional context in comments.

7 Comments

When it comes to result2 = await task2, it waits and runs the tasks that are ready. Does this mean it runs tasks 1,2 at the same time? Or first run Task 1 and then run Task 2?
Only one async task can run at a time. The currently running task continues to execute until it issues an await or completes. So when result2 = await task2 is executed only Task 1 will execute and will do so until it issues an await or completes. When Task 1 issues its await call, then Task 2 can start running.
Thanks. Now I understand this concept.
if you add asyncio.sleep(1) before print('tasks created') then you see that task1 is already running and it will display Do something with 1 before you use await task1
I think you mean task1 rather than taska. But no, you have to issue await asyncio.sleep(1) instead of just asyncio.sleep(1) otherwise you get RuntimeWarning: coroutine 'sleep' was never awaited.
OK, I was thinking that you means it needs exactly await task1 to start task1 but you mean any await and that is True. (and I wrote asyncio.sleep(1) but I was thinking await asyncio.sleep(1))
Yes, I meant any await.
2

When you call asyncio.create_task(coro), the coroutine doesn’t just get registered. It is scheduled immediately on the event loop. That means the coroutine is started right away, up until its first await.

That’s why you see Do something with 1... printed immediately after creating task1: the event loop gives it a chance to run at least until it hits await asyncio.sleep(...). Same for task2.

1 Comment

No! By default calling asyncio.create_task(coro) does not start up the coro task immediately. This new task only starts up eventually after the current task issues an await or completes. This can be seen by inserting a print('tasks created') statement after the statement task2 = asyncio.create_task(fetch_data(2)) in the OP's original code. The first line of output you will see is the result of this newly inserted print statement.
2

There are two things: coroutine and task. fetch_data is a coroutine, task1 is a task.

When you runs await coroutine then it converts coroutine to task, starts it and wait for result.

When you runs await task then this task is already running and it has to only wait for result.
It NOT starts task again.

It allows to run some other code at the same time

task1 = create_task(fetch_data(1))  # start task 1
task2 = create_task(fetch_data(2))  # start task 2

print("Here I can do something else")

result1 = await task1  # wait for end of task1 (and eventully get result)
result2 = await task2  # wait for end of task1 (and eventully get result)  

If you want run concurently then you have to use create_task(coroutine1) (and later await(there is also as_completed())) or gather([coroutine1, coroutine2])


Important detail. To really run these tasks there should be any await in main() which will allow to switch to these tasks. It can be even await asyncio.sleep().


If you add some asyncio.sleep() then you can see it runs it before you use await

task1 = create_task(fetch_data(1))  # start task 1
await asyncio.sleep(2)
task2 = create_task(fetch_data(2))  # start task 2
await asyncio.sleep(3)

print("Here I can do something else")

result1 = await task1  # wait for end of task1 (and eventully get result)
result2 = await task2  # wait for end of task2 (and eventully get result)  

Result:

Do something with 1...
Done with 1
Do something with 2...
Done with 2

Here I can do something else

2 Comments

Just a point, ` task2 = asyncio.create_task(fetch_data(2)) print('#11') task1 = asyncio.create_task(fetch_data(1)) print('#22') result2 = await task2` the prints #11 and #22 are run first. It means the main register tasks in the event loop, but runs them only when it has to wait for something. And by FIFO, the event loop will run the coroutines.
it display #11 as first because create_task(fetch_data(2)) print('#11') was used as first and it was already running when you run another create_task(fetch_data(1)) print('#22'). If you add async.sleep() then between create_task(fetch_data(2)) then you should see #11 before you use create_task() or await
1

To answer the second part of your question (and answer an unasked question as well):

The reason you don't see the Done with outputs are that you are not waiting for the tasks to complete. You start them, then return from main. If you instead did:

async def main():
    task1 = asyncio.create_task(fetch_data(1))
    task2 = asyncio.create_task(fetch_data(2))
    task1_results = await task1
    task2_results = await task2

or alternatively,

import asyncio

# ...

async def main():
    task1 = asyncio.create_task(fetch_data(1))
    task2 = asyncio.create_task(fetch_data(2))
    task1_results, task2_results = asyncio.gather(task1, task2)

then the main function will wait for task1 and task2 to complete before itself returning.

Finally, the reason the last line of the output is None is that your main() is not returning any values. If you want to see the return values from the tasks, you would need to add

    return task1_results, task2_results

or more directly (when using the asyncio.gather method)

    return asyncio.gather(task1, task2)

2 Comments

So the main is terminated before actually seeing the Done, yes?
well, main() returns and then the python script terminates, but yes.

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.