2015-06-10 6 views
29

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

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

+0

В случае это особенно важно, я бегу Python 3.4.3 на OS X 10.10.3. –

ответ

30

При 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 вызов добавил done_callback к tasks, который останавливает цикл. Итак, когда мы отменяем tasks, этот обратный вызов срабатывает, и цикл останавливается. В этот момент мы вызываем tasks.exception, чтобы избежать получения предупреждения об исключении исключения из _GatheringFuture.

+0

Ах, так что ничего не происходит с задачей за пределами цикла событий, даже не отмена, не так ли? Это звучит как простое правило, о котором нужно помнить. –

+0

Просто попробовал это, и он работает как рекламируемый. Милая! Замечание: Я также заметил, что если вы передадите 'return_exceptions = True' в' gather() ', вы можете оставить вызов' tasks.exception() ', поскольку исключения возвращаются в качестве результатов. –

+0

@NickChammas Правильно, цикл должен быть запущен, чтобы аннулирование вступило в силу [(как указано в документах)] (https://docs.python.org/3/library/asyncio-task.html#asyncio.Task .Отмена). И да, вообще ничего не произойдет с 'asyncio.Task', если цикл не будет активно управлять им. Использование 'return_exceptions = True' - хороший трюк, если вы в порядке с реальными исключениями (например, чем-то, кроме« CancelledError »), которые выбрасываются из ваших завернутых сопрограмм, которые на самом деле не возникают. – dano

2

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

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

+0

Можете ли вы прояснить, как это работает, и почему предпочтительнее просто захватить 'KeyboardInterrupt', как это было в моем примере? Прерывание, похоже, работает нормально. Проблема в том, что проблема с оставленными задачами. Это потому, что цикл событий не обрабатывает само прерывание? –

+0

Обычно, когда у вас есть цикл событий, вы должны обрабатывать все события в цикле событий. Я не могу сказать, почему у KeyboardInterrupt возникли проблемы. Подумайте, хотя это может прервать практически любой код, выполняемый в цикле событий (но я не могу сказать это точно, так как я не знаю деталей дизайна). –

+0

Как это работает, не описано в документах. Я полагаю, что это трюк с «собственной трубкой», вы должны заглянуть в источник Python, если хотите узнать. –

6

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

loop = asyncio.get_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: 
    loop.close() 

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

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