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)
相关推荐
游客5205 分钟前
自动化办公|xlwings生成图表
python·自动化
ylfhpy13 分钟前
Python常见面试题的详解16
开发语言·python·面试
蹦蹦跳跳真可爱58914 分钟前
Python----PyQt开发(PyQt高级:手搓一个音乐播放器)
python·pyqt
高力士等十万人16 分钟前
OpenCV对比度增强
人工智能·python·opencv
宝哥的菜鸟之路24 分钟前
Python 数据分析概述 ①
开发语言·python·数据分析
全栈若城25 分钟前
03 Python字符串与基础操作详解
java·开发语言·python
干饭高手1 小时前
Day9,Hot100(图论)
python·leetcode·图论
honghongstand1 小时前
代码随想录D52-53 图论 Python
开发语言·python·图论
程序媛-徐师姐1 小时前
基于 Python Django 的校园互助平台(附源码,文档)
开发语言·python·django·校园互助·校园互助平台
大数据追光猿2 小时前
【深度学习】Pytorch项目实战-基于协同过滤实现物品推荐系统
人工智能·pytorch·python·深度学习·ai编程·推荐算法