Как изящно завершить асинхронный скрипт с помощью Ctrl-C?

Я прочитал все сообщения, которые я мог найти о том, как изящно обрабатывать скрипт с асинхронным циклом событий, который завершается с помощью Ctrl-C, и я не смог заставить ни один из них работать без печати одной или нескольких трассировок, поскольку я Сделай так. Ответы почти повсюду, и я не смог реализовать ни один из них в этом маленьком скрипте:

import asyncio
import datetime
import functools
import signal


async def display_date(loop):
    end_time = loop.time() + 5.0
    while True:
        print(datetime.datetime.now())
        if (loop.time() + 1.0) >= end_time:
            break
        await asyncio.sleep(1)


def stopper(signame, loop):
    print("Got %s, stopping..." % signame)
    loop.stop()


loop = asyncio.get_event_loop()
for signame in ('SIGINT', 'SIGTERM'):
    loop.add_signal_handler(getattr(signal, signame), functools.partial(stopper, signame, loop))

loop.run_until_complete(display_date(loop))
loop.close()

Я хочу, чтобы сценарий завершился без печати каких-либо трассировок после Ctrl-C (или SIGTERM/SIGINT, отправленных через kill). Этот код печатает RuntimeError: Event loop stopped before Future completed. В МНОГИХ других формах, которые я пробовал на основе предыдущих ответов, я получил множество других типов классов исключений и сообщений об ошибках, не зная, как их исправить. Приведенный выше код сейчас минимален, но некоторые из попыток, которые я предпринял ранее, были совсем другими, и ни одна из них не была правильной.

Если вы можете изменить сценарий так, чтобы он завершался корректно, мы будем весьма признательны за объяснение того, почему ваш способ сделать это правильным способом.


person JK Laiho    schedule 01.02.2018    source источник


Ответы (2)


Остановка цикла событий во время его работы никогда не будет допустимой.

Здесь вам нужно поймать Ctrl-C, чтобы указать Python, что вы хотите справиться с этим самостоятельно, а не отображать трассировку стека по умолчанию. Это можно сделать с помощью классической попытки/за исключением:

coro = display_date(loop)
try:
    loop.run_until_complete(coro)
except KeyboardInterrupt:
    print("Received exit, exiting")

И, для вашего варианта использования, это все! Для более реальной программы вам, вероятно, потребуется очистить некоторые ресурсы. См. также Мягкое завершение сопрограмм asyncio

person Arthur    schedule 01.02.2018
comment
Спасибо. В качестве разъяснения для всех остальных читателей, мне пришлось удалить весь цикл добавления обработчика сигнала for, а последний loop.close() также был ненужным. - person JK Laiho; 02.02.2018
comment
Не могу поверить, сколько времени я потратил впустую, пытаясь заставить add_signal_handler(SIGINT) работать; спасибо за указание на гораздо более простой путь, который действительно работает. - person Mike C; 29.10.2019
comment
Мне также пришлось добавить RuntimeError в исключение, потому что я получал RuntimeError: Event loop stopped before Future completed. - person Leonardo Rick; 10.05.2021

Используйте обработчики signal:

import asyncio
from signal import SIGINT, SIGTERM

async def main_coro():
    try:
        await awaitable()
    except asyncio.CancelledError:
        do_cleanup()

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    main_task = asyncio.ensure_future(main_coro())
    for signal in [SIGINT, SIGTERM]:
        loop.add_signal_handler(signal, main_task.cancel)
    try:
        loop.run_until_complete(main_task)
    finally:
        loop.close()
person ManuelSchneid3r    schedule 13.11.2019
comment
Если вы пишете только сопрограмму, а не основной код, вы можете использовать asyncio.get_running_loop() и asyncio.current_task() для получения цикла и задачи соответственно, таким образом я добавил обработчики сигналов в сопрограмме (позволяя вызывать ее с помощью asyncio.run()) - person Scott Stevens; 21.01.2021
comment
asyncio.add_signal_handler() на win32 вызывает NotImplementedError (python 3.7.3). - person JimB; 05.02.2021