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)
相关推荐
充值修改昵称1 小时前
数据结构基础:B树磁盘IO优化的数据结构艺术
数据结构·b树·python·算法
C系语言1 小时前
python用pip生成requirements.txt
开发语言·python·pip
william_djj1 小时前
python3.8 提取xlsx表格内容填入单个文件
windows·python·xlsx
kszlgy6 小时前
Day 52 神经网络调参指南
python
wrj的博客7 小时前
python环境安装
python·学习·环境配置
Pyeako8 小时前
深度学习--BP神经网络&梯度下降&损失函数
人工智能·python·深度学习·bp神经网络·损失函数·梯度下降·正则化惩罚
摘星编程9 小时前
OpenHarmony环境下React Native:Geolocation地理围栏
python
充值修改昵称9 小时前
数据结构基础:从二叉树到多叉树数据结构进阶
数据结构·python·算法
q_354888515311 小时前
AI大模型:python新能源汽车推荐系统 协同过滤推荐算法 Echarts可视化 Django框架 大数据毕业设计(源码+文档)✅
大数据·人工智能·python·机器学习·信息可视化·汽车·推荐算法
Yeats_Liao11 小时前
开源生态资源:昇腾社区ModelZoo与DeepSeek的最佳实践路径
python·深度学习·神经网络·架构·开源