python - 如何从 asyncio 中的信号处理程序捕获自定义异常?

标签 python python-3.x python-asyncio uvloop

使用 asyncio 时,我在捕获从信号处理程序回调抛出的自定义异常时遇到问题。

如果我从下面的 do_io() 中抛出 ShutdownApp,我就能够在 run_app() 中正确捕获它。但是,当从 handle_sig() 引发异常时,我似乎无法捕获它。

Minimal, Reproducible Example使用 Python 3.8.5 进行测试:

import asyncio
from functools import partial
import os
import signal
from signal import Signals


class ShutdownApp(BaseException):
    pass


os.environ["PYTHONASYNCIODEBUG"] = "1"


class App:
    def __init__(self):
        self.loop = asyncio.get_event_loop()

    def _add_signal_handler(self, signal, handler):
        self.loop.add_signal_handler(signal, handler, signal)

    def setup_signals(self) -> None:
        self._add_signal_handler(signal.SIGINT, self.handle_sig)

    def handle_sig(self, signum):
        print(f"\npid: {os.getpid()}, Received signal: {Signals(signum).name}, raising error for exit")
        raise ShutdownApp("Exiting")

    async def do_io(self):
        print("io start. Press Ctrl+C now.")
        await asyncio.sleep(5)
        print("io end")

    def run_app(self):
        print("Starting Program")
        try:
            self.loop.run_until_complete(self.do_io())
        except ShutdownApp as e:
            print("ShutdownApp caught:", e)
            # TODO: do other shutdown related items
        except:
            print("Other error")
        finally:
            self.loop.close()


if __name__ == "__main__":
    my_app = App()
    my_app.setup_signals()
    my_app.run_app()
    print("Finished")

在异步 Debug模式下按CTRL+C(对于SIGINT)后的输出:

(env_aiohttp) anav@anav-pc:~/Downloads/test$ python test_asyncio_signal.py 
Starting Program
io start. Press Ctrl+C now.
^C
pid: 20359, Received signal: SIGINT, raising error for exit
Exception in callback App.handle_sig(<Signals.SIGINT: 2>)
handle: <Handle App.handle_sig(<Signals.SIGINT: 2>) created at /home/anav/miniconda3/envs/env_aiohttp/lib/python3.8/asyncio/unix_events.py:99>
source_traceback: Object created at (most recent call last):
  File "test_asyncio_signal.py", line 50, in <module>
    my_app.setup_signals()
  File "test_asyncio_signal.py", line 25, in setup_signals
    self._add_signal_handler(signal.SIGINT, self.handle_sig)
  File "test_asyncio_signal.py", line 22, in _add_signal_handler
    self.loop.add_signal_handler(signal, handler, signal)
  File "/home/anav/miniconda3/envs/env_aiohttp/lib/python3.8/asyncio/unix_events.py", line 99, in add_signal_handler
    handle = events.Handle(callback, args, self, None)
Traceback (most recent call last):
  File "/home/anav/miniconda3/envs/env_aiohttp/lib/python3.8/asyncio/events.py", line 81, in _run
    self._context.run(self._callback, *self._args)
  File "test_asyncio_signal.py", line 31, in handle_sig
    raise ShutdownApp("Exiting")
ShutdownApp: Exiting
io end
Finished

预期输出:

Starting Program
io start. Press Ctrl+C now.
^C
pid: 20359, Received signal: SIGINT, raising error for exit
ShutdownApp caught: Exiting
io end
Finished

是否可以从asyncio中的信号处理程序引发自定义异常?如果是这样,我如何正确捕获/排除它?

最佳答案

handle_sig 是一个回调,因此它直接在事件循环之外运行,并且其异常仅通过全局 Hook 报告给用户。如果您希望在程序的其他地方捕获那里引发的异常,则需要使用 future 将异常从 handle_sig 传输到您希望它注意到的位置。

要在顶层捕获异常,您可能需要引入另一个方法,我们将其称为 async_main(),它会等待任一 self. do_io() 或之前创建的 future 来完成:

    def __init__(self):
        self.loop = asyncio.get_event_loop()
        self.done_future = self.loop.create_future()

    async def async_main(self):
        # wait for do_io or done_future, whatever happens first
        io_task = asyncio.create_task(self.do_io())
        await asyncio.wait([self.done_future, io_task],
                           return_when=asyncio.FIRST_COMPLETED)
        if self.done_future.done():
            io_task.cancel()
            await self.done_future  # propagate the exception, if raised
        else:
            self.done_future.cancel()

要从handle_sig内部引发异常,您只需set the exception关于 future 的对象:

    def handle_sig(self, signum):
        print(f"\npid: {os.getpid()}, Received signal: {Signals(signum).name}, raising error for exit")
        self.done_future.set_exception(ShutdownApp("Exiting"))

最后,修改 run_app 以将 self.async_main() 传递给 run_until_complete,一切就完成了:

$ python3 x.py
Starting Program
io start. Press Ctrl+C now.
^C
pid: 2069230, Received signal: SIGINT, raising error for exit
ShutdownApp caught: Exiting
Finished

最后,请注意,可靠地捕获键盘中断是 notoriously tricky undertaking并且上述代码可能无法涵盖所有​​极端情况。

关于python - 如何从 asyncio 中的信号处理程序捕获自定义异常?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64227873/

相关文章:

python - 如何管理单个aiohttp.ClientSession?

python - 在 Python asyncio 中检测套接字 EOF

python - 使用 asyncio 有两个无限任务

python - 如果没有选择文件,如何知道FileChooserListView结构下的工作目录

python-3.x - 绘制具有不等长 x 轴和 y 轴的曲面图

python - 如何使用 python 查找簇?

python-3.x - 如何在 Python 中进行假设检验?

python - 将 <> 运算符转换为 python3

python - Pandas:使用 set_index() 将列设置为索引会创建一个子索引。为什么会发生这种情况以及如何消除它?

python - 是否有与 R 的 sample() 函数等效的 Python?