简体   繁体   English

从 C++ 函数调用 python lambda 时处理 GIL

[英]Handling GIL when calling python lambda from C++ function

The question问题

Is pybind11 somehow magically doing the work of PyGILState_Ensure() and PyGILState_Release() ? pybind11 是否以某种方式神奇地完成了PyGILState_Ensure()PyGILState_Release()的工作? And if not, how should I do it?如果没有,我该怎么做?

More details更多细节

There are many questions regarding passing a python function to C++ as a callback using pybind11, but I haven't found one that explains the use of the GIL with pybind11.关于使用pybind11将 python 函数作为回调传递给 C++ 很多问题,但我还没有找到一个解释 GIL 与 pybind11 一起使用的问题。

The documentation is pretty clear about the GIL: 文档对 GIL 非常清楚:

[...] However, when threads are created from C (for example by a third-party library with its own thread management), they don't hold the GIL, nor is there a thread state structure for them. [...] 但是,当从 C 语言创建线程时(例如由具有自己线程管理的第三方库),它们不持有 GIL,也没有线程状态结构。

If you need to call Python code from these threads (often this will be part of a callback API provided by the aforementioned third-party library), you must first register these threads with the interpreter by creating a thread state data structure, then acquiring the GIL, and finally storing their thread state pointer, before you can start using the Python/C API.如果您需要从这些线程调用 Python 代码(通常这将是上述第三方库提供的回调 API 的一部分),您必须首先通过创建线程状态数据结构将这些线程注册到解释器,然后获取GIL,最后存储它们的线程状态指针,然后才能开始使用 Python/C API。

I can easily bind a C++ function that takes a callback:我可以轻松绑定一个接受回调的 C++ 函数:

py::class_<SomeApi> some_api(m, "SomeApi"); 
some_api
    .def(py::init<>())
    .def("mode", &SomeApi::subscribe_mode, "Subscribe to 'mode' updates.");

With the corresponding C++ function being something like:对应的 C++ 函数类似于:

void subscribe_mode(const std::function<void(Mode mode)>& mode_callback);

But because pybind11 cannot know about the threading happening in my C++ implementation, I suppose it cannot handle the GIL for me.但是因为 pybind11 不知道我的 C++ 实现中发生的线程,我想它不能为我处理 GIL。 Therefore, if mode_callback is called by a thread created from C++, does that mean that I should write a wrapper to SomeApi::subscribe_mode that uses PyGILState_Ensure() and PyGILState_Release() for each call?因此,如果mode_callback由从 C++ 创建的线程调用,这是否意味着我应该为SomeApi::subscribe_mode编写一个包装器,每次调用都使用PyGILState_Ensure()PyGILState_Release()

This answer seems to be doing something similar, but still slightly different: instead of "taking the GIL" when calling the callback, it seems like it "releases the GIL" when starting/stopping the thread.这个答案似乎在做类似的事情,但仍然略有不同:在调用回调时不是“获取 GIL”,而是在启动/停止线程时“释放 GIL”。 Still I'm wondering if there exists something like py::call_guard<py::gil_scoped_acquire>() that would do exactly what I (believe I) need, ie wrapping my callback with PyGILState_Ensure() and PyGILState_Release() .我仍然想知道是否存在类似py::call_guard<py::gil_scoped_acquire>()的东西,它可以完全满足我(相信我)的需要,即用PyGILState_Ensure()PyGILState_Release()包装我的回调。

In general一般来说

pybind11 tries to do the Right Thing and the GIL will be held when pybind11 knows that it is calling a python function, or in C++ code that is called from python via pybind11. pybind11 尝试做正确的事情,当 pybind11 知道它正在调用 python 函数或在通过 pybind11 从 python 调用的 C++ 代码中时,GIL 将被保留。 The only time that you need to explicitly acquire the GIL when using pybind11 is when you are writing C++ code that accesses python and will be called from other C++ code, or if you have explicitly dropped the GIL.使用 pybind11 时唯一需要显式获取 GIL 的情况是,当您编写访问 python 并将从其他 C++ 代码调用的 C++ 代码时,或者如果您已显式删除 GIL。

std::function wrapper std::function 包装器

The wrapper for std::function always acquires the GIL via gil_scoped_acquire when the function is called , so your python callback will always be called with the GIL held, regardless which thread it is called from. 当调用函数时std::function的包装器始终通过gil_scoped_acquire获取 GIL,因此无论从哪个线程调用它,都将始终在持有 GIL 的情况下调用您的 python 回调。

If gil_scoped_acquire is called from a thread that does not currently have a GIL thread state associated with it, then it will create a new thread state .如果gil_scoped_acquire从当前没有与之关联的 GIL 线程状态的线程调用,那么它将创建一个新的线程状态 As a side effect, if nothing else in the thread acquires the thread state and increments the reference count, then once your function exits the GIL will be released by the destructor of gil_scoped_acquire and then it will delete the thread state associated with that thread .作为副作用,如果线程中没有其他任何东西获取线程状态并增加引用计数,那么一旦您的函数退出,GIL 将由gil_scoped_acquire的析构函数释放,然后它将删除与该线程关联的线程状态

