嵌入式Python 3.10中Py_FinalizeEx在threading._shutdown()处挂起死锁
在C++应用中嵌入Python,怀疑对PyGILState_Ensure/PyGILState_Release的使用存在误区,最终导致Py_FinalizeEx在threading._shutdown()(由Py_FinalizeEx调用)中因线程join()操作而挂起死锁。
初始化代码:
Py_InitializeEx(0); // Skip installing signal handlers auto gil = PyGILState_Ensure(); // ... running Python code PyGILState_Release(gil);
C++线程调用Python的宏:
#define pyscope() \ PyGILState_STATE gstate = PyGILState_Ensure(); \ utils::scope_guard sggstate([&]() \ { \ PyGILState_Release(gstate); \ });
释放资源的代码(死锁发生处):
PyGILState_STATE gstate = PyGILState_Ensure(); int res = Py_FinalizeEx(); // <--- HANGS!!!
调试后发现问题出在线程join阶段,可通过在Py_FinalizeEx前执行以下Python代码复现死锁:
import threading for t in threading.enumerate(): print('get_ident: {} ; native: {}'.format(t.ident, t.native_id)) if not threading.current_thread().ident == t.ident: t.join()
另外,未使用PyEval_SaveThread/RestoreThread,不确定是否需要使用,也不清楚如何将其与GIL_Ensure/Release配合使用,因为发现它们内部也会进行GIL的获取与释放。
请问该死锁的原因是什么?如何解决?
原因分析
死锁的核心原因是调用Py_FinalizeEx时还持有GIL:Py_FinalizeEx会触发threading._shutdown(),该函数会尝试join所有存活的Python线程。而这些Python线程在启动后,执行逻辑中需要获取GIL才能继续运行或退出;此时你当前调用Py_FinalizeEx的线程牢牢持有GIL,导致那些Python线程永远无法获取GIL完成收尾,当前线程又在等待它们join,形成双向阻塞的死锁。
关于PyEval_SaveThread/RestoreThread:PyGILState_Ensure内部已经封装了PyEval_RestoreThread的逻辑,PyGILState_Release则对应PyEval_SaveThread,所以你不需要额外调用这两个函数,当前的pyscope宏写法本身没有问题。
解决方法
调用
Py_FinalizeEx前必须释放GIL
这是解决死锁最关键的一步,修改释放资源的代码:// 如果需要执行Python层面的清理操作,先获取GIL PyGILState_STATE gstate = PyGILState_Ensure(); // 执行必要的清理(比如通知Python后台线程退出) PyGILState_Release(gstate); // 必须释放GIL后再调用Py_FinalizeEx int res = Py_FinalizeEx();释放GIL后,等待退出的Python线程才能获取到GIL完成收尾工作,
threading._shutdown()的join操作才能正常完成。提前主动终止所有Python后台线程
不要依赖threading._shutdown()的强制join,在清理阶段主动通知所有Python后台线程退出。比如可以设置一个全局的退出标志变量,让Python线程在逻辑中定期检查该标志,主动退出执行流程。确保所有C++线程已完成Python调用
检查所有使用pyscope宏的C线程,确保它们都已经执行完Python调用逻辑并释放了GIL,避免在Py_FinalizeEx执行期间,还有C线程尝试获取GIL,引发额外的锁冲突。遵守Py_FinalizeEx的调用规范
Py_FinalizeEx是全局资源清理函数,执行时不能持有任何Python相关的锁(包括GIL),否则必然会与线程收尾逻辑产生死锁冲突。这是嵌入Python开发中必须牢记的原则。
内容的提问来源于stack exchange,提问作者TCS

