在未捕获的主线程异常上优雅地退出子线程

问题描述 投票:0回答:1

我的工作线程设置如下:

from time import sleep
from threading import Event, Thread


class MyThread(Thread):
    
    def __init__(self, *args, **kwargs):
        # Following Doug Fort: "Terminating a Thread"
        # (https://www.oreilly.com/library/view/python-cookbook/0596001673/ch06s03.html)
        self._stop_request = Event()
        super().__init__(*args, **kwargs)
    
    def run(self):
        while not self._stop_request.is_set():
            print("My thread is running")
            sleep(.1)
        print("My thread is about to stop")  # Finish my thread's job
        
    def join(self, *args, **kwargs):
        self._stop_request.set()
        super().join(*args, **kwargs)
            
            
if __name__ == "__main__":
    my_thread = MyThread()
    my_thread.start()
    sleep(2)
    raise RuntimeError("Something went wrong!")

通过这个,我想实现以下目标:一旦主线程中发生任何未捕获的异常(如最后一行故意的

RuntimeError
),工作线程应该“完成其工作”(即运行行打印“我的线程即将停止”),然后也退出。

在实践中,会发生以下情况:

  • 在 Linux 终端(Debian WSL 上的 Python 3.5)上,这可以按预期工作。
  • 但是,在 Windows PowerShell 或命令提示符(Windows 10 上的 Python 3.7)上, 工作线程继续运行,永远不会退出其
    while
    循环。什么是 更糟糕的是,提示对键盘中断没有反应,所以我必须 强行关闭提示窗口。

使用

my_thread = MyThread(daemon=True)
似乎没有提供解决方案,因为它立即强制关闭工作线程,而不让它完成其工作。因此,Windows 上唯一的工作版本似乎是:一旦工作线程启动,将其他所有内容包装到一个
try–except
块中,因此:

if __name__ == "__main__":
    my_thread = MyThread()
    my_thread.start()
    try:
        sleep(2)
        raise RuntimeError("Something went wrong!")
    except:
        my_thread.join()

然而,这看起来有点笨拙。另外,我不明白为什么它只在 Windows 上是必要的。我错过了什么吗?有更好的解决办法吗?

编辑: 在非 WSL Linux(Ubuntu 20.04 上的 Python 3.9)上,我遇到了与 Windows 下类似的行为;也就是说,工作线程在

RuntimeError
之后继续 - 但至少我可以在这里使用键盘中断。所以,这似乎不是仅限 Windows 的行为,但可能暗示我的期望是错误的(毕竟,没有人在原始设置中明确调用
my_thread.join()
,那么为什么要设置它的
_stop_request
呢? )。但我的根本问题仍然是一样的:如何让工作线程正常退出,如上所述?

python multithreading exception
1个回答
1
投票

最初提出的解决方案

我似乎找到了一个独立于系统的解决方案。但还是感觉有点笨拙:

import sys
from time import sleep
from threading import Event, Thread


# Monkey-patch ``sys.excepthook`` to set a flag for notifying child threads
# about exceptions in the main thread
exception_raised_in_main_thread = Event()

def my_excepthook(type_, value, traceback):
    exception_raised_in_main_thread.set()
    sys.__excepthook__(type_, value, traceback)

sys.excepthook = my_excepthook


class MyThread(Thread):
    
    def run(self):
        while not exception_raised_in_main_thread.is_set():
            print("My thread is running")
            sleep(.1)
        print("My thread is about to stop")
        
            
if __name__ == "__main__":
    my_thread = MyThread()
    my_thread.start()
    sleep(2)
    raise RuntimeError("Something went wrong!")

通过修补

sys.excepthook
(根据 文档,似乎没有误用 - 引用“可以通过将另一个三参数函数分配给
sys.excepthook
来自定义此类顶级异常的处理”),我现在将设置一个事件,
exception_raised_in_main_thread
,通知所有子线程 关于主线程中发生的任何未捕获的异常。

中间解决方案

我在问题中提出的内容和我最初提出的答案之间也存在某种“中间立场解决方案”:(1) 提供全局

exception_raised_in_main_thread
事件,(2) 将所有
__main__
代码包围起来一个
try-except
块并设置异常情况下的事件;因此:

from time import sleep
from threading import Event, Thread


exception_raised_in_main_thread = Event()


class MyThread(Thread):
    
    def run(self):
        while not exception_raised_in_main_thread.is_set():
            print("My thread is running")
            sleep(.1)
        print("My thread is about to stop")
        
            
if __name__ == "__main__":
    try:
        my_thread = MyThread()
        my_thread.start()
        sleep(2)
        raise RuntimeError("Something went wrong!")
    except:
        exception_raised_in_main_thread.set()
  • 与最初提出的解决方案(上图)相比,我看到的好处是:无需修补
    sys.excepthook
    ;因此不存在干扰其他潜在补丁的风险。
  • 与问题中提到的解决方案相比,我看到的好处是:现在仍然由工作线程注册到提供的事件,而不是显式加入每个单独的工作线程(可能有多个) ,所以代码仍然相对干净。

注释

在上面的代码片段中,我删除了另一个事件 (

self._stop_request
)为了简洁起见,但它仍然可能出现 在正常情况下可以方便地终止工作线程。

© www.soinside.com 2019 - 2024. All rights reserved.