如何在子解释器中检查线程是否持有GIL?

3
我正在修改一个嵌入Python的库,需要使用子解释器以支持重置Python状态,同时避免调用Py_Finalize(因为之后调用Py_Initialize是不可取的)。
我对该库略有了解,但我越来越发现一些地方使用PyGILState_Ensure和其他PyGILState_*函数来获取GIL以响应某些外部回调。其中一些回调来自Python外部,因此我们的线程肯定没有持有GIL,但有时回调来自于Python内部,因此我们肯定持有GIL。
切换到子解释器后,我几乎立即在调用PyGILState_Ensure时看到了死锁,因为它调用了PyEval_RestoreThread,尽管显然已经从Python内部执行(因此持有GIL): stack trace at the point of deadlock “值得一提的是,我已经验证了在调用PyGILState_Ensure之前确实会执行调用PyEval_RestoreThread的行(在上面的图片中第一次调用Python之前很久就执行了)。
我正在使用Python 3.8.2。显然,文档没有说谎,它
请注意,PyGILState_*函数假定只有一个全局解释器(由Py_Initialize()自动创建)。Python支持创建额外的解释器(使用Py_NewInterpreter()),但混合多个解释器和PyGILState_* API是不受支持的。
重构库以便内部跟踪是否保留了GIL是相当麻烦且相当愚蠢的。应该有一种方法来确定是否保留了GIL!但是,我能找到的唯一函数是PyGILState_Check,但它是禁止使用的PyGILState API的成员。我不确定它是否有效。是否有一种规范的方法在子解释器中执行此操作?”
1个回答

1

我一直在思考文档中的这一行

也请注意,将此功能与PyGILState_* API结合使用是棘手的,因为这些API假定Python线程状态与操作系统级线程之间存在双射关系,而子解释器的存在打破了这种假设。

我怀疑问题出在PyGILState_* API的线程本地存储方面。

我现在想到的是,实际上很难确定应用程序是否持有全局解释器锁(GIL)。Python没有一个中央静态的位置来存储GIL被持有的信息,因为它要么被“你”(在外部代码中)持有,要么被Python代码持有。它总是被某个人持有。所以“GIL是否被持有”的问题并不是PyGILState API所询问的问题。它所问的是“这个线程是否持有GIL”,这样就更容易让多个非Python线程与解释器进行交互。

我通过尽量恢复双射关系,为每个子解释器创建一个单独的线程来解决了这个问题,操作顺序非常严格,如下所示:

  1. 在主线程中使用 Py_Initialize 显式地获取 GIL(全局解释器锁),或者隐式地获取 GIL(如果这是第一次)。非常小心,从 Py_Initialize 获取的线程状态只能在主线程中使用。不要将其恢复到另一个线程:某些模块可能会使用 PyGILState_* API,死锁会再次发生。
  2. 创建线程。我只是使用了 std::thread
  3. 使用 Py_NewInterpreter 生成子解释器。非常小心,这将给你一个新的线程状态。与主线程状态一样,此线程状态只能从此线程中使用。
  4. 当您准备好让 Python 进行操作时,在新线程中释放 GIL。

现在,我发现了一些需要注意的问题:

Python 3.8-3.9 中的 asyncio 存在一个 use-after-free 错误,其中第一个加载它的解释器管理一些资源。因此,如果该解释器结束(释放这些资源),并且新的解释器获取 asyncio,则会出现段错误。我通过在主解释器中手动使用 C API 加载 asyncio 来克服这个问题,因为主解释器永远存在。
许多库,包括 numpy、lxml 和几个网络库,在多个子解释器中都会遇到问题。我相信 Python 本身正在强制执行此操作:当使用“Interpreter change detected - This module can only be loaded into one interpreter per process.”导入这些库之一时,将导致 ImportError。到目前为止,这对我来说似乎是一个无法克服的问题,因为我确实需要在我的应用程序中使用 numpy。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接