Python、线程、GIL 与 C++

2 投票
3 回答
5695 浏览
提问于 2025-04-15 17:12

有没有办法让boost::python在每次与Python交互时都控制Python的全局解释器锁(GIL)?

我正在用boost::python写一个项目。我想为一个外部库写一个C++的封装,并用Python脚本来控制这个C++库。我不能修改外部库,只能修改我的封装程序。(我在为这个外部库写一个功能测试应用)

这个外部库是用C语言写的,使用函数指针和回调来处理很多复杂的操作。它是一个消息系统,所以当有消息到达时,就会调用一个回调函数。

我在我的库中实现了观察者模式,这样多个对象可以监听同一个回调。我已经把主要的部分导出得很好,能够在一定程度上很好地控制一切。

这个外部库会创建线程来处理消息、发送消息、进行处理等等。有些回调可能会从不同的进程中被调用,而我最近发现Python并不是线程安全的。

这些观察者可以在Python中定义,所以我需要能够随时调用Python,而Python也需要能够调用我的程序。

我这样设置对象和观察者:

class TestObserver( MyLib.ConnectionObserver ):
    def receivedMsg( self, msg ):
        print("Received a message!")

ob = TestObserver()
cnx = MyLib.Conection()
cnx.attachObserver( ob )

然后我创建一个源来发送到连接,接收到的消息函数就会被调用。

所以一个普通的source.send('msg')会进入我的C++应用,传递到C库,然后发送消息,连接会接收到它,然后调用回调,这个回调又回到我的C++库,连接尝试通知所有观察者,此时的观察者就是这里的Python类,因此它会调用那个方法。

当然,回调是从连接线程中调用的,而不是主应用线程。

昨天一切都崩溃了,我连发送一条消息都做不到。然后在Cplusplus-sig的档案中翻找后,我了解到GIL和一些锁定的函数。

所以我现在的C++ Python封装类看起来是这样的:

struct IConnectionObserver_wrapper : Observers::IConnectionObserver, wrapper<Observers::IConnectionObserver>
{
    void receivedMsg( const Message* msg )
    {
        PyGILState_STATE gstate = PyGILState_Ensure();
        if( override receivedMsg_func = this->get_override( "receivedMsg" ) )
            receivedMsg_func( msg );
        Observers::IConnectionObserver::receivedMsg( msg );
        PyGILState_Release( gstate );
    }
}

这有效,但当我尝试发送超过250条消息时:

for i in range(250)
    source.send('msg")

它又崩溃了。出现的错误和之前一样,

PyThreadState_Get: no current thread

所以我在想这次我可能是调用我的C++应用时出了问题,而不是调用Python。

我的问题是,有没有办法让boost::python自己处理每次与Python交互时的GIL?我在代码中找不到任何相关的内容,而且很难找到source.send调用是如何进入boost_python的 :(

3 个回答

2

我觉得最好的办法是避免使用全局解释器锁(GIL),确保你和Python的交互是单线程的。

我现在正在设计一个基于boost.python的测试工具,我想我可能会使用一个生产者/消费者队列来处理来自多线程库的事件,这些事件会被Python线程按顺序读取。

4

我不太清楚你的具体问题,但你可以看看“调用策略”(CallPolicies):

http://www.boost.org/doc/libs/1_37_0/libs/python/doc/v2/CallPolicies.html#CallPolicies-concept

你可以定义新的调用策略(比如有一种叫“返回内部引用”的策略),它可以在执行被包装的C++函数之前和/或者之后运行一些代码。我成功地实现了一种调用策略,可以在执行C++包装的函数之前自动释放全局解释器锁(GIL),然后在返回Python之前再重新获取它,这样我就可以写出这样的代码:

.def( "long_operation", &long_operation, release_gil<>() );

使用调用策略可能会让你更容易地编写这段代码。

4

我在一个邮件列表上发现了一个很冷门的帖子,里面提到在 BOOST_PYTHON_MODULE 中使用 PyEval_InitThreads();,结果这真的似乎解决了崩溃的问题。

不过,程序是否能报告它收到的所有消息还是个未知数。如果我发送2000条消息,大多数情况下它会说收到了2000条,但有时候报告的数量会少很多。

我怀疑这可能是因为多个线程同时访问我的计数器,所以我在这里回答这个问题,因为这属于另一个问题。

要解决这个问题,只需执行

BOOST_PYTHON_MODULE(MyLib)
{
    PyEval_InitThreads();
    class_ stuff

撰写回答