python代码主动kill父线程或其他线程, 使其抛异常结束的黑科技

对于python主动结束掉一个线程, 或者说kill一个线程, 一直没有找到好方法, 导致测试脚本经常出现一个线程异常结束, 整个测试本身可以结束的情况下, 因为无法控制主线程退出, 导致本身5分钟可以结束的用例, 有时会跑几个小时!

今天又遇到了这个问题, 测试脚本在运行, 我看日志明知道已经有bug, 可以结束测试了, 不想让它再继续循环几十分钟去检查, 因此在搜索解决方法的时候, 偶然间看到了一篇文章, 用其中的方法, 加上一些gdb使用技巧, 成功的让正在运行的python进程中的测试线程抛异常结束掉, 并继续往下执行测试. 文档链接参见我总结的这个文档: juejin.cn/post/734936...

在解决掉这个问题之后,就想要把这个步骤也融入到我们的框架代码中,后续就可以通过调用函数来做到了,但是使用文档中的方法,也遇到了和评论区同样的问题:

Linux端不生效,我用的python是3.8版本的,接下来开始分析一下问题

首先,我对Thread继承写了一个子类MyThread,封装了一个静态函数,给MyThread加一个is_kill_parent属性,来决定子线程异常退出后,是否要kill掉父线程。

测试代码如下

test.py 复制代码
import time
import threading
import traceback
import ctypes


class MyThread(threading.Thread):

    def __init__(self, *args, is_kill_parent=False, **kwargs):
        super().__init__(*args, **kwargs)
        self.parent_thread = threading.current_thread()
        print(f'parent_thread_id: {self.parent_thread.ident}')

        self.is_kill_parent = is_kill_parent

    @staticmethod
    def kill_thread(thread: threading.Thread):
        ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, ctypes.py_object(Exception))

    def run(self):
        try:
            super().run()
        except Exception:
            print(traceback.format_exc())
            if self.is_kill_parent:
                self.kill_thread(self.parent_thread)


def run(times):
    for i in range(times):
        print(i)
        time.sleep(1)
    raise RuntimeError("aaa")


if __name__ == '__main__':
    t = MyThread(target=run, args=(5,), is_kill_parent=True)
    t.start()
    for i in range(60):
        print("main")
        time.sleep(1)

该方法在windows上执行生效,效果如下:

shell 复制代码
parent_thread_id: 15444
0main
1main
main
2
main3
main4
main
Traceback (most recent call last):
  File "<input>", line 22, in run
  File "C:\Users\zk\AppData\Local\Programs\Python\Python38-32\lib\threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "<input>", line 33, in run
RuntimeError: aaa
Traceback (most recent call last):
  File "<input>", line 41, in <module>
Exception

子线程结束后,kill了父线程,main不再打印,符合预期。

但是,这段代码在linux上不生效:

可以看到没有把父线程kill掉,main继续打印,接下来分析原因: 首先,我本地有一份编译好的编译时启用了--with-pydebug选项的python3.8,用它gdb来断点看一下问题出在什么地方

可以看到,thread id传入PyThreadState_SetAsyncExc方法时,id已经不一样了,应该是因为这个原因所以不生效了。 看一下id情况:

python 复制代码
>>> print(hex(140737354004288))
0x7ffff7fdf740
>>> print(hex(18446744073575200576))
0xfffffffff7fdf740

可以看到,后面的7fdf740是一致的,前面的乱套了,接下来再分析为什么id乱了的原因,查看python源码PyThreadState_SetAsyncExc函数的定义为(顺带一提,后续测试看了一下ctypes.pythonapi.PyThreadState_SetAsyncExc函数的返回值,在windows上生效的情况下是1,在linux上不生效的情况下是0,也和注释中的返回值描述对的上,0代表找不到线程id):

arduino 复制代码
/* Asynchronously raise an exception in a thread.
   Requested by Just van Rossum and Alex Martelli.
   To prevent naive misuse, you must write your own extension
   to call this, or use ctypes.  Must be called with the GIL held.
   Returns the number of tstates modified (normally 1, but 0 if `id` didn't
   match any known thread id).  Can be called with exc=NULL to clear an
   existing async exception.  This raises no exceptions. */

int
PyThreadState_SetAsyncExc(unsigned long id, PyObject *exc)
{
    _PyRuntimeState *runtime = &_PyRuntime;

根据之前的经验,ctypes在进行接口调用的时候,如果是int对象,会默认按c_int类型去传参,导致如果是指针类型(8字节)的int值,会丢失4个字节的精度。看上面linux线程id是6个字节,也到了丢失精度的情况,因此把kill_thread函数调用改为

less 复制代码
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(thread.ident), ctypes.py_object(Exception))

在thread.ident的基础上,用ctypes.c_ulong()来转换成一个unsigned long类型的值,试一下效果:

大功告成!

贴上最终的代码供大家参考:

python 复制代码
import time
import threading
import traceback
import ctypes


class MyThread(threading.Thread):

    def __init__(self, *args, is_kill_parent=False, **kwargs):
        super().__init__(*args, **kwargs)
        self.parent_thread = threading.current_thread()
        print(f'parent_thread_id: {self.parent_thread.ident}')

        self.is_kill_parent = is_kill_parent

    @staticmethod
    def kill_thread(thread: threading.Thread):
        print(f"kill父线程结果为: {ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(thread.ident), ctypes.py_object(Exception))}")

    def run(self):
        try:
            super().run()
        except Exception:
            print(traceback.format_exc())
            if self.is_kill_parent:
                self.kill_thread(self.parent_thread)


def run(times):
    for i in range(times):
        print(i)
        time.sleep(1)
    raise RuntimeError("aaa")


if __name__ == '__main__':
    t = MyThread(target=run, args=(5,), is_kill_parent=True)
    t.start()
    for i in range(60):
        print("main")
        time.sleep(1)
相关推荐
装不满的克莱因瓶18 分钟前
【工业领域】了解目标检测基本流程——从数据到部署的完整工程化思路
人工智能·python·深度学习·机器学习·计算机视觉·目标跟踪·工业领域
叫我:松哥19 分钟前
基于Python flask的中学可控智能命题系统设计与实现,整合遗传算法、DeepSeek 大模型及数据库技术构建一体化应用
数据库·人工智能·python·算法·机器学习·flask·遗传算法
在放️21 分钟前
Python 练习题讲解 2 · 循环计算
开发语言·python
装不满的克莱因瓶22 分钟前
【工业领域】了解目标检测评估指标——从mAP到IoU的完整评价体系解析
人工智能·pytorch·python·深度学习·目标检测·计算机视觉·目标跟踪
m沐沐24 分钟前
【计算机视觉】OpenCV 模板匹配银行卡数字识别---下
人工智能·python·opencv·计算机视觉·pycharm·numpy
遇见小修修27 分钟前
选择正规上门修电脑服务,有哪些通用标准和判断方法?
python
财经资讯数据_灵砚智能39 分钟前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年6月16日
人工智能·python·ai·信息可视化·自然语言处理·ai编程·灵砚智能
闵孚龙9 小时前
动态图机制:为什么 PyTorch 调试起来更舒服
人工智能·pytorch·python
chushiyunen9 小时前
langchain4j笔记、tools
笔记·python·flask
程序员三藏10 小时前
Web自动化测试详解
自动化测试·软件测试·python·selenium·测试工具·职场和发展·测试用例