100

In asynchronous JavaScript, it is easy to run tasks in parallel and wait for all of them to complete using Promise.all:

async function bar(i) {
  console.log('started', i);
  await delay(1000);
  console.log('finished', i);
}

async function foo() {
    await Promise.all([bar(1), bar(2)]);
}

// This works too:
async function my_all(promises) {
    for (let p of promises) await p;
}

async function foo() {
    await my_all([bar(1), bar(2), bar(3)]);
}

I tried to rewrite the latter in python:

import asyncio

async def bar(i):
  print('started', i)
  await asyncio.sleep(1)
  print('finished', i)

async def aio_all(seq):
  for f in seq:
    await f

async def main():
  await aio_all([bar(i) for i in range(10)])

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

But it executes my tasks sequentially.

What is the simplest way to await multiple awaitables? Why doesn't my approach work?

Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97

5 Answers5

108

The equivalent would be using asyncio.gather:

import asyncio

async def bar(i):
  print('started', i)
  await asyncio.sleep(1)
  print('finished', i)

async def main():
  await asyncio.gather(*[bar(i) for i in range(10)])

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

Why doesn't my approach work?

Because when you await each item in seq, you block that coroutine. So in essence, you have synchronous code masquerading as async. If you really wanted to, you could implement your own version of asyncio.gather using loop.create_task or asyncio.ensure_future.

EDIT

The original answer used the lower-level asyncio.wait.

charmoniumQ
  • 5,214
  • 5
  • 33
  • 51
Jashandeep Sohi
  • 4,903
  • 2
  • 23
  • 25
23

I noticed that asyncio.gather() may be a better way to await other than asyncio.wait() if we want ordered results.

As the docs indicates, the order of result values from asyncio.gather() method corresponds to the order of awaitables in aws. However, the order of result values from asyncio.wait() won't do the same thing.You can test it.

请叫我小马哥
  • 1,653
  • 13
  • 11
16

https://docs.python.org/3/library/asyncio-task.html#asyncio.gather

asyncio.gather() will return the list of output from each async function calls.

import asyncio

async def bar(i):
    print('started', i)
    await asyncio.sleep(1)
    print('finished', i)
    return i

async def main():
    values = await asyncio.gather(*[bar(i) for i in range(10)])
    print(values)

asyncio.run(main())

This method, gather, takes arbitrary number of args for the concurrent jobs instead of a list, so we unpack.

It's very common to need this intermediate value, values in my eg, instead of designing your function/method to have side effects.

James T.
  • 910
  • 1
  • 11
  • 24
3

As of Python 3.11:

A more modern way to create and run tasks concurrently and wait for their completion is asyncio.TaskGroup.

... although, I couldn't find any reason why TaskGroup should be preferred over gather

kym
  • 818
  • 7
  • 12
1

To make this work the only change needed is to use asyncio.as_completed() to create an iterator of coroutines (awaitables) you can use for the for-loop:

async def asyncio_all(aws):
    for future in asyncio.as_completed(aws)
        await future

When applied to your code:

import asyncio

async def bar(i):
  print('started', i)
  await asyncio.sleep(1)
  print('finished', i)

async def aio_all(seq):
  for f in asyncio.as_completed(seq):
    await f

async def main():
  await aio_all([bar(i) for i in range(10)])

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
started 1
started 0
started 2
started 6
started 8
started 7
started 5
started 9
started 4
started 3
finished 1
finished 0
finished 2
finished 6
finished 8
finished 7
finished 5
finished 9
finished 4
finished 3

This comes closest to JavaScript Promise.all. However, using asyncio.gather or asyncio.TaskGroup (from v3.11) seems to be the more Pythonic way of doing it.

Hans Bouwmeester
  • 1,121
  • 1
  • 17
  • 19