If you're only calling the function once from another thread, this isn't a problem.如果您只从另一个线程调用该函数一次,这不是问题。 If you're calling the callback often, it will create/delete the thread state a lot, which probably isn't great for performance.如果你经常调用回调,它会创建/删除很多线程状态,这可能对性能不利。 It would be better to cause the thread state to be created when your thread starts (or even easier, start the thread from Python and call your C++ code from python).最好在线程启动时创建线程状态(或者更简单,从 Python 启动线程并从 python 调用 C++ 代码)。

Firstly, the documentation for the current version of python is here (unless you are targeting python 2.x?)首先,当前版本的 python 的文档在这里(除非你的目标是 python 2.x?)

How python handles the GIL in extensions python如何处理扩展中的GIL

The general setting in python is as follows: In the python context, the GIL is held. python中的一般设置如下: 在python上下文中,持有GIL。 Thus, when calling into an extension, the GIL is also held at the beginning of the call.因此,当呼叫分机时,GIL 也会在呼叫开始时保留。 This is independent of whether you use plain C, pybind11, cython, or any other tool to generate the extension.这与您是否使用普通 C、pybind11、cython 或任何其他工具来生成扩展名无关。

None of those tools magically release the GIL.这些工具都没有神奇地释放 GIL。 The reason is twofold: First, devs have come to rely on the guarantees of GIL, which makes it possible to include libraries which are not thread-save themselves into python as extensions without any additional synchronization.原因是双重的:首先,开发人员已经开始依赖 GIL 的保证,这使得将非线程保存到 python 中的库作为扩展包含在没有任何额外同步的情况下成为可能。 Second, any manipulation of python objects without the GIL held will potentially lead to crashes.其次,在没有持有 GIL 的情况下对 python 对象的任何操作都可能导致崩溃。

Writing multi-threaded code编写多线程代码

In terms of you writing multi-threaded code, problems occur whenever you interact with python within several threads.就您编写多线程代码而言,只要您在多个线程中与 python 交互,就会出现问题。 One workflow with respect to your function "mode" that avoids these issues is to避免这些问题的关于您的功能"mode"的一种工作流程是

  1. Convert all python data to C / C++ data in the original callback在原始回调中将所有python数据转换为C/C++数据
  2. Fork multiple threads to crunch the data, joining them afterwards分叉多个线程来处理数据,然后加入它们
  3. Create a python return value in the original thread after having finished your computations完成计算后在原始线程中创建一个 python 返回值

This should not cause any problems with your python context.这不会对您的 python 上下文造成任何问题。 It will however lock up the interpreter, since the GIL is held.然而,它会锁定解释器,因为 GIL 被持有。

So, if you want to crunch even more data by creating several python threads (using a ThreadPoolExecutor for instance) and calling into "mode" from several threads, only one call to "mode" will be executing at any given time.因此,如果您想通过创建多个 python 线程(例如使用ThreadPoolExecutor )并从多个线程调用"mode"来处理更多数据,那么在任何给定时间只会执行一个对"mode"的调用。

This is of course rather unnecessary, since your worker threads in step 2 don't need to manipulate any python objects and the GIL does not need to be held during this time.这当然是不必要的,因为您在步骤 2 中的工作线程不需要操作任何 python 对象,并且在此期间不需要持有 GIL。 Therefore, the GIL can be released during operations which do not need to interact with python.因此,可以在不需要与 python 交互的操作中释放 GIL。 Thus, many C backends would release the GIL so the python interpreter can do other stuff in the meantime ([here] for example 2 ).因此,许多 C 后端会释放 GIL,因此 python 解释器可以同时做其他事情([here] 例如2 )。 The call would look like this:调用将如下所示:

  1. Convert all python data to C / C++ data in the original callback在原始回调中将所有python数据转换为C/C++数据
  2. Release the GIL释放 GIL
  3. Fork multiple threads to crunch the data, joining them afterwards分叉多个线程来处理数据,然后加入它们
  4. Acquire the GIL获取 GIL
  5. Create a python return value in the original thread after having finished your computations完成计算后在原始线程中创建一个 python 返回值

So, the process is reversed compared to how locks are normally used.因此,与通常使用锁的方式相比,该过程是相反的。 Rather than acquiring a resource, working on it, and finally releasing it, a lock is released and reacquired after some work has been done.与其获取资源、处理资源并最终释放它,不如在完成一些工作后释放并重新获取锁。

However, there are some situations where the data crunching threads need to interact with python during the crunching.但是,在某些情况下,数据处理线程需要在处理期间与 python 交互。 Think for instance of a numerical optimization tool which needs additional function evaluations at different times.例如,一个数值优化工具需要在不同时间进行额外的函数评估。 Consequently, threads may need to acquire the GIL, do some python-related business and then release the GIL again.因此,线程可能需要获取 GIL,做一些与 python 相关的业务,然后再次释放 GIL。 This is shown in the corresponding example in the pybind11 documentation.这显示在 pybind11 文档中的相应示例中。

It is for this reason that there exist the gil_scoped_release class (release upon creation, acquire upon destruction), to be used at the top level, and the gil_scoped_acquire (working in reverse), to be used in threads which are executed while the GIL is released.正是出于这个原因,存在gil_scoped_release类(创建时释放,销毁时获取),用于顶层,以及gil_scoped_acquire (反向工作),用于在 GIL 运行时执行的线程中释放。 Again, there is no thread-related magic going on (afaik).同样,没有与线程相关的魔法发生(afaik)。 You have to use these classes in your code when you interact with python from different threads.当您从不同线程与 python 交互时,您必须在代码中使用这些类。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM