Почему `asyncio.run ()` никогда не возвращается в Python 3.8? - PullRequest
3 голосов
/ 11 февраля 2020

Мое приложение имеет следующий код верхнего уровня (немного сокращенно):

def main():
    # Some setup code ...
    try:
        asyncio.run(my_coroutine())
    except Exception as e:
        print("Exiting due to exception {}: {}".format(type(e).__name__, e))
    print("Coroutine finished")
    # Some cleanup code ...
    print("Shutdown complete")

В Python 3.8 asyncio.run никогда не завершается, поэтому код очистки не запускается (и текст завершения отключен не напечатано). Когда приложение должно завершиться, оно просто зависает навсегда. В Python 3.7 все работало нормально. Python 3.6 не имел asyncio.run, но довольно схожий код с loop.run_until_complete() и loop.close() также работал.

Некоторый дополнительный контекст: код установки и очистки запускается и изящно выходит из созданного рабочего потока с threading.Thread. Именно этот поток фактически останавливает основную сопрограмму: он отменяет сопрограмму (технически это отменяет другую сопрограмму, которая вызывается в my_coroutine()), используя loop.call_soon_threadsafe

1 Ответ

4 голосов
/ 11 февраля 2020

Суть вопроса неверна. (Я знал это, когда писал, но не тогда, когда у меня изначально была проблема, поэтому я оставил ее в этой форме для других с такой же проблемой.) asyncio.run() заканчивается , но перепрыгивает через все исключения код обработки и завершения работы:

def main():
    # Some setup code ...
    try:
        asyncio.run(my_coroutine())
    except Exception as e:
        # <---- In Python 3.7, control passed to here when asyncio.run() finshed
        print("Exiting due to exception {}: {}".format(type(e).__name__, e))
    print("Coroutine finished")
    # Some cleanup code ...
    print("Shutdown complete")
    # <---- In Python 3.8 it directly dropped out of here!

Это связано с тем, что в Python 3.8 базовый класс asyncio.CancelledError изменился с Exception на BaseException (т.е. базовый класс Exception). Это было сделано, чтобы избежать ошибок в асин c коде, когда люди ловят Exception, думая, что это означало, что какая-то операция завершилась неудачно (например, ошибка сети), но случайно предотвратила отмену; см. Python выпуск 32528 .

Приложению не удалось завершиться, поскольку Python ожидает завершения всех потоков до sh, если они не запущены с daemon=False, переданными в Thread конструктор (см. Обсуждение нескольких абзацев в Документы по потокам ). В моем случае поток не является потоком демона (потому что я действительно хочу, чтобы он завершился sh изящно), но он не завершится, пока я не запросил его, что делается в коде, в котором я добавляю комментарий "Некоторый код очистки ... ".

Решение состоит в том, чтобы либо явно отлавливать asyncio.CancelledError в дополнение к Exception, либо использовать вместо него блок finally:. Блок finally:, вероятно, лучше, поскольку он действительно гарантирует выполнение кода, даже несмотря на другие исключения, полученные из BaseException, такие как KeyboardInterrupt:

def main():
    # Some setup code ...
    try:
        asyncio.run(my_coroutine())
    finally:
        print("Coroutine finished")
        # Some cleanup code ...
        print("Shutdown complete")
...