【问题标题】:How to catch custom exception from signal handler in asyncio?如何从异步信号处理程序中捕获自定义异常?
【发布时间】:2020-10-06 14:27:13
【问题描述】:

我在使用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")

在异步调试模式下按下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 中的信号处理程序引发自定义异常?如果是这样,我该如何正确捕获/排除它?

【问题讨论】:

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


    【解决方案1】:

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

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

        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

        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,上面的代码可能无法涵盖所有​​极端情况。

    【讨论】:

      【解决方案2】:

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

      对上述问题的回应

      自定义异常实现:

      class RecipeNotValidError(Exception):    
          def __init__(self):       
              self.message = "Your recipe is not valid"        
              try:            
                  raise RecipeNotValidError
              except RecipeNotValidError as e:            
                  print(e.message)
      

      在方法handle_sig中,添加try和except块。此外,您可以在自定义异常中自定义消息。

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

      回复您的第二个问题: 是否可以从异步信号处理程序中引发自定义异常?如果是这样,我如何正确捕获/排除它?

      Asyncio 中的内置错误处理。如需更多文档,请访问https://docs.python.org/3/library/asyncio-exceptions.html

      
      import asyncio
      async def f():
          try:
              while True: await asyncio.sleep(0)
          except asyncio.CancelledError:  
              print('I was cancelled!')  
          else:
              return 111
      
      

      【讨论】:

      • 感谢您查看我的问题!尽管您的两种解决方案都可以捕获异常,但它们并没有解决我在原始问题中的担忧。 1. 我不想在handle_sig 中尝试/除外。我希望handle_sig 简单地引发异常,并让它在run_app 中被捕获 2. 异步代码 sn-p 的内置错误处理确实解决了引发自定义异常的问题
      • 1.我认为在您定义的这种情况下,您是否尝试过在自定义异常中编写 ToDo 代码,而不是在 run_app 中处理。因为run_app的Except块只有在run_app的Try里面执行代码时出现异常才会执行。 2.您可以尝试使用现有的内置异常,因为我没有看到在Asyncio中执行自定义异常的范围。
      猜你喜欢
      • 1970-01-01
      • 2018-05-30
      • 2013-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多