在使用多线程C++扩展时,是否需要注意Python GIL



我现在正在用Python实现一个数据订阅服务器,它订阅一个数据发布服务器(实际上是ZeroMQ发布服务器套接字),一旦收到任何新消息,就会收到通知。在我的订阅服务器中,消息在收到后会转储到数据处理器。完成后,处理器还会通知订阅者。由于数据处理器是用C++编写的,所以我必须用一个简单的C++模块来扩展Python代码。

下面是我的数据订阅者的一个简化的可运行代码示例。代码main.py,其中模块proc代表处理器,订阅localhost:10000上的ZeroMQ套接字,设置回调,并通过调用proc.onMsg将接收到的消息发送给处理器。

#!/bin/python
# main.py
import gevent
import logging
import zmq.green as zmq
import pub 
import proc
logging.basicConfig( format='[%(levelname)s] %(message)s', level=logging.DEBUG )
SUB_ADDR = 'tcp://localhost:10000'
def setupMqAndReceive():
'''Setup the message queue and receive messages.
'''
ctx  = zmq.Context()
sock = ctx.socket( zmq.SUB )
# add topics
sock.setsockopt_string( zmq.SUBSCRIBE, 'Hello' )
sock.connect( SUB_ADDR )
while True:
msg = sock.recv().decode( 'utf-8' )
proc.onMsg( msg )
def callback( a, b ):
print( '[callback]',  a, b ) 
def main():
'''Entrance of the module.
'''
pub.start()
proc.setCallback( callback )
'''A simple on-liner
gevent.spawn( setupMqAndReceive ).join()
works. However, the received messages will not be
processed by the processor.
'''
gevent.spawn( setupMqAndReceive )
proc.start()

模块proc简化为导出三个功能:

  • setCallback设置了回调功能,这样当消息处理时,我的订阅者就可以得到通知
  • CCD_ 6由订户调用
  • start设置一个新的工作线程来处理来自订阅者的消息,并使主线程加入以等待工作线程退出

源代码的完整版本可以在github上找到https://github.com/more-more-tea/python_gil.然而,它并没有如我所期望的那样运行。一旦添加了处理器线程,订阅者就无法在gevent循环中接收来自发布者的数据。如果我简单地放弃数据处理器模块,订阅者gevent循环就可以接收来自发布者的消息。

代码有什么问题吗?我怀疑GIL干扰了消息处理器中pthread的并发性,或者gevent循环不足。任何关于这个问题或如何调试它的提示都将不胜感激!

全局解释器锁本身不会阻止线程被调度。Python C API并不是到处都将自己注入pthread库。这有好有坏。

这很好,因为您实际上可以在一个C或C++扩展中同时做多件事。

这很糟糕,因为你可能会意外违反GIL规则。

GIL的规则(大致)如下:

  1. 当您的代码从Python调用时,您可能会假设您的线程具有GIL。当您的代码不是从Python调用时,您可能不会做出这种假设
  2. 除非另有明确说明,否则必须先拥有GIL,然后才能调用Python/C API的任何部分。这包括Python/C API拥有的所有内容,甚至包括refcounting宏Py_INCREF()Py_DECREF()等简单内容
  3. 当在C或C++函数中执行时,GIL不会自动释放自己。如果您不需要GIL,则需要手动执行此操作。特别是,当您调用像pthread_join()select()这样的阻塞函数时,它不会自动释放自己,这意味着您阻塞了整个解释器

此处指定了这些规则的正式版本。请密切关注"非Python创建的线程"部分;这正是关于你想做什么。

读取您的代码时,您似乎未能在procThread()函数中获取GIL,也未能在调用pthread_join()之前释放它。可能还有其他问题,但这些对我来说是最明显的

这是我对这个问题的解决方案,也是我对Python线程和pthread原生线程的理解。

Python线程虽然受到GIL的保护,但实际上是系统线程。唯一不同的是,在运行时,Python线程受到GIL的保护。threading.Thread派生的线程是Python线程,在这些线程中运行的所有代码都自动受到GIL的保护。如果本机线程与Python线程共存,并且Python线程即将运行阻塞语句,例如I/O、Thread.join、sleep等,则Python线程中的GIL必须与Py_BEGIN_ALLOW_THREADSPy_END_ALLOW_THREADS一起发布。

而Python世界之外的其他线程,例如pthread库,在执行Python代码时,应该使用Python C APIPyGILState_EnsurePyGILState_Release显式地获取GIL(对于纯C/C++代码,根据我的经验,不需要获取Python GIL),如Kevin的回答所示。

在GitHub上可以找到更新的代码。

如果有任何误解,请给我一个评论。谢谢大家!

最新更新