Skip to content

Commit 2d6116b

Browse files
committed
Fix GIL release and acquire when embedding the interpreter
Fixes a race condition when multiple threads try to acquire the GIL before `detail::internals` have been initialized. `gil_scoped_release` is now tasked with initializing `internals` (guaranteed single-threaded) to ensure the safety of subsequent `acquire` calls from multiple threads.
1 parent f42af24 commit 2d6116b

File tree

3 files changed

+39
-1
lines changed

3 files changed

+39
-1
lines changed

include/pybind11/pybind11.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1661,9 +1661,13 @@ class gil_scoped_acquire {
16611661
class gil_scoped_release {
16621662
public:
16631663
explicit gil_scoped_release(bool disassoc = false) : disassoc(disassoc) {
1664+
// `get_internals()` must be called here unconditionally in order to initialize
1665+
// `internals.tstate` for subsequent `gil_scoped_acquire` calls. Otherwise, an
1666+
// initialization race could occur as multiple threads try `gil_scoped_acquire`.
1667+
const auto &internals = detail::get_internals();
16641668
tstate = PyEval_SaveThread();
16651669
if (disassoc) {
1666-
auto key = detail::get_internals().tstate;
1670+
auto key = internals.tstate;
16671671
#if PY_MAJOR_VERSION < 3
16681672
PyThread_delete_key_value(key);
16691673
#else

tests/test_embed/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ else()
2626
target_link_libraries(test_embed PRIVATE ${PYTHON_LIBRARIES})
2727
endif()
2828

29+
find_package(Threads REQUIRED)
30+
target_link_libraries(test_embed PUBLIC ${CMAKE_THREAD_LIBS_INIT})
31+
2932
add_custom_target(cpptest COMMAND $<TARGET_FILE:test_embed>
3033
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
3134
add_dependencies(check cpptest)

tests/test_embed/test_interpreter.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include <pybind11/embed.h>
22
#include <catch.hpp>
33

4+
#include <thread>
5+
46
namespace py = pybind11;
57
using namespace py::literals;
68

@@ -185,3 +187,32 @@ TEST_CASE("Execution frame") {
185187
py::exec("var = dict(number=42)");
186188
REQUIRE(py::globals()["var"]["number"].cast<int>() == 42);
187189
}
190+
191+
TEST_CASE("Threads") {
192+
// Restart interpreter to ensure threads are not initialized
193+
py::finalize_interpreter();
194+
py::initialize_interpreter();
195+
REQUIRE_FALSE(has_pybind11_internals_static());
196+
197+
constexpr auto num_threads = 10;
198+
auto locals = py::dict("count"_a=0);
199+
200+
{
201+
py::gil_scoped_release gil_release{};
202+
REQUIRE(has_pybind11_internals_static());
203+
204+
auto threads = std::vector<std::thread>();
205+
for (auto i = 0; i < num_threads; ++i) {
206+
threads.emplace_back([&]() {
207+
py::gil_scoped_acquire gil{};
208+
py::exec("count += 1", py::globals(), locals);
209+
});
210+
}
211+
212+
for (auto &thread : threads) {
213+
thread.join();
214+
}
215+
}
216+
217+
REQUIRE(locals["count"].cast<int>() == num_threads);
218+
}

0 commit comments

Comments
 (0)