Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add runtime selection of GIL implementation #1322

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
138 changes: 138 additions & 0 deletions include/pybind11/detail/gil_internals.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
pybind11/detail/gil_internals.h: Containers for GIL implementations

Copyright (c) 2018 Kitware Inc. <kyle.edwards@kitware.com>

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<typename Obj>
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<typename Impl>
struct gil_impl_container : public gil_impl_container_base {
gil_container_base *create_release() const override {
return new gil_container<typename Impl::release>;
}

gil_container_base *create_acquire() const override {
return new gil_container<typename Impl::acquire>;
}

/*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_gil_impl> default_impl;
std::unique_ptr<gil_impl_container_base> 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<capsule>(builtins[id])) {
gil_internals_pp = static_cast<gil_internals **>(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_current *>(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<gil_internals_current &>(internals);
} else {
throw std::runtime_error("Incompatible gil_internals version");
}
}

template<typename Impl>
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<Impl>)))
throw std::runtime_error("Conflicting GIL requirements");
} else {
internals.selected_impl.reset(new gil_impl_container<Impl>);
}
}

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)
188 changes: 188 additions & 0 deletions include/pybind11/gil.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
pybind11/gil.h: GIL implementations

Copyright (c) 2018 Kitware Inc. <kyle.edwards@kitware.com>

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();
}*/
};

/* 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 {
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<typename Impl>
void select_gil_impl() {
detail::select_gil_impl<Impl>();
}

NAMESPACE_END(PYBIND11_NAMESPACE)
Loading