Qt调用python重复运行系统崩溃,使用多线程控制GIL后python函数内部不执行的问题

最近用在Qt里调用python,遇到了很多问题,记录一下。
一开始是直接普通的调用流程:

void testPython(){    
    // 连接python
    // 初始化python解释器
    Py_Initialize();
    
    PyObject * pModule = NULL;
    PyObject * pFunc = NULL;

    // 设置python文件路径
    PyRun_SimpleString("import sys");
    PyRun_SimpleString("sys.path.append('C:/postgraduate/LVPredictor-software/LVPredictor/')"); // 设置绝对路径
    
    pModule = PyImport_ImportModule("predictModel");//调用的Python文件名  py文件放置exe同级

    if (pModule == NULL)
    {
        PyErr_Print();
        cout << "PyImport_ImportModule Fail!" << endl;
        return;
    }

    // 设置调用的函数名
    pFunc = PyObject_GetAttrString(pModule, "predict");

    // 转换输入数据,将字典输入
    PyObject* dict = PyDict_New();
    for(auto data : inputData){
        PyDict_SetItem(dict, Py_BuildValue("s", data.first.c_str()), Py_BuildValue("d", data.second)); // 这里要注意"s"表示c类型的字符串,所以要加c_str()否则报错
    }
    
    // 创建参数对象
    PyObject* args = PyTuple_New(1);
    PyTuple_SetItem(args, 0, dict); // 0表示数据dict在参数args中的位置索引

    PyObject *pReturn = NULL; //返回值
    pReturn = PyEval_CallObject(pFunc, args); //调用函数

    // 转换返回值格式
    double re;
    PyArg_Parse(pReturn, "d", &re);
    cout<<"result: "<<re<<endl;

    Py_Finalize();//调用Py_Finalize,和Py_Initialize相对应的.

}

运行程序后发现,这个函数只能调用一次,再次重复执行的时候就会让系统崩溃。在网上查了很多资料后,说是由于Python解释器有全局解释锁GIL,导致在同一时刻只能有一个线程拥有解释器,所以在C++多线程调用python脚本时,需要控制GIL,线程获取GIL。所以一个程序里,无论有多少条线程调用python,python只能初始化一次。每次线程要调用python时,都要拥有GIL。

然后参考了以下两个博客:
https://blog.csdn.net/t15281180631/article/details/118942080
https://blog.csdn.net/weixin_42837024/article/details/100023253

对代码做了修改。
(1)新建一个PyThreadStateLock头文件

#ifndef PYTHREADSTATELOCK_H
#define PYTHREADSTATELOCK_H

//将全局解释器锁和线程的相关操作用类封装
#include "Python.h"

class PyThreadStateLock
{
    public:
        PyThreadStateLock(void)
        {
            _save = nullptr;
            nStatus = 0;
            nStatus = PyGILState_Check();   //检测当前线程是否拥有GIL
            PyGILState_STATE gstate;
            if (!nStatus)
            {
                gstate = PyGILState_Ensure();   //如果没有GIL,则申请获取GIL
                nStatus = 1;
            }
            _save = PyEval_SaveThread();
            PyEval_RestoreThread(_save);
        }
        ~PyThreadStateLock(void)
        {
            _save = PyEval_SaveThread();
            PyEval_RestoreThread(_save);
            if (nStatus)
            {
                PyGILState_Release(gstate);    //释放当前线程的GIL
            }
        }
    
    private:
        PyGILState_STATE gstate;
        PyThreadState *_save;
        int nStatus;
};

#endif // PYTHREADSTATELOCK_H

使用上面这一段代码,发现调用一次python执行完成之后总是报错“Fatal Python error: PyEval_SaveThread: NULL tstate”。我不知道是什么原因,就换成了下面这一段代码:

#ifndef PYTHREADSTATELOCK_H
#define PYTHREADSTATELOCK_H

//将全局解释器锁和线程的相关操作用类封装
#include "Python.h"

class PyThreadStateLock
{
	public:
	    PyThreadStateLock(void){
	        state = PyGILState_Ensure();
	    }
	    ~PyThreadStateLock(void){
	        PyGILState_Release(state);
	    }
	private:
	    PyGILState_STATE state;

};

#endif // PYTHREADSTATELOCK_H

(2)在需要调用python的代码文件里,创建一个初始化python解释器的函数,在界面初始化的时候调用一下这个函数,我是直接在构造函数里调用的。

void PythonInit()
{
    if (!Py_IsInitialized())
    {
        //1.初始化Python解释器,这是调用操作的第一步
        Py_Initialize();
        if (!Py_IsInitialized()) {
            qDebug("Initial Python failed!");
//            emit failed();
        }
        else {

            //执行单句Python语句,用于给出调用模块的路径,否则将无法找到相应的调用模块
			// 初始化线程支持
			PyEval_InitThreads();
			
            PyRun_SimpleString("import sys");
            PyRun_SimpleString("sys.path.append('C:/postgraduate/LVPredictor-software/LVPredictor/')"); // 设置绝对路径

            // 启动子线程前执行,为了释放PyEval_InitThreads获得的全局锁,否则子线程可能无法获取到全局锁。
            PyEval_ReleaseThread(PyThreadState_Get());
            qDebug("Initial Python Success!");

        }
    }

}

(3)在需要调用python的地方添加一句“class PyThreadStateLock PyThreadLock;”:

void testPython(){
	class PyThreadStateLock PyThreadLock;//获取全局锁
    PyObject * pModule = NULL;
    PyObject * pFunc = NULL;

    pModule = PyImport_ImportModule("predictModel");//调用的Python文件名  py文件放置exe同级

    if (pModule == NULL)
    {
        PyErr_Print();
        cout << "PyImport_ImportModule Fail!" << endl;
        return;
    }

    // 设置调用的函数名
    pFunc = PyObject_GetAttrString(pModule, "predict");

    // 转换输入数据,将字典输入
    PyObject* dict = PyDict_New();
    for(auto data : inputData){
        PyDict_SetItem(dict, Py_BuildValue("s", data.first.c_str()), Py_BuildValue("d", data.second)); // 这里要注意"s"表示c类型的字符串,所以要加c_str()否则报错
    }

    // 创建参数对象
    PyObject* args = PyTuple_New(1);
    PyTuple_SetItem(args, 0, dict); // 0表示数据dict在参数args中的位置索引

    PyObject *pReturn = NULL; //返回值
    pReturn = PyEval_CallObject(pFunc, args); //调用函数

    // 转换返回值格式
    double re;
    PyArg_Parse(pReturn, "d", &re);
    cout<<"result: "<<re<<endl;


    // Py_Finalize();//注意这一句不能要,否则会运行崩溃
}

以上三步就可以了。
但是我发现我还出了一个问题,我的python代码是这样的:

# 传入参数data是一个字典
def predict(data):
    x = pd.DataFrame(columns=["ID_1", "ID_2", "ID_3"])
    x = x.append(data, ignore_index=True)
    print(x)
    print('python used!')
    return 100

我在Qt里调用这个函数,发现Qt里能够获取到返回值,但是控制台却没有打印x和"python used!“。很奇怪。我以为是python函数里的语句没有执行,在网上查了很多也没查到什么解决方案。于是我将最后一句返回语句换成” return x.iloc[0][“ID_1”] ",即返回x的第一行第一列的数据(是由Qt传到python的数据),发现是能够准确返回到

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值