Asyncio.create_task выполняется даже без каких-либо ожиданий в программе

Это дополнительный вопрос к Что делает 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?

Кстати, я на самом деле сказал: «отправляет задачу в цикл событий и немедленно возвращает», а не запускается немедленно, что я считаю правильным. Интересное открытие, о котором я даже не подозревал!

2e0byo 10.06.2024 12:56

Отвечает ли это на ваш вопрос? Что на самом деле запускает задачи Asyncio?

jupiterbjy 11.06.2024 02:07

@2e0byo, спасибо. На самом деле я имел в виду короткий комментарий Пинчии по самому вопросу, а не ваш ответ. То, что вы написали, верно.

user118967 12.06.2024 01:09

@jupiterbjy, спасибо, это очень поучительный ответ, но я не думаю, что он решает одну из проблем, а именно то, что задачи все еще выполняются за один шаг до завершения программы.

user118967 12.06.2024 01:11

@jupiterbjy: ой, извините, подумав дальше, я думаю, что это действительно ответ, хотя, думаю, я все еще надеюсь понять, почему задачи отменяются только потому, что main уже вышел. Мне кажется интуитивно понятным, что цикл событий завершит выполнение всех ожидающих запланированных задач, а не отменит их. В конце концов, они были созданы и запланированы, поэтому понятно, что пользователь хочет, чтобы они были полностью выполнены.

user118967 12.06.2024 01:19

@user118967 обновленный ответ, в котором подробно описывается, включая исходный код asyncio, надеюсь, это поможет! Честно говоря, не уверен, стоит ли мне переместить этот раздел сюда или просто оставить его там, превратив его в стену текста.

jupiterbjy 12.06.2024 05:09
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
6
74
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Использование 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() закончится, а остальные задачи сразу же отменяются, не имея возможности запуститься ни разу.

user118967 13.06.2024 07:35

Другие вопросы по теме