Это дополнительный вопрос к Что делает asyncio.create_task()?
Там, как и в других местах, asyncio.create_task(c)
описывается как «немедленный» запуск сопрограммы c
по сравнению с простым вызовом сопрограммы, которая затем выполняется только при ее await
вызове.
Это имеет смысл, если вы интерпретируете «немедленно» как «без необходимости await
», но на самом деле созданная задача не выполняется до тех пор, пока мы не запустим некоторые await
(возможно, для других сопрограмм) (в исходном вопросе slow_coro
начала выполняться только тогда, когда мы await fast_coro
).
Однако, если мы вообще не запускаем await
, задачи всё равно выполняются (только один шаг, не до завершения) в конце программы:
import asyncio
async def counter_loop(x, n):
for i in range(1, n + 1):
print(f"Counter {x}: {i}")
await asyncio.sleep(0.5)
return f"Finished {x} in {n}"
async def main():
slow_task = asyncio.create_task(counter_loop("Slow", 4))
fast_coro = asyncio.create_task(counter_loop("Fast", 2))
print("Created tasks")
for _ in range(1000):
pass
print("main ended")
asyncio.run(main())
print("program ended")
результат
Created tasks
main ended
Counter Slow: 1
Counter Fast: 1
program ended
Мне интересно: почему две созданные задачи вообще выполняются, если нигде не было запуска await
?
Отвечает ли это на ваш вопрос? Что на самом деле запускает задачи Asyncio?
@2e0byo, спасибо. На самом деле я имел в виду короткий комментарий Пинчии по самому вопросу, а не ваш ответ. То, что вы написали, верно.
@jupiterbjy, спасибо, это очень поучительный ответ, но я не думаю, что он решает одну из проблем, а именно то, что задачи все еще выполняются за один шаг до завершения программы.
@jupiterbjy: ой, извините, подумав дальше, я думаю, что это действительно ответ, хотя, думаю, я все еще надеюсь понять, почему задачи отменяются только потому, что main
уже вышел. Мне кажется интуитивно понятным, что цикл событий завершит выполнение всех ожидающих запланированных задач, а не отменит их. В конце концов, они были созданы и запланированы, поэтому понятно, что пользователь хочет, чтобы они были полностью выполнены.
@user118967 обновленный ответ, в котором подробно описывается, включая исходный код asyncio, надеюсь, это поможет! Честно говоря, не уверен, стоит ли мне переместить этот раздел сюда или просто оставить его там, превратив его в стену текста.
Использование create_task также добавляет его в цикл событий. Даже после выхода из основного они все еще здесь и активны.
Вам необходимо активно отменить их перед выходом main
или установить барьер ожидания в начале асинхронной функции.
Цикл не знает, что код скоро завершится и будет выполняться так, как запланировано:
Обратите внимание, что отмененные задачи все равно будут выполняться один раз последовательно до первого барьера ожидания (см.: base_events.py выполняется из asycio.run_forevever).
Ожидание задачи позволяет коду await... выполняться дальше (если это возможно).
Попробуем выразить это такими словами:
цикл событий приступает к выполнению с одной задачей, которую необходимо выполнить: main()
.
Когда функция main() завершена, есть еще две задачи, готовые к обработке, поэтому перед возвратом к вызывающему объекту main()
они выполняются до следующего оператора await
внутри каждой из них. (либо await
, либо async for
, либо async with
).
На следующей итерации цикла, поскольку основная задача завершена, цикл отменяет оставшиеся задачи и завершает работу. Это происходит потому, что сигнал цикла о завершении «основной» задачи — это обратный вызов, который устанавливается, когда loop.run_until_complete
(вызываемый asyncio.run
) выполнен: именно этот обратный вызов сигнализирует о том, что цикл должен остановиться. Но сам обратный вызов будет выполнен только на следующей итерации цикла, после завершения сопрограммы main
. И итерация цикла, хотя и получает пометку о том, что цикл asyncio должен остановиться на этом, фактически завершается только после однократного выполнения всех ожидающих задач — это подразумевает продвижение каждой созданной задачи к следующей точке await
.
Это делается путем добавления asyncio.CancelledError
в код задач в операторе ожидания. Таким образом, если у вас есть предложение try/except/finally
, включающее await
, вы все равно можете очистить свою задачу до завершения цикла.
Все это не документировано на «английском» языке — это скорее текущее поведение кода в реализации asyncio. Необходимо следовать коду на asyncio/base_events.py файл, чтобы понять это.
Спасибо! Последующий вопрос: был бы более интуитивным и естественным дизайн, если бы обратный вызов, о котором вы говорите, запускался сразу после завершения main
? Этого можно было бы достичь, если бы run
сразу после вызова обернул сопрограмму c
, которую она получает (здесь, main()
), в более крупную подпрограмму C
, которая вызывает c
и обратный вызов: ``` def C(): c() callback() ` `` Тогда обратный вызов выполняется, как только main()
закончится, а остальные задачи сразу же отменяются, не имея возможности запуститься ни разу.
Кстати, я на самом деле сказал: «отправляет задачу в цикл событий и немедленно возвращает», а не запускается немедленно, что я считаю правильным. Интересное открытие, о котором я даже не подозревал!