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)
相关推荐
不去幼儿园5 小时前
【启发式算法】狼群算法(Wolf Pack Algorithm, WPA)算法详细介绍(Python)
python·算法·启发式算法·任务分配·集群智能
傻啦嘿哟5 小时前
告别爬取困境:用Playwright完美抓取复杂动态网页
python
做科研的周师兄5 小时前
【机器学习入门】9.2:感知机 Python 实践代码模板(苹果香蕉分类任务适配)
人工智能·python·学习·机器学习·分类·数据挖掘·numpy
java1234_小锋5 小时前
PyTorch2 Python深度学习 - 数据集与数据加载
开发语言·python·深度学习·pytorch2
小兔崽子去哪了6 小时前
数据结构和算法(Python)
数据结构·python
永远有缘6 小时前
四种编程语言常用函数对比表
java·开发语言·c++·python
Pocker_Spades_A6 小时前
Python快速入门专业版(五十三):Python程序调试进阶:PyCharm调试工具(可视化断点与变量监控)
开发语言·python·pycharm
stayhungry_c6 小时前
Quarto生成PDF无法正常显示中文的问题
python·macos·r语言·pdf
程序员小远6 小时前
selenium元素定位---(元素点击交互异常)解决方法
自动化测试·软件测试·python·selenium·测试工具·测试用例·交互
ColderYY6 小时前
DrissionPage自动化
python·自动化