@3FANG

Как работает запуск корутин в asyncio?

Изучаю асинхронное программирование и возникли непонятки.

Есть корутина, симулирующая ввод данных:

import asyncio 
from time import time, sleep


async def waste_time():
    print("Start work...")
    await asyncio.sleep(2)
    print("End work!")

Запускать её мы будем в корутине main(), с которой у меня и возникает вопрос.
Ее описание будет ниже.

Запуск скрипта осуществляется следующим образом:

if __name__ == '__main__':
    start = time()
    asyncio.run(main())
    print(time() - start)

Я хочу запустить 10 функций waste_time().
Но я не могу понять, почему такой код выполняется как положено:

async def main():
    tasks = []

    for _ in range(10):
        task = asyncio.create_task(waste_time())
        tasks.append(task)

    for task in tasks:
        await task

$ python3 main.py
Start work...
Start work...
Start work...
Start work...
Start work...
Start work...
Start work...
Start work...
Start work...
Start work...
End work!
End work!
End work!
End work!
End work!
End work!
End work!
End work!
End work!
End work!
2.005629301071167

А такой - нет:

async def main():
    tasks = []

    for _ in range(10):
        task = asyncio.create_task(waste_time())
        tasks.append(task)
        await task

Start work...
End work!
Start work...
End work!
Start work...
End work!
Start work...
End work!
Start work...
End work!
Start work...
End work!
Start work...
End work!
Start work...
End work!
Start work...
End work!
Start work...
End work!
20.023030042648315
  • Вопрос задан
  • 183 просмотра
Пригласить эксперта
Ответы на вопрос 3
@kiriharu
Python backend, Linux enjoyer
При создании задачи при помощи asyncio.create_task она немедленно (при ближайшем переключении контекста, например при встрече await) начинает выполняться в цикле событий. Именно поэтому у тебя в первом примере сразу же стартанули все указанные задачи.

await же, указанный в твоем коде, позволяет ожидать завершения задачи. Поэтому в твоем коде второго примера ты создаешь задачу, ждешь её выполнения и только потом переходишь к следующей:

async def main():
    tasks = []

    for _ in range(10):
        task = asyncio.create_task(waste_time()) # создаем задачу
        tasks.append(task)
        await task # ожидаем выполнения
        # итерация завершена, переходим к следующей


Если хочется запустить сразу все задачи, то тут было бы правильнее воспользоваться asyncio.gather, которая как раз будет ожидать выполнения всех задач:

import asyncio 
from time import time, sleep


async def waste_time():
    print("Start work...")
    await asyncio.sleep(2)
    print("End work!")

async def main():
    tasks = []
    for _ in range(10):
        task = asyncio.create_task(waste_time())
        tasks.append(task)
    await asyncio.gather(*tasks)

asyncio.run(main())
Ответ написан
@Everything_is_bad
Потому что, в первом случае ты сначала все запустил, а потом ожидаешь выполнение, а во втором, ты ожидаешь выполнения после каждого конкретно запуска, тут даже create_task не нужен, можно тупо await waste_time()
Ответ написан
Vindicar
@Vindicar
RTFM!
3FANG,
насчёт твоего второго вопроса: всё предельно просто. Ты запустил 10 копий корутины, они все выполнили print('start') и ушли в спячку на две секунды - практически одновременно!
Точнее, первая корутина встала в очередь, create_task() закончила свою работу и вернула управление в твой цикл запуска корутин. И так 10 раз. Все корутины стоят в очереди плотно друг за другом.
Затем ты входишь в цикл ожидания корутин, пишешь в консоль "старт", дожидаешься первой корутины, при этом main() встаёт в очередь на вызов. Первая корутина запустилась, написала start, и ушла в ожидание. Следующая в очереди готова вторая корутина. Она запустилась, написала start, ушла в ожидание. И так далее. В конце main() снова оказывается в начале очереди - но она ждёт первую корутину, она ещё не готова выполняться. Она отправляется в конец очереди. Поэтому программа ждёт первую корутину, но ожидание-то тикает у всех!
Тогда первая корутина отработала - но прежде чем ты вернёшься из await-вызова в main(), сначала успеют отработать всё стоящие в очереди корутины, потому что их время тоже подошло, и они стоят в очереди раньше! И ты видишь последовательность start-finish внутри корутин.
Затем очередь вызова доходит до main() и ты видишь "финиш" ожидания первой корутины. Ты начинаешь ждать вторую (старт) - но она уже отработала, поэтому main() единственная в очереди, и она тут же получает управление обратно (финиш). И так с оставшимися корутинами.

Если ты сделаешь так, чтобы каждая следующая копия корутины ждала дольше предыдущей, то ситуация будет интереснее:
код
import asyncio 
from time import time, sleep


async def waste_time(i, delay):
    print(f"Start work {i}...")
    await asyncio.sleep(delay)
    print(f"End work {i}!")


async def main():
    tasks = []

    for i in range(10):
        task = asyncio.create_task(waste_time(i+1, (i+1)*1.0))
        tasks.append(task)

    for i, task in enumerate(tasks, 1):
        print(f'start wait {i}')
        await task
        print(f'end wait{i}')

if __name__ == '__main__':
    start = time()
    asyncio.run(main())
    print(time() - start)

вывод
start wait 1
Start work 1...
Start work 2...
Start work 3...
Start work 4...
Start work 5...
Start work 6...
Start work 7...
Start work 8...
Start work 9...
Start work 10...
End work 1!
end wait1
start wait 2
End work 2!
end wait2
start wait 3
End work 3!
end wait3
start wait 4
End work 4!
end wait4
start wait 5
End work 5!
end wait5
start wait 6
End work 6!
end wait6
start wait 7
End work 7!
end wait7
start wait 8
End work 8!
end wait8
start wait 9
End work 9!
end wait9
start wait 10
End work 10!
end wait10
10.012470483779907
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы