ARM64架构下C++多线程嵌入Python/C API死锁问题咨询
你的问题核心在于主线程初始化Python后默认持有GIL,未释放导致其他线程无法获取GIL而死锁,下面一步步拆解:
1. Py_Initialize()后的GIL状态
当你在主线程调用Py_Initialize()时,Python解释器会自动让主线程持有全局解释器锁(GIL)。此时如果你的主线程后续没有主动释放GIL,其他线程调用PyGILState_Ensure()时,会一直等待主线程释放GIL——这就是你遇到死锁的原因。
2. 为什么PyEval_SaveThread()能解决问题?
PyEval_SaveThread()的作用是释放当前线程持有的GIL,并返回该线程的Python状态(这个返回值通常用于后续调用PyEval_RestoreThread()恢复状态)。
在你的场景中,主线程初始化Python后不需要再执行任何Python API调用,所以直接调用PyEval_SaveThread()释放GIL是安全的——即使你没有调用PyEval_RestoreThread(),因为主线程之后不再接触Python代码,不会有状态冲突。释放GIL后,其他线程调用PyGILState_Ensure()就能正常获取锁并执行Python代码了。
3. 关于Py_BEGIN_ALLOW_THREADS宏的疑问
Py_BEGIN_ALLOW_THREADS和Py_END_ALLOW_THREADS本质是对PyEval_SaveThread()和PyEval_RestoreThread()的封装,它们的设计初衷确实是方便Python线程在执行非Python代码时释放GIL,但这不代表它们不能用在嵌入场景的主线程中。
如果你的主线程后续还有需要执行Python API的逻辑,用这对宏包裹非Python代码段会更规范:
PyHandler::PyHandler() { Py_Initialize(); Py_BEGIN_ALLOW_THREADS // 主线程的非Python代码逻辑 Py_END_ALLOW_THREADS }
但如果主线程初始化后完全不需要再碰Python,直接调用PyEval_SaveThread()是更简洁的选择。
4. 你的代码优化建议
目前的实现虽然能运行,但有一个潜在风险:如果后续主线程需要调用Python API,你没有保存PyEval_SaveThread()的返回值,就无法恢复线程状态。建议稍微调整构造函数:
class PyHandler { public: PyHandler(); ~PyHandler(); bool run_fun(); // ... private: PyGILState_STATE _gstate; std::mutex _mutex; PyThreadState* _main_thread_state = nullptr; // 新增主线程状态变量 } PyHandler::PyHandler() { Py_Initialize(); // 保存线程状态,方便后续主线程需要调用Python API时恢复 _main_thread_state = PyEval_SaveThread(); }
然后在析构函数(或者主线程需要重新调用Python API的地方)恢复:
PyHandler::~PyHandler() { // 如果主线程之前释放了GIL,需要先恢复才能执行Py_Finalize() if (_main_thread_state != nullptr) { PyEval_RestoreThread(_main_thread_state); } if (Py_IsInitialized()) Py_Finalize(); }
这样能保证析构时主线程正确持有GIL,避免Py_Finalize()出现未定义行为。
总结
- 嵌入Python到C++多线程程序时,主线程初始化Python后必须主动释放GIL,否则其他线程无法获取GIL会导致死锁。
PyEval_SaveThread()和Py_BEGIN_ALLOW_THREADS都可以用于释放GIL,选择哪种取决于主线程后续是否需要再调用Python API。PyGILState_Ensure()/Release()是管理非主线程GIL的正确方式,但前提是主线程已经释放了初始持有的GIL。
内容的提问来源于stack exchange,提问作者N.R.

