我们知道, Python 程序有全局锁,任何时候都只有一个 Python 语句在执行。在 Python 中,这通过全局的 GIL 锁来控制。当 C++和 Python 混合编程,且使用多线程时,也必须考虑到 GIL 锁(单线程无需考虑)。
一个 C++和 Python 的多线程混合编程的例子如下:
#include <python3.12/Python.h>
#include <thread>
void python_function() {
PyGILState_STATE state = PyGILState_Ensure();
// do something, includes call python objects and functions
PyGILState_Release(state);
}
int main() {
Py_Initialize();
PyThreadState* state = PyEval_SaveThread();
std::thread t(python_function);
t.join();
PyEval_RestoreThread(state);
// do other thing, includes call python objects and functions
}
为什么要这么写?主要的就是为了获取和释放 GIL 全局锁:
- Py_Initialize 会初始化 Python 解析器,此时主线程会获取到 GIL 锁。
- PyEval_SaveThread() 会保存主线程状态,同时释放 GIL。
- 子线程里 PyGILState_Ensure 获取全局锁,开始执行 Python 代码。
- 子线程执行完毕之后,使用 PyGILState_Release 释放 GIL。
- 主线程通过 PyEval_RestoreThread 重新恢复线程状态,获取 GIL ,后续又可以跑 Python 代码了。
我们可以用 RAII 技术定义两个辅助类:
/**
* @brief Python的全局GIL自动锁。该对象在构建时,可自动获取GIL锁,在销毁时,自动释放GIL全局锁。
*/
class Lock
{
private:
PyGILState_STATE state; //!< @brief 全局GIL锁。
public:
/// @brief 自动锁上。
Lock() { state = PyGILState_Ensure(); }
/// @brief 自动解锁。
~Lock() { PyGILState_Release(state); }
};
/**
* @brief 线程状态管理器。
*
* 在 c++ 中嵌入 python 解释器、并使用 c++ 库创建多线程来执行 python 代码的场景中,
* 为了确保 python 解释器安全的切换线程,需要跟踪管理 c++ 所创建线程的状态。
* 该对象在构建时,会自动释放GIL并保存当前线程的状态,然后可切换到其他线程执行python代码。
* 该对象在销毁时,会重新获取GIL并恢复当前线程的状态,然后当前线程可以继续执行其他python代码。
* 由于使用了 RAII 技术,请使用时正确指定该对象的作用域,使析构函数在正确位置被调用。
*/
class PyThreadStateMgr
{
private:
PyThreadState* state; //!< @brief python 线程状态。
public:
/// @brief 释放 GIL,并自动保存线程当前状态
PyThreadStateMgr() { state = PyEval_SaveThread(); }
/// @brief 重新获取 GIL,并自动恢复线程状态。
~PyThreadStateMgr() { PyEval_RestoreThread(state); }
};
这样在使用时就很方便了:
#include <python3.12/Python.h>
#include <thread>
void python_function() {
Lock _;
// do something, includes call python objects and functions
}
int main() {
Py_Initialize();
{
PyThreadStateMgr _;
std::thread t(python_function);
t.join();
}
// do other thing, includes call python objects and functions
}
Q. E. D.