c++qtcpython

Integrated CPython in my Qt program, the result of multi-threaded execution is abnormal


// Automatically fetched and automatically released when out of scope
class PyGILState
{
public:
    PyGILState()
        : gstate(PyGILState_Ensure())
    {
    }

    ~PyGILState()
    {
        PyGILState_Release(gstate);
    }

    PyGILState(const PyGILState&) = delete;
    PyGILState& operator=(const PyGILState&) = delete;

    PyGILState_STATE gstate;
};

// The class that manages Cpython is moved to a child thread
class PythonThread : public QObject {
    Q_OBJECT
public:
    PythonThread(QObject *parent = nullptr) : QObject(parent) {}

    ~PythonThread() { }

    PyObject* module;

    void newPythonEnv(){
        qDebug() << "newPythonEnv thread id : " << QThread::currentThreadId();

        Py_SetPythonHome(L"D://python");  // Set your Python installation path
        Py_SetPath(L"D://python//Lib;D://python//DLLs;D://python;");

        Py_Initialize();
        PyEval_InitThreads();  // Initialize thread support

        PyRun_SimpleString("import sys");
        PyRun_SimpleString("sys.path.append('./script')");

        module = PyImport_ImportModule("test1");  //Import Python script modules
    }

public slots:
    // start state machine
    void start() {
        qDebug() << "State machine start...";
        PyGILState pyGILState;

        PyObject* pFunc = PyObject_GetAttrString(module, "state_machine");

        if (pFunc && PyCallable_Check(pFunc)) {
            PyObject_CallObject(pFunc, nullptr);
        } else {
            PyErr_Print();
            std::cerr << "Failed to call state_machine function" << std::endl;
        }
        qDebug() << "State machine thread end...";
    }

    // stop state machine
    void stop() {
        qDebug() << "State machine stop...";
        PyGILState pyGILState;

        PyObject *globals_dict = PyModule_GetDict(module);
        if (globals_dict) {
            PyDict_SetItemString(globals_dict, "isRuning", Py_False);
            qDebug() << PyLong_AsLong(PyDict_GetItemString(globals_dict, "isRuning"));

        } else {
            qDebug() << "Failed to get globals_dict";
        }
        qDebug() << "State machine stopped";
    }
}

//MainWindow
class MainWindow : public QWidget {
    Q_OBJECT
public:
    MainWindow() {
        pythonThread = new PythonThread;

        thread = new QThread;
        pythonThread->moveToThread(thread);
        thread->start();

        connect(this, &MainWindow::start_thread, pythonThread, &PythonThread::start);
        connect(this, &MainWindow::new_python_env, pythonThread, &PythonThread::newPythonEnv);

        qDebug() << "Main thread id: " << QThread::currentThreadId();
        emit new_python_env();

        QVBoxLayout* layout = new QVBoxLayout(this);
        QPushButton* startButton = new QPushButton("Start State Machine");
        QPushButton* stopButton = new QPushButton("Stop State Machine");

        layout->addWidget(startButton);
        layout->addWidget(stopButton);


        connect(startButton, &QPushButton::clicked, this, &MainWindow::startStateMachine);
        connect(stopButton, &QPushButton::clicked, this, &MainWindow::stopStateMachine);

        setLayout(layout);
        setWindowTitle("Python State Machine Controller");
    }

signals:
    void start_thread();
    void new_python_env();

public slots:
    void startStateMachine() {
        emit start_thread();
    }

    void stopStateMachine() {
        pythonThread->stop();
    }


private:
    PythonThread* pythonThread;
    QThread *thread;
};

Python scripts Code

isRuning = True 

def state_machine():
    global isRuning
    print('exec ... state machine ',flush=True)
    isRuning = True
    isPause = False
    while isRuning:
        print(f"state machine runing ...  {isRuning}",flush=True)
        time.sleep(0.1)
        while isPause == True : 
            print('wait continue ...', flush=True)
            # time.sleep(1)
            if isRuning == False : 
                return
    isRuning = True

This is the minimal implementation that can reproduce the problem

About Console Output :

Flow 1: Click the start state machine button, and a few seconds later click the stop state machine button

The logs are as follows :

Main thread id:  0x399c
newPythonEnv thread id :  0x658
State machine start...
exec ... state machine 
state machine runing ...  True
state machine runing ...  True
state machine runing ...  True
state machine runing ...  True
state machine runing ...  True
State machine stop...
0
State machine stopped
State machine thread end...

Flow 2: Just click the stop state machine button after the software starts

The logs are as follows :

Main thread id:  0x4b20
newPythonEnv thread id :  0x1070
State machine stop...

(The UI won't work after that)

Below the image where the UI is not working: it's getting stuck

What I tried:

I tried to create a python parser on the main thread, but it still didn't work.

Expected results:

I expect the Start State Machine button to start the python State Machine and the Stop State Machine button to end the python state machine. Yes, in my minimal implementation it works if I click the buttons in order. But if I just clicked the Stop State Machine button it would get stuck.


Solution

  • you are clearly hanging while trying to acquire the GIL.

    After creating the python environment with Py_Initialize you need to release the GIL with a call to PyEval_SaveThread just before you return from newPythonEnv, otherwise the GIL will never be released.

    When the worker is running it is releasing the GIL from that thread, if the worker never ran then the GIL will never be released, and the main thread trying to acquire it is a deadlock.

    Note: you also need to acquire the GIL then call Py_Finalize in the destructor otherwise you are leaking memory.