diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 2bfd81cf3..08268b8e8 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1661,9 +1661,13 @@ private: class gil_scoped_release { public: explicit gil_scoped_release(bool disassoc = false) : disassoc(disassoc) { + // `get_internals()` must be called here unconditionally in order to initialize + // `internals.tstate` for subsequent `gil_scoped_acquire` calls. Otherwise, an + // initialization race could occur as multiple threads try `gil_scoped_acquire`. + const auto &internals = detail::get_internals(); tstate = PyEval_SaveThread(); if (disassoc) { - auto key = detail::get_internals().tstate; + auto key = internals.tstate; #if PY_MAJOR_VERSION < 3 PyThread_delete_key_value(key); #else diff --git a/tests/test_embed/CMakeLists.txt b/tests/test_embed/CMakeLists.txt index a651031f5..0a43e0e22 100644 --- a/tests/test_embed/CMakeLists.txt +++ b/tests/test_embed/CMakeLists.txt @@ -26,6 +26,9 @@ else() target_link_libraries(test_embed PRIVATE ${PYTHON_LIBRARIES}) endif() +find_package(Threads REQUIRED) +target_link_libraries(test_embed PUBLIC ${CMAKE_THREAD_LIBS_INIT}) + add_custom_target(cpptest COMMAND $ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) add_dependencies(check cpptest) diff --git a/tests/test_embed/test_interpreter.cpp b/tests/test_embed/test_interpreter.cpp index 58f791a61..2d5823c39 100644 --- a/tests/test_embed/test_interpreter.cpp +++ b/tests/test_embed/test_interpreter.cpp @@ -1,6 +1,8 @@ #include #include +#include + namespace py = pybind11; using namespace py::literals; @@ -185,3 +187,32 @@ TEST_CASE("Execution frame") { py::exec("var = dict(number=42)"); REQUIRE(py::globals()["var"]["number"].cast() == 42); } + +TEST_CASE("Threads") { + // Restart interpreter to ensure threads are not initialized + py::finalize_interpreter(); + py::initialize_interpreter(); + REQUIRE_FALSE(has_pybind11_internals_static()); + + constexpr auto num_threads = 10; + auto locals = py::dict("count"_a=0); + + { + py::gil_scoped_release gil_release{}; + REQUIRE(has_pybind11_internals_static()); + + auto threads = std::vector(); + for (auto i = 0; i < num_threads; ++i) { + threads.emplace_back([&]() { + py::gil_scoped_acquire gil{}; + py::exec("count += 1", py::globals(), locals); + }); + } + + for (auto &thread : threads) { + thread.join(); + } + } + + REQUIRE(locals["count"].cast() == num_threads); +}