Тупик, когда QThread пытается получить Python GIL через PyGILState_Ensure()

У меня есть приложение C++/Qt, в которое я хочу встроить интерпретатор Python. Я хочу вызвать Python из QThread, но у меня возникает тупиковая ситуация в строке, где я вызываю PyGILState_Ensure(), чтобы попытаться получить глобальную блокировку интерпретатора (GIL).

Ниже я приведу минимальный и простой пример, который следует рекомендациям, данным здесь:

//main.cpp:
#include <QCoreApplication>
#include <QThread>
#include "Worker.h"

void startThread()
{
    QThread* thread = new QThread;
    Worker* worker = new Worker();
    worker->moveToThread(thread);
    QObject::connect(thread, SIGNAL(started()), worker, SLOT(process()));
    QObject::connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
    QObject::connect(worker, SIGNAL(finished()), worker, SLOT(deleteLater()));
    QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
    thread->start();
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Py_Initialize();
    startThread();
    Py_FinalizeEx();
    return a.exec();
}


//Worker.h:
#ifndef WORKER_H
#define WORKER_H

#include <QObject>
#include "Python.h"

class Worker : public QObject
{
    Q_OBJECT
public:
    explicit Worker(QObject *parent = nullptr) : QObject(parent) {}

Q_SIGNALS:
    void finished();

public Q_SLOTS:
    void process()
    {
        qDebug("Calling Python");
        PyGILState_STATE gstate = PyGILState_Ensure();
        PyRun_SimpleString("print(\"hello\")");
        PyGILState_Release(gstate);
        qDebug("Done calling Python");
        Q_EMIT finished();
    }
};

#endif // WORKER_H

Некоторые дополнительные комментарии:

  • Примечание. Файл .pro содержит строку CONFIG += no_keywords, чтобы избежать конфликтов имен с заголовком Python.
  • Следует отметить, что хотя поток останавливает выполнение при вызове PyGILState_Ensure(), основной поток продолжает работать без ограничений. Если я изменю return a.exec(); на return 0;, программа завершится. (Поэтому, возможно, тупик — неправильный термин для использования.)
  • Обратите внимание, что я не заинтересован в создании потоков из Python. Я просто хочу напрямую вызвать данный скрипт Python из QThread.
  • Я читал другие подобные questions, но чувствовал, что рассмотренные там случаи немного отличаются, и я не смог решить свою проблему из даны там ответы. Кроме того, меня смущают рекомендации вызывать PyEval_InitThreads(), что, если я понимаю документация по Python/C API не нужна.

person andreasdr    schedule 28.12.2017    source источник
comment
Как понять, что это мертвый замок? Когда приложение зависает, используйте отладчик для проверки всех стеков вызовов потоков.   -  person Marek R    schedule 28.12.2017
comment
Если я остановлю отладчик на строке, где вызывается PyGILState_Ensure(), и оттуда попытаюсь перейти на одну строку вперед, программа заблокируется. т.е. поток застревает где-то внутри PyGILState_Ensure().   -  person andreasdr    schedule 28.12.2017


Ответы (1)


После просмотра SO я нашел решение. Пример 1 в этом ответе был особенно полезен.

Что мне нужно сделать, так это вызвать PyEval_InitThreads() из основного потока (совсем непонятно из очень загадочная документация). Затем, чтобы позволить PyGILState_Ensure() вообще получить GIL из других потоков (или иначе застрять в бесконечном цикле в исходном коде Python, постоянно пытаясь и безуспешно пытаясь получить GIL), мне нужно освободить GIL в основном потоке через звонок PyEval_SaveThread(). Наконец, я должен снова получить GIL в основном потоке через вызов PyEval_RestoreThread() (убедившись, что все потоки, которые хотят вызвать PyGILState_Ensure(), определенно завершены до этого, иначе риск снова получить блокировку по той же причине, что и раньше).

Вот обновленная версия main.cpp, которая решает проблему:

#include <QCoreApplication>
#include <QThread>
#include "Worker.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Py_Initialize();

    //Initialize threads and release GIL:
    PyEval_InitThreads();
    PyThreadState *threadState;
    threadState = PyEval_SaveThread();

    QThread* thread = new QThread;
    Worker* worker = new Worker();
    worker->moveToThread(thread);
    QObject::connect(thread, SIGNAL(started()), worker, SLOT(process()));
    QObject::connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
    QObject::connect(worker, SIGNAL(finished()), worker, SLOT(deleteLater()));
    QObject::connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
    thread->start();

    //wait until thread is finished calling Python code:
    thread->wait(1000); //(ugly, but in a proper Qt application, this would be handled differently..)

    //Retrieve GIL again and clean up:
    PyEval_RestoreThread(threadState);
    Py_FinalizeEx();

    return a.exec();
}
person andreasdr    schedule 04.01.2018