Skip to content

Commit e2b884c

Browse files
bzarcowjakob
authored andcommitted
Use PyGILState_GetThisThreadState when using gil_scoped_acquire. (#1211)
This avoids GIL deadlocking when pybind11 tries to acquire the GIL in a thread that already acquired it using standard Python API (e.g. when running from a Python thread).
1 parent 81da988 commit e2b884c

File tree

4 files changed

+133
-0
lines changed

4 files changed

+133
-0
lines changed

include/pybind11/pybind11.h

+9
Original file line numberDiff line numberDiff line change
@@ -1871,6 +1871,15 @@ class gil_scoped_acquire {
18711871
auto const &internals = detail::get_internals();
18721872
tstate = (PyThreadState *) PYBIND11_TLS_GET_VALUE(internals.tstate);
18731873

1874+
if (!tstate) {
1875+
/* Check if the GIL was acquired using the PyGILState_* API instead (e.g. if
1876+
calling from a Python thread). Since we use a different key, this ensures
1877+
we don't create a new thread state and deadlock in PyEval_AcquireThread
1878+
below. Note we don't save this state with internals.tstate, since we don't
1879+
create it we would fail to clear it (its reference count should be > 0). */
1880+
tstate = PyGILState_GetThisThreadState();
1881+
}
1882+
18741883
if (!tstate) {
18751884
tstate = PyThreadState_New(internals.istate);
18761885
#if !defined(NDEBUG)

tests/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ set(PYBIND11_TEST_FILES
4040
test_eval.cpp
4141
test_exceptions.cpp
4242
test_factory_constructors.cpp
43+
test_gil_scoped.cpp
4344
test_iostream.cpp
4445
test_kwargs_and_defaults.cpp
4546
test_local_bindings.cpp

tests/test_gil_scoped.cpp

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
tests/test_gil_scoped.cpp -- acquire and release gil
3+
4+
Copyright (c) 2017 Borja Zarco (Google LLC) <[email protected]>
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
10+
#include "pybind11_tests.h"
11+
#include <pybind11/functional.h>
12+
13+
14+
class VirtClass {
15+
public:
16+
virtual void virtual_func() {}
17+
virtual void pure_virtual_func() = 0;
18+
};
19+
20+
class PyVirtClass : public VirtClass {
21+
void virtual_func() override {
22+
PYBIND11_OVERLOAD(void, VirtClass, virtual_func,);
23+
}
24+
void pure_virtual_func() override {
25+
PYBIND11_OVERLOAD_PURE(void, VirtClass, pure_virtual_func,);
26+
}
27+
};
28+
29+
TEST_SUBMODULE(gil_scoped, m) {
30+
py::class_<VirtClass, PyVirtClass>(m, "VirtClass")
31+
.def(py::init<>())
32+
.def("virtual_func", &VirtClass::virtual_func)
33+
.def("pure_virtual_func", &VirtClass::pure_virtual_func);
34+
35+
m.def("test_callback_py_obj",
36+
[](py::object func) { func(); });
37+
m.def("test_callback_std_func",
38+
[](const std::function<void()> &func) { func(); });
39+
m.def("test_callback_virtual_func",
40+
[](VirtClass &virt) { virt.virtual_func(); });
41+
m.def("test_callback_pure_virtual_func",
42+
[](VirtClass &virt) { virt.pure_virtual_func(); });
43+
}

tests/test_gil_scoped.py

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import multiprocessing
2+
import threading
3+
from pybind11_tests import gil_scoped as m
4+
5+
6+
def _run_in_process(target, *args, **kwargs):
7+
"""Runs target in process and returns its exitcode after 1s (None if still alive)."""
8+
process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
9+
process.daemon = True
10+
try:
11+
process.start()
12+
# Do not need to wait much, 1s should be more than enough.
13+
process.join(timeout=1)
14+
return process.exitcode
15+
finally:
16+
if process.is_alive():
17+
process.terminate()
18+
19+
20+
def _python_to_cpp_to_python():
21+
"""Calls different C++ functions that come back to Python."""
22+
class ExtendedVirtClass(m.VirtClass):
23+
def virtual_func(self):
24+
pass
25+
26+
def pure_virtual_func(self):
27+
pass
28+
29+
extended = ExtendedVirtClass()
30+
m.test_callback_py_obj(lambda: None)
31+
m.test_callback_std_func(lambda: None)
32+
m.test_callback_virtual_func(extended)
33+
m.test_callback_pure_virtual_func(extended)
34+
35+
36+
def _python_to_cpp_to_python_from_threads(num_threads, parallel=False):
37+
"""Calls different C++ functions that come back to Python, from Python threads."""
38+
threads = []
39+
for _ in range(num_threads):
40+
thread = threading.Thread(target=_python_to_cpp_to_python)
41+
thread.daemon = True
42+
thread.start()
43+
if parallel:
44+
threads.append(thread)
45+
else:
46+
thread.join()
47+
for thread in threads:
48+
thread.join()
49+
50+
51+
def test_python_to_cpp_to_python_from_thread():
52+
"""Makes sure there is no GIL deadlock when running in a thread.
53+
54+
It runs in a separate process to be able to stop and assert if it deadlocks.
55+
"""
56+
assert _run_in_process(_python_to_cpp_to_python_from_threads, 1) == 0
57+
58+
59+
def test_python_to_cpp_to_python_from_thread_multiple_parallel():
60+
"""Makes sure there is no GIL deadlock when running in a thread multiple times in parallel.
61+
62+
It runs in a separate process to be able to stop and assert if it deadlocks.
63+
"""
64+
assert _run_in_process(_python_to_cpp_to_python_from_threads, 8, parallel=True) == 0
65+
66+
67+
def test_python_to_cpp_to_python_from_thread_multiple_sequential():
68+
"""Makes sure there is no GIL deadlock when running in a thread multiple times sequentially.
69+
70+
It runs in a separate process to be able to stop and assert if it deadlocks.
71+
"""
72+
assert _run_in_process(_python_to_cpp_to_python_from_threads, 8, parallel=False) == 0
73+
74+
75+
def test_python_to_cpp_to_python_from_process():
76+
"""Makes sure there is no GIL deadlock when using processes.
77+
78+
This test is for completion, but it was never an issue.
79+
"""
80+
assert _run_in_process(_python_to_cpp_to_python) == 0

0 commit comments

Comments
 (0)