From 512e9d80c70f23bd82f042ed7a1987cf63cc1c13 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Wed, 14 Mar 2018 16:29:07 -0400 Subject: [PATCH 1/2] Start adding GIL implementation machinery --- CMakeLists.txt | 2 + include/pybind11/detail/gil_internals.h | 138 ++++++++++++++++++++ include/pybind11/gil.h | 167 ++++++++++++++++++++++++ include/pybind11/pybind11.h | 1 + 4 files changed, 308 insertions(+) create mode 100644 include/pybind11/detail/gil_internals.h create mode 100644 include/pybind11/gil.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4280ba742d..85a2238828 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ set(PYBIND11_HEADERS include/pybind11/detail/class.h include/pybind11/detail/common.h include/pybind11/detail/descr.h + include/pybind11/detail/gil_internals.h include/pybind11/detail/init.h include/pybind11/detail/internals.h include/pybind11/detail/typeid.h @@ -58,6 +59,7 @@ set(PYBIND11_HEADERS include/pybind11/embed.h include/pybind11/eval.h include/pybind11/functional.h + include/pybind11/gil.h include/pybind11/numpy.h include/pybind11/operators.h include/pybind11/pybind11.h diff --git a/include/pybind11/detail/gil_internals.h b/include/pybind11/detail/gil_internals.h new file mode 100644 index 0000000000..30191582b1 --- /dev/null +++ b/include/pybind11/detail/gil_internals.h @@ -0,0 +1,138 @@ +/* + pybind11/detail/gil_internals.h: Containers for GIL implementations + + Copyright (c) 2018 Kitware Inc. + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include "common.h" +#include "internals.h" + +#include "../gil.h" + +NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +class basic_gil_impl; + +// Change this to change the default GIL implementation +typedef basic_gil_impl default_gil_impl; + +NAMESPACE_BEGIN(detail) + +#define PYBIND11_GIL_INTERNALS_MAJOR_VERSION 1 +#define PYBIND11_GIL_INTERNALS_MINOR_VERSION 0 +#define PYBIND11_GIL_INTERNALS_ID "__pybind11_gil_internals__" + +struct gil_container_base { + virtual ~gil_container_base() = default; +}; + +template +struct gil_container : public gil_container_base { + Obj obj; +}; + +struct gil_impl_container_base { + virtual gil_container_base *create_release() const = 0; + virtual gil_container_base *create_acquire() const = 0; + //virtual bool thread_has_impl() const = 0; +}; + +template +struct gil_impl_container : public gil_impl_container_base { + gil_container_base *create_release() const override { + return new gil_container; + } + + gil_container_base *create_acquire() const override { + return new gil_container; + } + + /*bool thread_has_impl() const override { + return Impl::thread_has_impl(); + }*/ +}; + +/// Internal data structure used to track desired GIL behavior. +struct gil_internals { + unsigned int major_version; + unsigned int minor_version; +}; + +/// V1 of the gil_internals structure. +struct gil_internals_v1_0 : public gil_internals { + gil_impl_container default_impl; + std::unique_ptr selected_impl; +}; + +typedef gil_internals_v1_0 gil_internals_current; + +inline gil_internals **&get_gil_internals_pp() { + static gil_internals **gil_internals_pp = nullptr; + return gil_internals_pp; +} + +/// Return a reference to the current `gil_internals` data +PYBIND11_NOINLINE inline gil_internals &get_gil_internals_base() { + auto **&gil_internals_pp = get_gil_internals_pp(); + if (gil_internals_pp && *gil_internals_pp) + return **gil_internals_pp; + + constexpr auto *id = PYBIND11_GIL_INTERNALS_ID; + auto builtins = handle(PyEval_GetBuiltins()); + if (builtins.contains(id) && isinstance(builtins[id])) { + gil_internals_pp = static_cast(capsule(builtins[id])); + } else { + if (!gil_internals_pp) gil_internals_pp = new gil_internals*(); + gil_internals *&gil_internals_ptr = *gil_internals_pp; + gil_internals_ptr = new gil_internals_current; + builtins[id] = capsule(gil_internals_pp); + + gil_internals_v1_0 *ptr = static_cast(gil_internals_ptr); + ptr->major_version = PYBIND11_GIL_INTERNALS_MAJOR_VERSION; + ptr->minor_version = PYBIND11_GIL_INTERNALS_MINOR_VERSION; + ptr->selected_impl = nullptr; + } + return **gil_internals_pp; +} + +// When PYBIND11_GIL_INTERNALS_MINOR_VERSION is 0, a warning is triggered +#pragma GCC diagnostic ignored "-Wtype-limits" +PYBIND11_NOINLINE inline gil_internals_current &get_gil_internals() { + gil_internals &internals = get_gil_internals_base(); + if (internals.major_version == PYBIND11_GIL_INTERNALS_MAJOR_VERSION && + internals.minor_version >= PYBIND11_GIL_INTERNALS_MINOR_VERSION) { + return static_cast(internals); + } else { + throw std::runtime_error("Incompatible gil_internals version"); + } +} + +template +void select_gil_impl() { + gil_internals_current &internals = get_gil_internals(); + + if (internals.selected_impl) { + if (!same_type(typeid(*internals.selected_impl), typeid(gil_impl_container))) + throw std::runtime_error("Conflicting GIL requirements"); + } else { + internals.selected_impl.reset(new gil_impl_container); + } +} + +PYBIND11_NOINLINE inline gil_impl_container_base &get_selected_gil_impl() { + gil_internals_current &internals = get_gil_internals(); + + if (internals.selected_impl) { + return *internals.selected_impl; + } else { + return internals.default_impl; + } +} + +NAMESPACE_END(detail) +NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/gil.h b/include/pybind11/gil.h new file mode 100644 index 0000000000..4f32b70f78 --- /dev/null +++ b/include/pybind11/gil.h @@ -0,0 +1,167 @@ +/* + pybind11/gil.h: GIL implementations + + Copyright (c) 2018 Kitware Inc. + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#pragma once + +#include "detail/common.h" +#include "detail/gil_internals.h" + +NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +class basic_gil_impl { +public: + class acquire { + public: + acquire() { + state = PyGILState_Ensure(); + } + + ~acquire() { + PyGILState_Release(state); + } + + private: + PyGILState_STATE state; + }; + + class release { + public: + release() { + state = PyEval_SaveThread(); + } + + ~release() { + PyEval_RestoreThread(state); + } + + private: + PyThreadState *state; + }; + + /*static bool thread_has_impl() { + return !!PyGILState_GetThisThreadState(); + }*/ +}; + +class advanced_gil_impl { +public: + class acquire { + public: + PYBIND11_NOINLINE acquire() { + auto const &internals = detail::get_internals(); + tstate = (PyThreadState *) PyThread_get_key_value(internals.tstate); + + if (!tstate) { + tstate = PyThreadState_New(internals.istate); + #if !defined(NDEBUG) + if (!tstate) + pybind11_fail("scoped_acquire: could not create thread state!"); + #endif + tstate->gilstate_counter = 0; + #if PY_MAJOR_VERSION < 3 + PyThread_delete_key_value(internals.tstate); + #endif + PyThread_set_key_value(internals.tstate, tstate); + } else { + release = detail::get_thread_state_unchecked() != tstate; + } + + if (release) { + /* Work around an annoying assertion in PyThreadState_Swap */ + #if defined(Py_DEBUG) + PyInterpreterState *interp = tstate->interp; + tstate->interp = nullptr; + #endif + PyEval_AcquireThread(tstate); + #if defined(Py_DEBUG) + tstate->interp = interp; + #endif + } + + inc_ref(); + } + + void inc_ref() { + ++tstate->gilstate_counter; + } + + PYBIND11_NOINLINE void dec_ref() { + --tstate->gilstate_counter; + #if !defined(NDEBUG) + if (detail::get_thread_state_unchecked() != tstate) + pybind11_fail("scoped_acquire::dec_ref(): thread state must be current!"); + if (tstate->gilstate_counter < 0) + pybind11_fail("scoped_acquire::dec_ref(): reference count underflow!"); + #endif + if (tstate->gilstate_counter == 0) { + #if !defined(NDEBUG) + if (!release) + pybind11_fail("scoped_acquire::dec_ref(): internal error!"); + #endif + PyThreadState_Clear(tstate); + PyThreadState_DeleteCurrent(); + PyThread_delete_key_value(detail::get_internals().tstate); + release = false; + } + } + + ~acquire() { + dec_ref(); + if (release) + PyEval_SaveThread(); + } + + private: + PyThreadState *tstate = nullptr; + bool release = true; + }; + + class release { + public: + explicit 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 = internals.tstate; + #if PY_MAJOR_VERSION < 3 + PyThread_delete_key_value(key); + #else + PyThread_set_key_value(key, nullptr); + #endif + } + } + + ~release() { + if (!tstate) + return; + PyEval_RestoreThread(tstate); + if (disassoc) { + auto key = detail::get_internals().tstate; + #if PY_MAJOR_VERSION < 3 + PyThread_delete_key_value(key); + #endif + PyThread_set_key_value(key, tstate); + } + } + + private: + PyThreadState *tstate; + bool disassoc; + }; +}; + +template +void select_gil_impl() { + detail::select_gil_impl(); +} + +NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index a50de836ff..bbca509f22 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -41,6 +41,7 @@ #endif #include "attr.h" +#include "gil.h" #include "options.h" #include "detail/class.h" #include "detail/init.h" From 71aa38b8cb8c1999e9aec62b560a06b7d06ea64d Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Wed, 14 Mar 2018 16:43:21 -0400 Subject: [PATCH 2/2] Change gil_scoped_acquire/release to use GIL impl --- include/pybind11/gil.h | 21 ++++++ include/pybind11/pybind11.h | 140 ++---------------------------------- 2 files changed, 26 insertions(+), 135 deletions(-) diff --git a/include/pybind11/gil.h b/include/pybind11/gil.h index 4f32b70f78..5d39a4229d 100644 --- a/include/pybind11/gil.h +++ b/include/pybind11/gil.h @@ -49,6 +49,27 @@ class basic_gil_impl { }*/ }; +/* The functions below essentially reproduce the PyGILState_* API using a RAII + * pattern, but there are a few important differences: + * + * 1. When acquiring the GIL from an non-main thread during the finalization + * phase, the GILState API blindly terminates the calling thread, which + * is often not what is wanted. This API does not do this. + * + * 2. The advanced_gil_impl::release function can optionally cut the + * relationship of a PyThreadState and its associated thread, which allows + * moving it to another thread (this is a fairly rare/advanced use case). + * + * 3. The reference count of an acquired thread state can be controlled. This + * can be handy to prevent cases where callbacks issued from an external + * thread would otherwise constantly construct and destroy thread state data + * structures. + * + * See the Python bindings of NanoGUI (http://github.com/wjakob/nanogui) for an + * example which uses features 2 and 3 to migrate the Python thread of + * execution to another thread (to run the event loop on the original thread, + * in this case). + */ class advanced_gil_impl { public: class acquire { diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index bbca509f22..36408e41c3 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1728,151 +1728,21 @@ void print(Args &&...args) { detail::print(c.args(), c.kwargs()); } -#if defined(WITH_THREAD) && !defined(PYPY_VERSION) - -/* The functions below essentially reproduce the PyGILState_* API using a RAII - * pattern, but there are a few important differences: - * - * 1. When acquiring the GIL from an non-main thread during the finalization - * phase, the GILState API blindly terminates the calling thread, which - * is often not what is wanted. This API does not do this. - * - * 2. The gil_scoped_release function can optionally cut the relationship - * of a PyThreadState and its associated thread, which allows moving it to - * another thread (this is a fairly rare/advanced use case). - * - * 3. The reference count of an acquired thread state can be controlled. This - * can be handy to prevent cases where callbacks issued from an external - * thread would otherwise constantly construct and destroy thread state data - * structures. - * - * See the Python bindings of NanoGUI (http://github.com/wjakob/nanogui) for an - * example which uses features 2 and 3 to migrate the Python thread of - * execution to another thread (to run the event loop on the original thread, - * in this case). - */ - class gil_scoped_acquire { public: - PYBIND11_NOINLINE gil_scoped_acquire() { - auto const &internals = detail::get_internals(); - tstate = (PyThreadState *) PyThread_get_key_value(internals.tstate); - - if (!tstate) { - tstate = PyThreadState_New(internals.istate); - #if !defined(NDEBUG) - if (!tstate) - pybind11_fail("scoped_acquire: could not create thread state!"); - #endif - tstate->gilstate_counter = 0; - #if PY_MAJOR_VERSION < 3 - PyThread_delete_key_value(internals.tstate); - #endif - PyThread_set_key_value(internals.tstate, tstate); - } else { - release = detail::get_thread_state_unchecked() != tstate; - } - - if (release) { - /* Work around an annoying assertion in PyThreadState_Swap */ - #if defined(Py_DEBUG) - PyInterpreterState *interp = tstate->interp; - tstate->interp = nullptr; - #endif - PyEval_AcquireThread(tstate); - #if defined(Py_DEBUG) - tstate->interp = interp; - #endif - } - - inc_ref(); - } - - void inc_ref() { - ++tstate->gilstate_counter; - } - - PYBIND11_NOINLINE void dec_ref() { - --tstate->gilstate_counter; - #if !defined(NDEBUG) - if (detail::get_thread_state_unchecked() != tstate) - pybind11_fail("scoped_acquire::dec_ref(): thread state must be current!"); - if (tstate->gilstate_counter < 0) - pybind11_fail("scoped_acquire::dec_ref(): reference count underflow!"); - #endif - if (tstate->gilstate_counter == 0) { - #if !defined(NDEBUG) - if (!release) - pybind11_fail("scoped_acquire::dec_ref(): internal error!"); - #endif - PyThreadState_Clear(tstate); - PyThreadState_DeleteCurrent(); - PyThread_delete_key_value(detail::get_internals().tstate); - release = false; - } - } + gil_scoped_acquire() : gil(detail::get_selected_gil_impl().create_acquire()) {} - PYBIND11_NOINLINE ~gil_scoped_acquire() { - dec_ref(); - if (release) - PyEval_SaveThread(); - } private: - PyThreadState *tstate = nullptr; - bool release = true; + std::unique_ptr gil; }; 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 = internals.tstate; - #if PY_MAJOR_VERSION < 3 - PyThread_delete_key_value(key); - #else - PyThread_set_key_value(key, nullptr); - #endif - } - } - ~gil_scoped_release() { - if (!tstate) - return; - PyEval_RestoreThread(tstate); - if (disassoc) { - auto key = detail::get_internals().tstate; - #if PY_MAJOR_VERSION < 3 - PyThread_delete_key_value(key); - #endif - PyThread_set_key_value(key, tstate); - } - } -private: - PyThreadState *tstate; - bool disassoc; -}; -#elif defined(PYPY_VERSION) -class gil_scoped_acquire { - PyGILState_STATE state; -public: - gil_scoped_acquire() { state = PyGILState_Ensure(); } - ~gil_scoped_acquire() { PyGILState_Release(state); } -}; + gil_scoped_release() : gil(detail::get_selected_gil_impl().create_release()) {} -class gil_scoped_release { - PyThreadState *state; -public: - gil_scoped_release() { state = PyEval_SaveThread(); } - ~gil_scoped_release() { PyEval_RestoreThread(state); } +private: + std::unique_ptr gil; }; -#else -class gil_scoped_acquire { }; -class gil_scoped_release { }; -#endif error_already_set::~error_already_set() { if (type) {