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

Add PYBIND11_SIMPLE_GIL_MANAGEMENT option (cmake, C++ define) #4216

Merged
merged 34 commits into from Oct 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
75acc11
Add option to force the use of the PYPY GIL scoped acquire/release lo…
SeeChange-CI Oct 5, 2022
1cd4cff
Apply suggestions from code review
henryiii Oct 21, 2022
b855af2
Update CMakeLists.txt
henryiii Oct 21, 2022
fb233f0
docs: update upgrade guide
henryiii Oct 23, 2022
7c4f8fe
Update docs/upgrade.rst
henryiii Oct 23, 2022
6f7317e
All bells & whistles.
rwgk Oct 26, 2022
cf1bb86
Add Reminder to common.h, so that we will not forget to purge `!WITH_…
rwgk Oct 26, 2022
8885e79
New sentence instead of semicolon.
rwgk Oct 26, 2022
f2538ba
Temporarily pull in snapshot of PR #4246
rwgk Oct 26, 2022
52629ac
Add `test_release_acquire`
rwgk Oct 26, 2022
4062387
Add more unit tests for nested gil locking
SeeChange-CI Oct 27, 2022
31b3868
Add test_report_builtins_internals_keys
rwgk Oct 27, 2022
87e50b0
Very minor enhancement: sort list only after filtering.
rwgk Oct 27, 2022
fe7470d
Revert change in docs/upgrade.rst
rwgk Oct 28, 2022
897c9dd
Add test_multi_acquire_release_cross_module, while also forcing uniqu…
rwgk Oct 28, 2022
620cc70
Hopefully fix apparently new ICC error.
rwgk Oct 28, 2022
a8ac941
clang-tidy fixes
rwgk Oct 28, 2022
e25bcd9
Workaround for PYPY WIN exitcode None
rwgk Oct 28, 2022
1e72313
Revert "Temporarily pull in snapshot of PR #4246"
rwgk Oct 28, 2022
af1d9ee
Another workaround for PYPY WIN exitcode None
rwgk Oct 28, 2022
83c099d
Clean up how the tests are run "run in process" Part 1: uniformity
rwgk Oct 28, 2022
ee8353e
Clean up how the tests are run "run in process" Part 2: use `@pytest.…
rwgk Oct 28, 2022
5019457
Skip some tests `#if defined(THREAD_SANITIZER)` (tested with TSAN usi…
rwgk Oct 28, 2022
ab45337
Run all tests again but ignore ThreadSanitizer exitcode 66 (this is l…
rwgk Oct 28, 2022
857ee1a
bug fix: missing common.h include before using `PYBIND11_SIMPLE_GIL_M…
rwgk Oct 28, 2022
10b0334
if process.exitcode is None: assert t_delta > 9.9
rwgk Oct 29, 2022
ea8b132
More sophisiticated `_run_in_process()` implementation, clearly repor…
rwgk Oct 30, 2022
4c19a9a
Wrap m.intentional_deadlock in a Python function, for `ForkingPickler…
rwgk Oct 30, 2022
2c5f864
Add link to potential solution for WOULD-BE-NICE-TO-HAVE feature.
rwgk Oct 30, 2022
19513d4
Add `SKIP_IF_DEADLOCK = True` option, to not pollute the CI results w…
rwgk Oct 30, 2022
5ce396e
Add COPY-PASTE-THIS: gdb ... command (to be used for debugging the de…
rwgk Oct 30, 2022
8c9bce0
style: pre-commit fixes
pre-commit-ci[bot] Oct 30, 2022
065ac50
Do better than automatic pre-commit fixes.
rwgk Oct 30, 2022
6984cab
Add `PYBIND11_SIMPLE_GIL_MANAGEMENT` to `pytest_report_header()` (so …
rwgk Oct 30, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -102,10 +102,12 @@ jobs:
run: python -m pip install pytest-github-actions-annotate-failures

# First build - C++11 mode and inplace
# More-or-less randomly adding -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON here.
- name: Configure C++11 ${{ matrix.args }}
run: >
cmake -S . -B .
-DPYBIND11_WERROR=ON
-DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=11
Expand All @@ -129,10 +131,12 @@ jobs:
run: git clean -fdx

# Second build - C++17 mode and in a build directory
# More-or-less randomly adding -DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF here.
- name: Configure C++17
run: >
cmake -S . -B build2
-DPYBIND11_WERROR=ON
-DPYBIND11_SIMPLE_GIL_MANAGEMENT=OFF
-DDOWNLOAD_CATCH=ON
-DDOWNLOAD_EIGEN=ON
-DCMAKE_CXX_STANDARD=17
Expand Down
6 changes: 6 additions & 0 deletions CMakeLists.txt
Expand Up @@ -91,10 +91,16 @@ endif()
option(PYBIND11_INSTALL "Install pybind11 header files?" ${PYBIND11_MASTER_PROJECT})
option(PYBIND11_TEST "Build pybind11 test suite?" ${PYBIND11_MASTER_PROJECT})
option(PYBIND11_NOPYTHON "Disable search for Python" OFF)
option(PYBIND11_SIMPLE_GIL_MANAGEMENT
"Use simpler GIL management logic that does not support disassociation" OFF)
set(PYBIND11_INTERNALS_VERSION
""
CACHE STRING "Override the ABI version, may be used to enable the unstable ABI.")

if(PYBIND11_SIMPLE_GIL_MANAGEMENT)
add_compile_definitions(PYBIND11_SIMPLE_GIL_MANAGEMENT)
endif()

cmake_dependent_option(
USE_PYTHON_INCLUDE_DIR
"Install pybind11 headers in Python include directory instead of default installation prefix"
Expand Down
5 changes: 5 additions & 0 deletions include/pybind11/detail/common.h
Expand Up @@ -206,6 +206,7 @@
#endif

#include <Python.h>
// Reminder: WITH_THREAD is always defined if PY_VERSION_HEX >= 0x03070000
#if PY_VERSION_HEX < 0x03060000
# error "PYTHON < 3.6 IS UNSUPPORTED. pybind11 v2.9 was the last to support Python 2 and 3.5."
#endif
Expand All @@ -229,6 +230,10 @@
# undef copysign
#endif

#if defined(PYPY_VERSION) && !defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
# define PYBIND11_SIMPLE_GIL_MANAGEMENT
#endif

#if defined(_MSC_VER)
# if defined(PYBIND11_DEBUG_MARKER)
# define _DEBUG
Expand Down
16 changes: 15 additions & 1 deletion include/pybind11/detail/internals.h
Expand Up @@ -9,6 +9,12 @@

#pragma once

#include "common.h"

#if defined(WITH_THREAD) && defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
# include "../gil.h"
#endif

#include "../pytypes.h"

#include <exception>
Expand Down Expand Up @@ -49,7 +55,7 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass);
// `Py_LIMITED_API` anyway.
# if PYBIND11_INTERNALS_VERSION > 4
# define PYBIND11_TLS_KEY_REF Py_tss_t &
# ifdef __GNUC__
# if defined(__GNUC__) && !defined(__INTEL_COMPILER)
// Clang on macOS warns due to `Py_tss_NEEDS_INIT` not specifying an initializer
// for every field.
# define PYBIND11_TLS_KEY_INIT(var) \
Expand Down Expand Up @@ -169,10 +175,12 @@ struct internals {
PyTypeObject *default_metaclass;
PyObject *instance_base;
#if defined(WITH_THREAD)
// Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined:
PYBIND11_TLS_KEY_INIT(tstate)
# if PYBIND11_INTERNALS_VERSION > 4
PYBIND11_TLS_KEY_INIT(loader_life_support_tls_key)
# endif // PYBIND11_INTERNALS_VERSION > 4
// Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined:
PyInterpreterState *istate = nullptr;
~internals() {
# if PYBIND11_INTERNALS_VERSION > 4
Expand Down Expand Up @@ -408,6 +416,10 @@ PYBIND11_NOINLINE internals &get_internals() {
return **internals_pp;
}

#if defined(WITH_THREAD)
# if defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
gil_scoped_acquire gil;
# else
// Ensure that the GIL is held since we will need to make Python calls.
// Cannot use py::gil_scoped_acquire here since that constructor calls get_internals.
struct gil_scoped_acquire_local {
Expand All @@ -417,6 +429,8 @@ PYBIND11_NOINLINE internals &get_internals() {
~gil_scoped_acquire_local() { PyGILState_Release(state); }
const PyGILState_STATE state;
} gil;
# endif
#endif
error_scope err_scope;

PYBIND11_STR_TYPE id(PYBIND11_INTERNALS_ID);
Expand Down
53 changes: 40 additions & 13 deletions include/pybind11/gil.h
Expand Up @@ -10,7 +10,10 @@
#pragma once

#include "detail/common.h"
#include "detail/internals.h"

#if defined(WITH_THREAD) && !defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
# include "detail/internals.h"
#endif

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)

Expand All @@ -21,7 +24,9 @@ PyThreadState *get_thread_state_unchecked();

PYBIND11_NAMESPACE_END(detail)

#if defined(WITH_THREAD) && !defined(PYPY_VERSION)
#if defined(WITH_THREAD)

# if !defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)

/* The functions below essentially reproduce the PyGILState_* API using a RAII
* pattern, but there are a few important differences:
Expand Down Expand Up @@ -62,11 +67,11 @@ class gil_scoped_acquire {

if (!tstate) {
tstate = PyThreadState_New(internals.istate);
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
if (!tstate) {
pybind11_fail("scoped_acquire: could not create thread state!");
}
# endif
# endif
tstate->gilstate_counter = 0;
PYBIND11_TLS_REPLACE_VALUE(internals.tstate, tstate);
} else {
Expand All @@ -87,20 +92,20 @@ class gil_scoped_acquire {

PYBIND11_NOINLINE void dec_ref() {
--tstate->gilstate_counter;
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
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
# endif
if (tstate->gilstate_counter == 0) {
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
# if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
if (!release) {
pybind11_fail("scoped_acquire::dec_ref(): internal error!");
}
# endif
# endif
PyThreadState_Clear(tstate);
if (active) {
PyThreadState_DeleteCurrent();
Expand Down Expand Up @@ -178,12 +183,14 @@ class gil_scoped_release {
bool disassoc;
bool active = true;
};
#elif defined(PYPY_VERSION)

# else // PYBIND11_SIMPLE_GIL_MANAGEMENT

class gil_scoped_acquire {
PyGILState_STATE state;

public:
gil_scoped_acquire() { state = PyGILState_Ensure(); }
gil_scoped_acquire() : state{PyGILState_Ensure()} {}
gil_scoped_acquire(const gil_scoped_acquire &) = delete;
gil_scoped_acquire &operator=(const gil_scoped_acquire &) = delete;
~gil_scoped_acquire() { PyGILState_Release(state); }
Expand All @@ -194,19 +201,39 @@ class gil_scoped_release {
PyThreadState *state;

public:
gil_scoped_release() { state = PyEval_SaveThread(); }
gil_scoped_release() : state{PyEval_SaveThread()} {}
gil_scoped_release(const gil_scoped_release &) = delete;
gil_scoped_release &operator=(const gil_scoped_acquire &) = delete;
~gil_scoped_release() { PyEval_RestoreThread(state); }
void disarm() {}
};
#else

# endif // PYBIND11_SIMPLE_GIL_MANAGEMENT

#else // WITH_THREAD
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: Do we follow this style anywhere else? I find it quite confusing, because it reads like this is the "WITH_THREAD" block, while it's actually the not WITH_THREAD block. For things like this, they should be enforced (such as the comment at the end of a namespace is forced). I don't think it can, because this style can't be expanded to an elif chain.

Also this block goes away when we drop 3.6, right? We should be able to deduce that when we remove the WITH_THREAD.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: Do we follow this style anywhere else?

In the wild I see negations, or not. I don't think it's consistent in the world, and that's clearly confusing.

What do we want to follow here?

I decided even with that ambiguity, having the markers is more helpful than not, because the first scope (non-simple) is quite large.

Ultimately, something like this would be best:

// No ifdefs
gil_scoped_aquire_non_simple ...
...
gil_scoped_acquire_simple ...

#if defined PYBIND11_SIMPLE_GIL_MANAGEMENT
using gil_scoped_acquire = gil_scoped_acquire_simple;
#else
using gil_scoped_acquire = gil_scoped_acquire_non_simple;
#endif

Although if we push it there I'd want to find better names than simple and non-simple.

Please let me know what you prefer:

  • What's here.
  • Different style.
  • The (or a different) fully formal solution.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with it as is (that's why it's a nit, can be ignored). This code will simplify later when we remove 3.6. I'm just not a fan of commented out code, it can be confusing (on the else, it's ambiguous), and it can easy become wrong if things get moved around (no test for it matching up). I'd rather use indentation and then visually check, just like I already have to do if I don't trust comments like this (which I don't), and everyone has to do on most other if/else blocks.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately, something like this would be best

This would be wasteful. You'd be processing and compiling both sides, while you'd only use one. Unless we added gil_scoped_acquire_simple/gil_scoped_acquire_notsimple to our API, which I don't think we want to do, then it's much harder to remove the complex one later.


class gil_scoped_acquire {
public:
gil_scoped_acquire() {
// Trick to suppress `unused variable` error messages (at call sites).
(void) (this != (this + 1));
}
gil_scoped_acquire(const gil_scoped_acquire &) = delete;
gil_scoped_acquire &operator=(const gil_scoped_acquire &) = delete;
void disarm() {}
};

class gil_scoped_release {
public:
gil_scoped_release() {
// Trick to suppress `unused variable` error messages (at call sites).
(void) (this != (this + 1));
}
gil_scoped_release(const gil_scoped_release &) = delete;
gil_scoped_release &operator=(const gil_scoped_acquire &) = delete;
void disarm() {}
};
#endif

#endif // WITH_THREAD

PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
1 change: 1 addition & 0 deletions tests/conftest.py
Expand Up @@ -210,4 +210,5 @@ def pytest_report_header(config):
f" {pybind11_tests.compiler_info}"
f" {pybind11_tests.cpp_std}"
f" {pybind11_tests.PYBIND11_INTERNALS_ID}"
f" PYBIND11_SIMPLE_GIL_MANAGEMENT={pybind11_tests.PYBIND11_SIMPLE_GIL_MANAGEMENT}"
)
67 changes: 65 additions & 2 deletions tests/cross_module_gil_utils.cpp
Expand Up @@ -6,9 +6,15 @@
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/
#if defined(PYBIND11_INTERNALS_VERSION)
# undef PYBIND11_INTERNALS_VERSION
#endif
#define PYBIND11_INTERNALS_VERSION 21814642 // Ensure this module has its own `internals` instance.
#include <pybind11/pybind11.h>

#include <cstdint>
#include <string>
#include <thread>

// This file mimics a DSO that makes pybind11 calls but does not define a
// PYBIND11_MODULE. The purpose is to test that such a DSO can create a
Expand All @@ -21,24 +27,81 @@
namespace {

namespace py = pybind11;

void gil_acquire() { py::gil_scoped_acquire gil; }

std::string gil_multi_acquire_release(unsigned bits) {
if ((bits & 0x1u) != 0u) {
py::gil_scoped_acquire gil;
}
if ((bits & 0x2u) != 0u) {
py::gil_scoped_release gil;
}
if ((bits & 0x4u) != 0u) {
py::gil_scoped_acquire gil;
}
if ((bits & 0x8u) != 0u) {
py::gil_scoped_release gil;
}
return PYBIND11_INTERNALS_ID;
}

struct CustomAutoGIL {
CustomAutoGIL() : gstate(PyGILState_Ensure()) {}
~CustomAutoGIL() { PyGILState_Release(gstate); }

PyGILState_STATE gstate;
};
struct CustomAutoNoGIL {
CustomAutoNoGIL() : save(PyEval_SaveThread()) {}
~CustomAutoNoGIL() { PyEval_RestoreThread(save); }

PyThreadState *save;
};

template <typename Acquire, typename Release>
void gil_acquire_inner() {
Acquire acquire_outer;
Acquire acquire_inner;
Release release;
}

template <typename Acquire, typename Release>
void gil_acquire_nested() {
Acquire acquire_outer;
Acquire acquire_inner;
Release release;
auto thread = std::thread(&gil_acquire_inner<Acquire, Release>);
thread.join();
}

constexpr char kModuleName[] = "cross_module_gil_utils";

struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT, kModuleName, nullptr, 0, nullptr, nullptr, nullptr, nullptr, nullptr};

} // namespace

#define ADD_FUNCTION(Name, ...) \
PyModule_AddObject(m, Name, PyLong_FromVoidPtr(reinterpret_cast<void *>(&__VA_ARGS__)));

extern "C" PYBIND11_EXPORT PyObject *PyInit_cross_module_gil_utils() {

PyObject *m = PyModule_Create(&moduledef);

if (m != nullptr) {
static_assert(sizeof(&gil_acquire) == sizeof(void *),
"Function pointer must have the same size as void*");
PyModule_AddObject(
m, "gil_acquire_funcaddr", PyLong_FromVoidPtr(reinterpret_cast<void *>(&gil_acquire)));
ADD_FUNCTION("gil_acquire_funcaddr", gil_acquire)
ADD_FUNCTION("gil_multi_acquire_release_funcaddr", gil_multi_acquire_release)
ADD_FUNCTION("gil_acquire_inner_custom_funcaddr",
gil_acquire_inner<CustomAutoGIL, CustomAutoNoGIL>)
ADD_FUNCTION("gil_acquire_nested_custom_funcaddr",
gil_acquire_nested<CustomAutoGIL, CustomAutoNoGIL>)
ADD_FUNCTION("gil_acquire_inner_pybind11_funcaddr",
gil_acquire_inner<py::gil_scoped_acquire, py::gil_scoped_release>)
ADD_FUNCTION("gil_acquire_nested_pybind11_funcaddr",
gil_acquire_nested<py::gil_scoped_acquire, py::gil_scoped_release>)
}

return m;
Expand Down
6 changes: 6 additions & 0 deletions tests/pybind11_tests.cpp
Expand Up @@ -89,6 +89,12 @@ PYBIND11_MODULE(pybind11_tests, m) {
#endif
m.attr("cpp_std") = cpp_std();
m.attr("PYBIND11_INTERNALS_ID") = PYBIND11_INTERNALS_ID;
m.attr("PYBIND11_SIMPLE_GIL_MANAGEMENT") =
#if defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
true;
#else
false;
#endif

bind_ConstructorStats(m);

Expand Down
1 change: 0 additions & 1 deletion tests/test_embed/test_interpreter.cpp
Expand Up @@ -293,7 +293,6 @@ TEST_CASE("Threads") {

{
py::gil_scoped_release gil_release{};
REQUIRE(has_pybind11_internals_static());

auto threads = std::vector<std::thread>();
for (auto i = 0; i < num_threads; ++i) {
Expand Down