Python 什么';在中断事件循环后,正确的清理方法是什么?

Python 什么';在中断事件循环后,正确的清理方法是什么?,python,python-3.4,python-asyncio,Python,Python 3.4,Python Asyncio,我有一个事件循环,它作为命令行工具的一部分运行一些co例程。用户可以使用通常的Ctrl+C中断工具,此时我希望在中断的事件循环之后正确地清理工具 这是我试过的 导入异步IO @异步协同程序 def shleepy_时间(秒): 打印(“刷新{s}秒…”。格式(s=秒)) 异步睡眠的产量(秒) 如果uuuu name uuuuuu='\uuuuuuu main\uuuuuuu': loop=asyncio.get\u event\u loop() #旁注:显然,async()在3.4.4中会被弃用

我有一个事件循环,它作为命令行工具的一部分运行一些co例程。用户可以使用通常的Ctrl+C中断工具,此时我希望在中断的事件循环之后正确地清理工具

这是我试过的

导入异步IO
@异步协同程序
def shleepy_时间(秒):
打印(“刷新{s}秒…”。格式(s=秒))
异步睡眠的产量(秒)
如果uuuu name uuuuuu='\uuuuuuu main\uuuuuuu':
loop=asyncio.get\u event\u loop()
#旁注:显然,async()在3.4.4中会被弃用。
#见:https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
任务=[
asyncio.async(shleepy_时间(秒=5)),
asyncio.async(shleepy_时间(秒=10))
]
尝试:
循环。运行_直到_完成(asyncio.gather(*任务))
键盘中断除外,如e:
打印(“捕捉到键盘中断。取消任务…”)
#这似乎不是正确的解决方案。
对于任务中的t:
t、 取消
最后:
loop.close()
运行此命令并按Ctrl+C组合键可生成:

$python3 asyncio-keyboardinterrupt-example.py
小睡5秒钟。。。
小睡10秒钟。。。
^键盘中断。正在取消任务。。。
任务已被销毁,但它处于挂起状态!
任务:
任务已被销毁,但它处于挂起状态!
任务:
显然,我没有正确地清理。我想对任务调用
cancel()


在中断事件循环后,正确的清理方法是什么?

除非您在Windows上,否则请为SIGINT(以及SIGTERM)设置基于事件循环的信号处理程序,以便将其作为服务运行。在这些处理程序中,您可以立即退出事件循环,也可以启动某种清理序列并稍后退出


官方Python文档中的示例:

当您按住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()

一旦捕捉到
键盘中断
,我们调用
任务。取消()
,然后再次启动
循环<代码>永远运行
实际上会在
任务
被取消时退出(请注意,取消
asyncio.gather
返回的
未来
也会取消其中的所有
未来
),因为中断的
循环.run\u until\u complete
调用向
任务添加了一个
done\u回调
,从而停止循环。因此,当我们取消
任务
时,会触发回调,循环停止。此时我们调用
tasks.exception
,只是为了避免从
\u GatheringFuture
Python 3.7+注意:下面的代码现在作为标准库
asyncio.run
函数的一部分实现–将下面的代码替换为
sys.exit(loop.run>)(amain(loop))
一旦您准备好升级!(如果您想打印消息,只需将
try…除了
-子句以外的内容移动到
amain

为Python 3.6+更新了:添加对
循环的调用。关闭\u asyncgens
以避免未完全使用的异步生成器发生内存泄漏

受其他一些答案的启发,以下解决方案几乎适用于所有情况,并且不依赖于您手动跟踪需要在Ctrl+C上清理的任务:

上面的代码将使用
asyncio.Task从事件循环中获取所有当前任务。所有_任务
并使用
asyncio.gather
将它们放在一个组合的未来中。然后使用未来的
取消()
方法。
return\u exceptions=True
然后确保存储所有接收到的
asyncio.CancelledError
异常,而不是导致未来出错

上述代码还将覆盖默认异常处理程序,以防止记录生成的
asyncio.canceledError
异常


从2020-12-17更新:删除了Python 3.5的兼容代码。

Python 3.7+中,建议您使用启动异步主函数

asyncio.run
将负责为您的程序创建事件循环,并确保当主功能退出时(包括由于
键盘中断
异常),事件循环已关闭,所有任务均已清除

它大致类似于以下内容(请参阅):

def运行(coro,*,debug=False):
“`asyncio.run`在Python 3.7中是新的”
loop=asyncio.get\u event\u loop()
尝试:
loop.set_调试(调试)
返回循环。运行直到完成(coro)
最后:
尝试:
所有_任务=asyncio.gather(*asyncio.all_任务(循环),返回_异常=True)
所有_任务。取消()
使用contextlib.suppress(asyncio.CancelleError):
循环。运行\u直到\u完成(所有\u任务)
loop.run_直到_完成(loop.shutdown_asyncgens())
最后:
loop.close()

使用
信号
模块在
信号.SIGINT
信号(Ctrl+C)上设置
异步IO.Event
,可以清楚地告诉所有异步代码自然停止。这一点尤其重要,因为一些库,如
aiohttp

下面是一个使用
aiohttp
库的示例。这里有一个
asyncio.sleep(5)
来防止连接返回池,让用户有机会按住ctrl+c键并模拟键盘中断
异常

示例代码:

导入日志
导入异步
输入s
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()
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.run_until_complete(loop.shutdown_asyncgens())
    loop.close()
> python C:\Users\mark\Temp\test_aiohttp.py
2021-03-06 22:21:08,684 MainThread root       INFO    : making http request
2021-03-06 22:21:09,132 MainThread root       INFO    : get data: `{'value': '500'}`
Traceback (most recent call last):
  File "C:\Users\auror\Temp\test_aiohttp.py", line 52, in <module>
    asyncio.run(run())
  File "c:\python39\lib\asyncio\runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "c:\python39\lib\asyncio\base_events.py", line 629, in run_until_complete
    self.run_forever()
  File "c:\python39\lib\asyncio\windows_events.py", line 316, in run_forever
    super().run_forever()
  File "c:\python39\lib\asyncio\base_events.py", line 596, in run_forever
    self._run_once()
  File "c:\python39\lib\asyncio\base_events.py", line 1854, in _run_once
    event_list = self._selector.select(timeout)
  File "c:\python39\lib\asyncio\windows_events.py", line 434, in select
    self._poll(timeout)
  File "c:\python39\lib\asyncio\windows_events.py", line 783, in _poll
    status = _overlapped.GetQueuedCompletionStatus(self._iocp, ms)
KeyboardInterrupt
Exception ignored in: <function _ProactorBasePipeTransport.__del__ at 0x000001CFFD75BB80>
Traceback (most recent call last):
  File "c:\python39\lib\asyncio\proactor_events.py", line 116, in __del__
    self.close()
  File "c:\python39\lib\asyncio\proactor_events.py", line 108, in close
    self._loop.call_soon(self._call_connection_lost, None)
  File "c:\python39\lib\asyncio\base_events.py", line 746, in call_soon
    self._check_closed()
  File "c:\python39\lib\asyncio\base_events.py", line 510, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

> python C:\Users\mark\Temp\test_aiohttp.py
2021-03-06 22:20:29,656 MainThread root       INFO    : making http request
2021-03-06 22:20:30,106 MainThread root       INFO    : get data: `{'value': '367'}`
2021-03-06 22:20:35,122 MainThread root       INFO    : making http request
2021-03-06 22:20:35,863 MainThread root       INFO    : get data: `{'value': '489'}`
2021-03-06 22:20:38,695 MainThread root       INFO    : SIGINT caught!
2021-03-06 22:20:40,867 MainThread root       INFO    : stop event was set, sleeping to let aiohttp close it's connections
2021-03-06 22:20:40,962 MainThread root       INFO    : sleep finished, returning