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)
相关推荐
醒过来摸鱼6 小时前
Java classloader
java·开发语言·python
superman超哥6 小时前
仓颉语言中元组的使用:深度剖析与工程实践
c语言·开发语言·c++·python·仓颉
小鸡吃米…7 小时前
Python - 继承
开发语言·python
祁思妙想7 小时前
Python中的FastAPI框架的设计特点和性能优势
开发语言·python·fastapi
Dingdangcat867 小时前
反恐精英角色识别与定位-基于改进的boxinst_r101_fpn_ms-90k_coco模型实现
python
世界唯一最大变量7 小时前
利用自定义积分公式,目前可以求出所有1元方程和1元积分的近似值
python
写代码的【黑咖啡】8 小时前
深入理解 Python 中的模块(Module)
开发语言·python
爱笑的眼睛119 小时前
超越 `cross_val_score`:深度解析Scikit-learn交叉验证API的架构、技巧与陷阱
java·人工智能·python·ai
smj2302_7968265210 小时前
解决leetcode第3782题交替删除操作后最后剩下的整数
python·算法·leetcode
gCode Teacher 格码致知10 小时前
Python基础教学:Python 3中的字符串在解释运行时的内存编码表示-由Deepseek产生
python·内存编码