在使用多线程C++扩展时,是否需要注意Python GIL
Does Python GIL need to be taken care when work with multi-thread C++ extension?
我现在正在用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的规则(大致)如下:
- 当您的代码从Python调用时,您可能会假设您的线程具有GIL。当您的代码不是从Python调用时,您可能不会做出这种假设
- 除非另有明确说明,否则必须先拥有GIL,然后才能调用Python/C API的任何部分。这包括Python/C API拥有的所有内容,甚至包括refcounting宏
Py_INCREF()
和Py_DECREF()
等简单内容 - 当在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_THREADS
和Py_END_ALLOW_THREADS
一起发布。
而Python世界之外的其他线程,例如pthread库,在执行Python代码时,应该使用Python C APIPyGILState_Ensure
和PyGILState_Release
显式地获取GIL(对于纯C/C++代码,根据我的经验,不需要获取Python GIL),如Kevin的回答所示。
在GitHub上可以找到更新的代码。
如果有任何误解,请给我一个评论。谢谢大家!
- 如何运行位于boost/libs/python/example/tutorial目录中的hello.cpp和Jamfil
- Pybind11:将元组列表从Python传递到C++
- 如何在c++中使用引用实现类似python的行为
- 是否可以通过C++扩展强制多个python进程共享同一内存
- 递归列出所有目录中的C++与Python与Ruby的性能
- IPC使用多个管道和分支进程来运行Python程序
- 从python中调用C++函数并获取返回值
- Python 3.7 和 excess_args 的 SWIG 问题
- Python中的for循环与C++有何不同
- 使用Pybind11向Python公开Eigen::张量
- Python str to C++ to Python str
- 如何使用Python从C++中读取谷物序列化数据
- 如何在C++中使用pybind11加载一个pickle python列表
- 如何在c++中使用system()来运行包含空格的python脚本
- python集合的C++等价物是什么.计数器
- 如何从主线程正确释放 Python C API GIL
- 在使用多线程C++扩展时,是否需要注意Python GIL
- Python GIL and threads
- Python GIL:并发c++嵌入
- python函数返回没有GIL错误的指针