Я знаю, что task.cancel() ничего не делает, кроме установки флага, говорящего, что задача должна быть отменена (здесь), так что на следующей итерации цикла событий CancelledError попадает в обернутую сопрограмму.
Также я знаю, что выбрасывание исключений в сопрограммы происходит только при вызовах операторов await. Сопрограмма должна быть в этом состоянии.
Теперь взгляните на это:
import asyncio
async def coro_1():
print("inside coro_1")
try:
await asyncio.sleep(0)
except asyncio.CancelledError:
print("cancellation error occurred")
raise
async def main():
print("inside main")
new_task = asyncio.create_task(coro_1())
print("finishing main")
asyncio.run(main())
выход:
inside main
finishing main
inside coro_1
cancellation error occurred
В main() сопрограмме я создал новую задачу, но не дал ей возможности запуститься. (ничего не ждал). Сопрограмма main() завершается, но выполняется coro_1!
Хорошо, я знаю, что это произошло, потому что я использовал asyncio.run(), но после того, как я просмотрел исходный код, я не мог понять, почему и где. Я должен быть определенно неправ в одном из следующих шагов.
когда задача coro_1() создается в main, она планирует новый обратный вызов (self.__step) в цикле событий для следующего цикла. (в очереди self._ready).
main завершен, выполнение переходит в метод __exit__ объекта Runner (здесь).
тогда .cancel()все оставшиеся задачи здесь. Теперь у нас есть наша coro_1 задача. Его флаг _must_cancel установлен, и его __step ожидает выполнения на следующей итерации с этим набором исключений.
Поскольку exc установлен, мы достигаем в этой строки:
...
else:
result = coro.throw(exc)
...
Теперь, как это возможно? наш coro_1 не в том состоянии, когда мы можем выдать ему исключение. Кто-то должен был выполнить его до строки await asyncio.sleep(0) в прошлом. кто и где?
Я использую Python 3.11.
Заранее спасибо.






Я нашел ответ.
Во-первых, в цикле событий есть очередь с именем self._ready(), в которой все обратные вызовы готовы к выполнению.
Вот шаги:
Когда мы запускаем нашу main сопрограмму с помощью .run(), она создает новую задачу, добавляет свой обратный вызов в self._ready, а затем вызывает run_until_complete(). Наш self._ready содержит только этот обратный вызов.
В run_until_complete он добавляет _run_until_complete_cb обратный вызов к add_done_callback будущего (это будет называться «будущее готово»). какова его работа? он останавливает цикл событий. в основном он устанавливает атрибут self._stopping цикла событий в True, который используется здесь.
Теперь он вызывает run_forever(), который действительно перебирает очередь self._ready и запускает все обратные вызовы. (сейчас у нас есть только одна main сопрограмма).
Обратите внимание, что обратные вызовы извлекаются из self._ready для вызова, поэтому self._ready пуст, когда выполнение достигает строки new_task = asyncio.create_task(coro_1()). Затем он добавляет новый обратный вызов к self._ready, который отвечает за запуск coro_1 сопрограммы.
main() достигает конца, и все готово. что будет дальше? пришло время запланировать его _run_until_complete_cb, который мы зарегистрировали ранее.
А что внутри нашего self._ready ? два обратных вызова, один для выполнения coro_1 и один для выполнения _run_until_complete_cb. Цикл еще не остановлен. Таким образом, запускается еще один цикл цикла. Он выполняет coro_1 до оператора await (теперь он готов к исключению CancelledError), а также выполняет _run_until_complete_cb. Этот последний обратный вызов останавливает цикл событий. что внутри нашего self._ready? только один обратный вызов, который отвечает за продолжение coro_1. (он запланировал себя обратно в цикл событий).
Теперь пришло время вызвать метод __exit__ объекта Runner. Что оно делает? это отменяет все оставшиеся задачи. наша единственная задача coro_1, которая находится на рассмотрении. Теперь в третьем цикле цикла событий он бросает CancelledError в coro_1.