Python 使用多线程 C++ 扩展时需要注意 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
.[= 将接收到的消息发送给处理器。 20=]
#!/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
设置回调函数,以便在消息处理时通知我的订阅者;
onMsg
被订阅者调用;
start
设置一个新的工作线程来处理来自订阅者的消息,并让主线程加入等待工作线程退出。
完整版本的源代码可以在 github at 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 拥有的 一切 ,甚至是像引用计数宏
Py_INCREF()
和 Py_DECREF()
.[=34= 这样简单的东西]
- 在 C 或 C++ 函数中执行时,GIL 不会自动释放自身。如果不需要 GIL,则需要手动执行此操作。特别是,当您调用
pthread_join()
或 select()
等阻塞函数时,它不会自动释放自身,这意味着您阻塞了整个解释器。
这些规则的正式版本已指定here。密切注意 "Non-Python created threads" 部分;这正是您要尝试做的事情。
看了你的代码,看来你在procThread()
函数中获取GIL失败,而且在调用pthread_join()
之前释放GIL也失败了。可能还有其他问题,但这些对我来说是最明显的。
有我对问题的解答和我对 Python thread 和 pthread native 的理解。
Python个线程,虽然有GIL保护,但实际上是系统线程。唯一不同的是当运行ning时,Python线程被GIL保护。 threading.Thread
生成的线程是 Python 线程,这些线程中的所有代码 运行ning 都自动受 GIL 保护。 Python 线程中的 GIL 必须与 Py_BEGIN_ALLOW_THREADS
和 Py_END_ALLOW_THREADS
一起释放,如果本机线程与 Python 线程共存并且 Python 线程即将 运行 阻塞语句,例如I/O、Thread.join、睡觉等
而其他线程在 Python 世界之外产生,例如通过 pthread 库,应该在执行 Python 代码时使用 Python C API PyGILState_Ensure
和 PyGILState_Release
显式获取 GIL(对于纯 C/C+ + 代码,根据我的经验无需获取 Python GIL)按照凯文的回答中的指示。
可以在 GitHub 上找到更新的代码。
如有理解错误,请大家多多指教。谢谢大家!
我现在正在使用 Python 实现一个数据订阅者,它订阅一个数据发布者(实际上是一个 ZeroMQ 发布者套接字),一旦有任何新消息被馈送就会收到通知。在我的订阅者中,消息在收到后被转储到数据处理器。完成后,订阅者也会收到处理器的通知。由于数据处理器是用 C++ 编写的,因此我必须使用简单的 C++ 模块扩展 Python 代码。
下面是我的数据订阅者的简化 运行 可用代码示例。代码 main.py
,其中模块 proc 代表处理器,在 localhost:10000
上订阅 ZeroMQ 套接字,设置回调,并通过调用 proc.onMsg
.[= 将接收到的消息发送给处理器。 20=]
#!/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
设置回调函数,以便在消息处理时通知我的订阅者;onMsg
被订阅者调用;start
设置一个新的工作线程来处理来自订阅者的消息,并让主线程加入等待工作线程退出。
完整版本的源代码可以在 github at 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 拥有的 一切 ,甚至是像引用计数宏
Py_INCREF()
和Py_DECREF()
.[=34= 这样简单的东西] - 在 C 或 C++ 函数中执行时,GIL 不会自动释放自身。如果不需要 GIL,则需要手动执行此操作。特别是,当您调用
pthread_join()
或select()
等阻塞函数时,它不会自动释放自身,这意味着您阻塞了整个解释器。
这些规则的正式版本已指定here。密切注意 "Non-Python created threads" 部分;这正是您要尝试做的事情。
看了你的代码,看来你在procThread()
函数中获取GIL失败,而且在调用pthread_join()
之前释放GIL也失败了。可能还有其他问题,但这些对我来说是最明显的。
有我对问题的解答和我对 Python thread 和 pthread native 的理解。
Python个线程,虽然有GIL保护,但实际上是系统线程。唯一不同的是当运行ning时,Python线程被GIL保护。 threading.Thread
生成的线程是 Python 线程,这些线程中的所有代码 运行ning 都自动受 GIL 保护。 Python 线程中的 GIL 必须与 Py_BEGIN_ALLOW_THREADS
和 Py_END_ALLOW_THREADS
一起释放,如果本机线程与 Python 线程共存并且 Python 线程即将 运行 阻塞语句,例如I/O、Thread.join、睡觉等
而其他线程在 Python 世界之外产生,例如通过 pthread 库,应该在执行 Python 代码时使用 Python C API PyGILState_Ensure
和 PyGILState_Release
显式获取 GIL(对于纯 C/C+ + 代码,根据我的经验无需获取 Python GIL)按照凯文的回答中的指示。
可以在 GitHub 上找到更新的代码。
如有理解错误,请大家多多指教。谢谢大家!