在PySide中多线程Boost Python C++代码

4

我有一段C++代码,正打算使用Boost Python对其进行封装。我的想法是,创建一个共享对象,让我的Python GUI可以使用它来实例化变量,进而调用C++功能。

这段C++代码涉及到一些繁重的操作,我希望能够让封装好的对象并发运行,以避免GUI被阻塞。

我使用CMake编译了一个Boost Python共享对象:

find_package(Boost COMPONENTS system thread python REQUIRED)
find_package(PythonLibs REQUIRED)
include_directories(${Boost_INCLUDE_DIRS})
include_directories(${PYTHON_INCLUDE_DIRS})
link_directories(${Boost_LIBRARY_DIRS})
link_directories(${PYTHON_LIBRARIES})
set(Boost_USE_MULTITHREADED ON)
add_library(mynewlibinpython SHARED src/boost_python_wrapper_code.cpp)
target_link_libraries(mynewlibinpython ${Boost_LIBRARIES} ${PYTHON_LIBRARIES} mylibincpp)

我可以在Python中实例化对象,但它会导致GUI阻塞。以下是我的Python代码的一般片段:

from mynewlibinpython import *
class CppWrapper(QtCore.QObject):
    def __init__(self):
         super(CppWrapper,self).__init__()
         #some more initialization...

    @QtCore.Slot()
    def heavy_lifting_cpp_invocation():
         #constructor that takes a long time to complete
         self.cpp_object = CppObject()

在具有成员函数的对象中,当我在GUI中按下按钮时触发该函数:

class GuiObjectWithUnimportantName:
     def __init__(self):
          #inititalization

          self.cpp_thread = QThread()
          self.cpp_wrapper = CppWrapper()

     def function_triggered_by_button_press(self):
          self.cpp_wrapper.moveToThread(self.cpp_thread)
          self.cpp_thread.started.connect(self.cpp_wrapper.heavy_lifting_cpp_invocation)
          print "starting thread"       
          self.cpp_thread.start()
          print "Thread started"      

因此,cpp代码将开始执行其任务,我也会立即从两个print语句("starting thread"和"Thread started")中获得输出。

但GUI被冻结了。这与Python GIL有关吗?我尝试将这些宏添加到boost_python_wrapper_code.cpp中并重新编译,但导致Python GUI在启动时立即崩溃。

1个回答

3

是的,我相信你的问题与全局解释器锁(GIL)有关。以下是可能发生的情况的简单示例:

from PyQt4 import QtCore, QtGui
import time

class Window(QtGui.QDialog):

    def __init__(self):
        super(Window, self).__init__()
        self.resize(200,100)

        layout = QtGui.QVBoxLayout(self)

        layout.addWidget(QtGui.QLineEdit())

        self.button1 = QtGui.QPushButton("Sleep")
        self.button1.clicked.connect(self.go_safe)
        layout.addWidget(self.button1)

        self.button2 = QtGui.QPushButton("Crunch")
        self.button2.clicked.connect(self.go_GIL_buster)
        layout.addWidget(self.button2)

    def go_safe(self):
        t = QtCore.QThread(self)
        heavy = Heavy()
        heavy.moveToThread(t)
        t.started.connect(heavy.safe)
        print "starting thread"
        t.start()
        print "thread started"
        self.t1 = t
        self.heavy1 = heavy

    def go_GIL_buster(self):
        t = QtCore.QThread(self)
        heavy = Heavy()
        heavy.moveToThread(t)
        t.started.connect(heavy.GIL_buster)
        print "starting thread"
        t.start()
        print "thread started"
        self.t2 = t
        self.heavy2 = heavy

class Heavy(QtCore.QObject):

    def safe(self):
        print "Sleeping"
        time.sleep(10)
        print "Done"

    def GIL_buster(self):
        print "Crunching numbers"
        x = 2
        y = 500000000
        foo = x**y
        print "Done"


if __name__ == "__main__":
    app = QtGui.QApplication([])
    win = Window()
    win.show()
    win.raise_()
    app.exec_()

当您单击第一个按钮时,线程将休眠10秒钟,您会注意到仍然可以在文本字段中输入。 休眠会释放GIL,因此主线程可以继续工作。
当您单击第二个按钮时,线程执行繁重的数字操作。 这会在整个时间内获取GIL,并且GUI将锁定直到完成为止。
如果您的boost扩展在任何方式上都不使用python对象,则认为可以释放GIL以进行其操作。 然后它就不会阻塞GUI。 如果您正在对Python对象进行繁重的操作,则可能会遇到问题。
有一个关于创建基于范围的GIL释放类的提示,您可以在任何函数中使用该类以在该函数的持续时间内释放GIL(当助手实例被销毁时)。
class ScopedGILRelease
{
// C & D ----------------------------
public:
    inline ScopedGILRelease()
    {
        m_thread_state = PyEval_SaveThread();
    }

    inline ~ScopedGILRelease()
    {
        PyEval_RestoreThread(m_thread_state);
        m_thread_state = NULL;
    }

private:
    PyThreadState * m_thread_state;
};

And using it...

int foo_wrapper(int x)
{
    ScopedGILRelease scoped;
    return foo(x);
}

这个技巧在这个仓库中还有更多的使用示例:https://bitbucket.org/wwitzel3/code/src/tip/nogil/nogil.cpp


好的,如果我理解正确的话,你的解决方案涉及在.cpp包装器源代码周围使用Py_BEGIN_ALLOW_THREADSPy_END_ALLOW_THREADS,或者在.cpp原始源代码周围使用(例如在重载函数实现周围)? - eqzx
无论在boost中获取和释放gil的约定是什么,只要代码不修改Python对象,你就可以在该函数周围释放gil。 - jdi
感谢提供 GIL 释放类的链接。它完美地运行了。 - eqzx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接