Skip to content

Commit 31e169c

Browse files
Add option to use the PyGILState API in gil_scoped_acquire (pybind#1276)
1 parent 2d0507d commit 31e169c

File tree

6 files changed

+144
-31
lines changed

6 files changed

+144
-31
lines changed

include/pybind11/options.h

+7
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,18 @@ class options {
3838

3939
options& enable_function_signatures() & { global_state().show_function_signatures = true; return *this; }
4040

41+
options& disable_use_gilstate() & { global_state().use_gilstate = false; return *this; }
42+
43+
options& enable_use_gilstate() & { global_state().use_gilstate = true; return *this; }
44+
4145
// Getter methods (return the global state):
4246

4347
static bool show_user_defined_docstrings() { return global_state().show_user_defined_docstrings; }
4448

4549
static bool show_function_signatures() { return global_state().show_function_signatures; }
4650

51+
static bool use_gilstate() { return global_state().use_gilstate; }
52+
4753
// This type is not meant to be allocated on the heap.
4854
void* operator new(size_t) = delete;
4955

@@ -52,6 +58,7 @@ class options {
5258
struct state {
5359
bool show_user_defined_docstrings = true; //< Include user-supplied texts in docstrings.
5460
bool show_function_signatures = true; //< Include auto-generated function signatures in docstrings.
61+
bool use_gilstate = false; //< Use the PyGILState API in gil_scoped_acquire.
5562
};
5663

5764
static state &global_state() {

include/pybind11/pybind11.h

+47-31
Original file line numberDiff line numberDiff line change
@@ -1749,42 +1749,53 @@ void print(Args &&...args) {
17491749
* example which uses features 2 and 3 to migrate the Python thread of
17501750
* execution to another thread (to run the event loop on the original thread,
17511751
* in this case).
1752+
*
1753+
* Due to the fact that gil_scoped_acquire creates its own internal thread
1754+
* state, this can get out of sync with the PyGILState_* API's thread state,
1755+
* causing GIL deadlocks in complicated setups with third party code that
1756+
* calls the PyGILState_* functions. To force gil_scoped_acquire to use the
1757+
* PyGILState_* API, create a pybind11::options object and call
1758+
* options::enable_use_gilstate().
17521759
*/
17531760

17541761
class gil_scoped_acquire {
17551762
public:
17561763
PYBIND11_NOINLINE gil_scoped_acquire() {
1757-
auto const &internals = detail::get_internals();
1758-
tstate = (PyThreadState *) PyThread_get_key_value(internals.tstate);
1759-
1760-
if (!tstate) {
1761-
tstate = PyThreadState_New(internals.istate);
1762-
#if !defined(NDEBUG)
1763-
if (!tstate)
1764-
pybind11_fail("scoped_acquire: could not create thread state!");
1765-
#endif
1766-
tstate->gilstate_counter = 0;
1767-
#if PY_MAJOR_VERSION < 3
1768-
PyThread_delete_key_value(internals.tstate);
1769-
#endif
1770-
PyThread_set_key_value(internals.tstate, tstate);
1764+
if (options::use_gilstate()) {
1765+
state = PyGILState_Ensure();
17711766
} else {
1772-
release = detail::get_thread_state_unchecked() != tstate;
1773-
}
1767+
auto const &internals = detail::get_internals();
1768+
tstate = (PyThreadState *) PyThread_get_key_value(internals.tstate);
17741769

1775-
if (release) {
1776-
/* Work around an annoying assertion in PyThreadState_Swap */
1777-
#if defined(Py_DEBUG)
1778-
PyInterpreterState *interp = tstate->interp;
1779-
tstate->interp = nullptr;
1780-
#endif
1781-
PyEval_AcquireThread(tstate);
1782-
#if defined(Py_DEBUG)
1783-
tstate->interp = interp;
1784-
#endif
1785-
}
1770+
if (!tstate) {
1771+
tstate = PyThreadState_New(internals.istate);
1772+
#if !defined(NDEBUG)
1773+
if (!tstate)
1774+
pybind11_fail("scoped_acquire: could not create thread state!");
1775+
#endif
1776+
tstate->gilstate_counter = 0;
1777+
#if PY_MAJOR_VERSION < 3
1778+
PyThread_delete_key_value(internals.tstate);
1779+
#endif
1780+
PyThread_set_key_value(internals.tstate, tstate);
1781+
} else {
1782+
release = detail::get_thread_state_unchecked() != tstate;
1783+
}
17861784

1787-
inc_ref();
1785+
if (release) {
1786+
/* Work around an annoying assertion in PyThreadState_Swap */
1787+
#if defined(Py_DEBUG)
1788+
PyInterpreterState *interp = tstate->interp;
1789+
tstate->interp = nullptr;
1790+
#endif
1791+
PyEval_AcquireThread(tstate);
1792+
#if defined(Py_DEBUG)
1793+
tstate->interp = interp;
1794+
#endif
1795+
}
1796+
1797+
inc_ref();
1798+
}
17881799
}
17891800

17901801
void inc_ref() {
@@ -1812,12 +1823,17 @@ class gil_scoped_acquire {
18121823
}
18131824

18141825
PYBIND11_NOINLINE ~gil_scoped_acquire() {
1815-
dec_ref();
1816-
if (release)
1817-
PyEval_SaveThread();
1826+
if (options::use_gilstate()) {
1827+
PyGILState_Release(state);
1828+
} else {
1829+
dec_ref();
1830+
if (release)
1831+
PyEval_SaveThread();
1832+
}
18181833
}
18191834
private:
18201835
PyThreadState *tstate = nullptr;
1836+
PyGILState_STATE state;
18211837
bool release = true;
18221838
};
18231839

tests/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ set(PYBIND11_TEST_FILES
5757
test_smart_ptr.cpp
5858
test_stl.cpp
5959
test_stl_binders.cpp
60+
test_use_gilstate.cpp
6061
test_virtual_functions.cpp
6162
)
6263

tests/conftest.py

+5
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ def pytest_namespace():
199199
from pybind11_tests.eigen import have_eigen
200200
except ImportError:
201201
have_eigen = False
202+
try:
203+
import threading
204+
except ImportError:
205+
threading = None
202206
pypy = platform.python_implementation() == "PyPy"
203207

204208
skipif = pytest.mark.skipif
@@ -210,6 +214,7 @@ def pytest_namespace():
210214
reason="eigen and/or numpy are not installed"),
211215
'requires_eigen_and_scipy': skipif(not have_eigen or not scipy,
212216
reason="eigen and/or scipy are not installed"),
217+
'requires_threading': skipif(not threading, reason="no threading"),
213218
'unsupported_on_pypy': skipif(pypy, reason="unsupported on PyPy"),
214219
'unsupported_on_py2': skipif(sys.version_info.major < 3,
215220
reason="unsupported on Python 2.x"),

tests/test_use_gilstate.cpp

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#include "pybind11_tests.h"
2+
3+
#if defined(WITH_THREAD)
4+
5+
#include <thread>
6+
7+
static bool check_internal(bool option_use_gilstate) {
8+
auto const &internals = py::detail::get_internals();
9+
#if !defined(PYPY_VERSION)
10+
if (option_use_gilstate)
11+
return !PyThread_get_key_value(internals.tstate);
12+
else
13+
return !!PyThread_get_key_value(internals.tstate);
14+
#else
15+
(void)option_use_gilstate;
16+
return !PyThread_get_key_value(internals.tstate);
17+
#endif
18+
}
19+
20+
TEST_SUBMODULE(use_gilstate, m) {
21+
m.def("check_use_gilstate", [](bool option_use_gilstate) -> bool {
22+
py::options options;
23+
if (option_use_gilstate)
24+
options.enable_use_gilstate();
25+
else
26+
options.disable_use_gilstate();
27+
{
28+
py::gil_scoped_acquire acquire;
29+
return check_internal(option_use_gilstate);
30+
}
31+
}, py::call_guard<py::gil_scoped_release>())
32+
.def("check_default", []() -> bool {
33+
py::gil_scoped_acquire acquire;
34+
return check_internal(false);
35+
}, py::call_guard<py::gil_scoped_release>())
36+
.def("check_use_gilstate_cthread", [](bool option_use_gilstate) -> bool {
37+
py::options options;
38+
if (option_use_gilstate)
39+
options.enable_use_gilstate();
40+
else
41+
options.disable_use_gilstate();
42+
43+
bool result = false;
44+
std::thread thread([option_use_gilstate, &result]() {
45+
py::gil_scoped_acquire acquire;
46+
result = check_internal(option_use_gilstate);
47+
});
48+
thread.join();
49+
return result;
50+
}, py::call_guard<py::gil_scoped_release>());
51+
}
52+
53+
#endif

tests/test_use_gilstate.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import pytest
2+
3+
pytestmark = pytest.requires_threading
4+
5+
with pytest.suppress(ImportError):
6+
import threading
7+
from pybind11_tests import use_gilstate as ug
8+
9+
10+
def run_in_thread(target, args=()):
11+
def thread_routine(target, args):
12+
threading.current_thread()._return = target(*args)
13+
14+
thread = threading.Thread(target=thread_routine, args=(target, args))
15+
thread.start()
16+
thread.join()
17+
return thread._return
18+
19+
20+
def test_use_gilstate():
21+
assert run_in_thread(target=ug.check_use_gilstate, args=(False,))
22+
assert run_in_thread(target=ug.check_use_gilstate, args=(True,))
23+
24+
25+
def test_default():
26+
assert run_in_thread(target=ug.check_default)
27+
28+
29+
def test_cthread():
30+
assert ug.check_use_gilstate_cthread(False)
31+
assert ug.check_use_gilstate_cthread(True)

0 commit comments

Comments
 (0)