Подтвердить что ты не робот

Каков правильный способ очистки после прерывания цикла событий?

У меня есть цикл событий, который запускает некоторые совлокальные подпрограммы как часть инструмента командной строки. Пользователь может прервать инструмент с помощью обычного Ctrl + C, после чего я хочу правильно очистить его после прерывания цикла событий.

Вот что я пробовал.

import asyncio


@asyncio.coroutine
def shleepy_time(seconds):
    print("Shleeping for {s} seconds...".format(s=seconds))
    yield from asyncio.sleep(seconds)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()

    # Side note: Apparently, async() will be deprecated in 3.4.4.
    # See: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
    tasks = [
        asyncio.async(shleepy_time(seconds=5)),
        asyncio.async(shleepy_time(seconds=10))
    ]

    try:
        loop.run_until_complete(asyncio.gather(*tasks))
    except KeyboardInterrupt as e:
        print("Caught keyboard interrupt. Canceling tasks...")

        # This doesn't seem to be the correct solution.
        for t in tasks:
            t.cancel()
    finally:
        loop.close()

Запуск этого и нажатие Ctrl + C дает:

$ python3 asyncio-keyboardinterrupt-example.py 
Shleeping for 5 seconds...
Shleeping for 10 seconds...
^CCaught keyboard interrupt. Canceling tasks...
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(1)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(0)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>

Понятно, что я правильно не очистился. Я думал, что, возможно, вызов cancel() для задач будет способом сделать это.

Каков правильный способ очистки после прерывания цикла событий?

4b9b3361

Ответ 1

Когда вы CTRL + C, цикл события останавливается, поэтому ваши вызовы t.cancel() фактически не вступают в силу. Чтобы задачи были отменены, вам нужно снова запустить цикл.

Вот как вы можете справиться с этим:

import asyncio

@asyncio.coroutine
def shleepy_time(seconds):
    print("Shleeping for {s} seconds...".format(s=seconds))
    yield from asyncio.sleep(seconds)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()

    # Side note: Apparently, async() will be deprecated in 3.4.4.
    # See: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
    tasks = asyncio.gather(
        asyncio.async(shleepy_time(seconds=5)),
        asyncio.async(shleepy_time(seconds=10))
    )

    try:
        loop.run_until_complete(tasks)
    except KeyboardInterrupt as e:
        print("Caught keyboard interrupt. Canceling tasks...")
        tasks.cancel()
        loop.run_forever()
        tasks.exception()
    finally:
        loop.close()

Как только мы поймаем KeyboardInterrupt, мы вызываем tasks.cancel(), а затем снова запускаем loop. run_forever фактически выйдет, как только tasks будет отменен (обратите внимание, что отмена Future, возвращаемого asyncio.gather, также отменяет все Futures внутри него), потому что прерванный вызов loop.run_until_complete добавил a done_callback до tasks, который останавливает цикл. Итак, когда мы отменяем tasks, этот обратный вызов срабатывает, и цикл останавливается. В этот момент мы вызываем tasks.exception, чтобы избежать предупреждения об исключении исключения из _GatheringFuture.

Ответ 2

Обновлен для Python 3. 6+: добавлен вызов loop.shutdown_asyncgens чтобы избежать утечек памяти асинхронными генераторами, которые использовались не полностью. Кроме asyncio.new_event_loop, теперь используется asyncio.get_event_loop а не asyncio.get_event_loop чтобы гарантировать, что окончательный вызов loop.close не будет мешать другим возможным loop.close.

Следующее решение, основанное на некоторых других ответах, должно работать почти во всех случаях и не зависит от того, как вы вручную отслеживаете задачи, которые необходимо очистить с помощью Ctrl + C:

loop = asyncio.new_event_loop()
try:
    # Here 'amain(loop)' is the core coroutine that may spawn any
    # number of tasks
    sys.exit(loop.run_until_complete(amain(loop)))
except KeyboardInterrupt:
    # Optionally show a message if the shutdown may take a while
    print("Attempting graceful shutdown, press Ctrl+C again to exit…", flush=True)

    # Do not show 'asyncio.CancelledError' exceptions during shutdown
    # (a lot of these may be generated, skip this if you prefer to see them)
    def shutdown_exception_handler(loop, context):
        if "exception" not in context \
        or not isinstance(context["exception"], asyncio.CancelledError):
            loop.default_exception_handler(context)
    loop.set_exception_handler(shutdown_exception_handler)

    # Handle shutdown gracefully by waiting for all tasks to be cancelled
    tasks = asyncio.gather(*asyncio.Task.all_tasks(loop=loop), loop=loop, return_exceptions=True)
    tasks.add_done_callback(lambda t: loop.stop())
    tasks.cancel()

    # Keep the event loop running until it is either destroyed or all
    # tasks have really terminated
    while not tasks.done() and not loop.is_closed():
        loop.run_forever()
finally:
    if hasattr(loop, "shutdown_asyncgens"):  # This check is only needed for Python 3.5 and below
        loop.run_until_complete(loop.shutdown_asyncgens())
    loop.close()

Приведенный выше код получит все asyncio.Task.all_tasks задачи из цикла событий, используя asyncio.Task.all_tasks и разместит их в одном объединенном будущем, используя asyncio.gather. Все задачи в этом будущем (которые являются в настоящий момент выполняемыми задачами) затем отменяются с помощью метода future .cancel(). В этом случае return_exceptions=True гарантирует, что все полученные исключения asyncio.CancelledError будут сохранены, а не будут ошибаться в будущем.

Приведенный выше код также переопределит обработчик исключений по умолчанию, чтобы сгенерированные исключения asyncio.CancelledError не регистрировались.

Ответ 3

Если вы не находитесь в Windows, настройте обработчики сигналов на основе цикла событий для SIGINT (а также SIGTERM, чтобы вы могли запускать его как службу). В этих обработчиках вы можете немедленно выйти из цикла событий или инициировать некоторую последовательность очистки и выйти позже.

Пример в официальной документации Python: https://docs.python.org/3.4/library/asyncio-eventloop.html#set-signal-handlers-for-sigint-and-sigterm