Skip to content

Commit 84f7892

Browse files
author
Vincent Palancher
committed
qipython.guard: Adds GIL manipulation types.
In `pybind11`, GIL manipulation RAII guard types are not reentrant (at least `gil_scoped_release` is not) which means we cannot reliably use it in subfunctions without having previous knowledge of the GIL state. This patch adds new re-entrant types which wrap `pybind11` own guards. These new types also check at destruction if the interpreter is finalizing, potentially (if `pybind11` supports it) disabling the guard, thus avoiding a thread termination. It also replaces any occurrences of previous guards with the new ones. Change-Id: Ie9b134ce05df1787ae5c7b93eae7e750fb317123 Reviewed-on: http://gerrit2.aldebaran.lan/1792 Reviewed-by: jmonnon <jmonnon@aldebaran.com> Reviewed-by: philippe.martin <philippe.martin@softbankrobotics.com> Tested-by: vincent.palancher <vincent.palancher@softbankrobotics.com>
1 parent da9074f commit 84f7892

26 files changed

Lines changed: 422 additions & 275 deletions

qipython/common.hpp

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@
1919
#include <future>
2020
#include <thread>
2121

22+
23+
// MAJOR, MINOR and PATCH must be in [0, 255].
24+
#define QI_PYBIND11_VERSION(MAJOR, MINOR, PATCH) \
25+
(((MAJOR) << 16) | \
26+
((MINOR) << 8) | \
27+
((PATCH) << 0))
28+
29+
#ifdef PYBIND11_VERSION_HEX
30+
// Remove the lowest 8 bits which represent the serial and level version components.
31+
# define QI_CURRENT_PYBIND11_VERSION (PYBIND11_VERSION_HEX >> 8)
32+
#else
33+
# define QI_CURRENT_PYBIND11_VERSION \
34+
QI_PYBIND11_VERSION(PYBIND11_VERSION_MAJOR, \
35+
PYBIND11_VERSION_MINOR, \
36+
PYBIND11_VERSION_PATCH)
37+
#endif
38+
2239
namespace qi
2340
{
2441
namespace py
@@ -93,32 +110,20 @@ boost::optional<T> extractKeywordArg(pybind11::dict kwargs,
93110
return pyArg.cast<T>();
94111
}
95112

96-
/// A deleter that deletes the pointer outside of the GIL.
97-
///
98-
/// Useful for types that might deadlock on destruction if they keep the GIL
99-
/// locked.
100-
struct DeleteOutsideGIL
101-
{
102-
template<typename T>
103-
void operator()(T* ptr) const
104-
{
105-
pybind11::gil_scoped_release unlock;
106-
delete ptr;
107-
}
108-
};
109-
110-
/// Delay the destruction of an object to a thread.
111-
struct DeleteInOtherThread
113+
/// Returns whether or not the interpreter is finalizing. If the information
114+
/// could not be fetched, the function returns an empty optional. Otherwise, it
115+
/// returns an optional set with a boolean stating if the interpreter is indeed
116+
/// finalizing or not.
117+
inline boost::optional<bool> interpreterIsFinalizing()
112118
{
113-
template<typename T>
114-
void operator()(T* ptr) const
115-
{
116-
pybind11::gil_scoped_release unlock;
117-
auto fut = std::async(std::launch::async, [](std::unique_ptr<T>) {},
118-
std::unique_ptr<T>(ptr));
119-
QI_IGNORE_UNUSED(fut);
120-
}
121-
};
119+
// `_Py_IsFinalizing` is only available on CPython 3.7+
120+
#if PY_VERSION_HEX >= 0x03070000
121+
return boost::make_optional(_Py_IsFinalizing() != 0);
122+
#else
123+
// There is no way of knowing on older versions.
124+
return {};
125+
#endif
126+
}
122127

123128
} // namespace py
124129
} // namespace qi

qipython/pyfuture.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ using Promise = qi::Promise<AnyValue>;
2525
// future or its value.
2626
inline pybind11::object resultObject(const Future& fut, bool async)
2727
{
28-
pybind11::gil_scoped_acquire lock;
28+
GILAcquire lock;
2929
if (async)
3030
return castToPyObject(fut);
3131

3232
// Wait for the future outside of the GIL.
33-
auto res = invokeGuarded<pybind11::gil_scoped_release>(qi::SrcFuture{}, fut);
33+
auto res = invokeGuarded<GILRelease>(qi::SrcFuture{}, fut);
3434
return castToPyObject(res);
3535
}
3636

qipython/pyguard.hpp

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ namespace qi
1717
namespace py
1818
{
1919

20+
/// Returns whether or not the GIL is currently held by the current thread.
21+
inline bool currentThreadHoldsGil()
22+
{
23+
return PyGILState_Check() == 1;
24+
}
25+
2026
/// DefaultConstructible Guard
2127
/// Procedure<_ (Args)> F
2228
template<typename Guard, typename F, typename... Args>
@@ -121,13 +127,115 @@ class Guarded
121127
explicit operator const Object&() const { return object(); }
122128
};
123129

130+
/// G == pybind11::gil_scoped_acquire || G == pybind11::gil_scoped_release
131+
template<typename G>
132+
void pybind11GuardDisarm(G& guard)
133+
{
134+
QI_IGNORE_UNUSED(guard);
135+
// The disarm API was introduced in v2.6.2.
136+
#if QI_CURRENT_PYBIND11_VERSION >= QI_PYBIND11_VERSION(2,6,2)
137+
guard.disarm();
138+
#endif
139+
}
140+
124141
} // namespace detail
125142

143+
/// RAII utility type that guarantees that the GIL is locked for the scope of
144+
/// the lifetime of the object.
145+
///
146+
/// This type is re-entrant.
147+
///
148+
/// postcondition: `GILAcquire acq;` establishes `currentThreadHoldsGil()`
149+
struct GILAcquire
150+
{
151+
inline GILAcquire()
152+
{
153+
// `gil_scoped_acquire` is re-entrant by itself, so we don't need to check
154+
// whether or not the GIL is already held by the current thread.
155+
QI_ASSERT(currentThreadHoldsGil());
156+
}
157+
158+
GILAcquire(const GILAcquire&) = delete;
159+
GILAcquire& operator=(const GILAcquire&) = delete;
160+
161+
inline ~GILAcquire()
162+
{
163+
const auto optIsFinalizing = interpreterIsFinalizing();
164+
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
165+
if (definitelyFinalizing)
166+
detail::pybind11GuardDisarm(_acq);
167+
}
168+
169+
private:
170+
pybind11::gil_scoped_acquire _acq;
171+
};
172+
173+
/// RAII utility type that guarantees that the GIL is unlocked for the scope of
174+
/// the lifetime of the object.
175+
///
176+
/// This type is re-entrant.
177+
///
178+
/// postcondition: `GILRelease rel;` establishes `!currentThreadHoldsGil()`
179+
struct GILRelease
180+
{
181+
inline GILRelease()
182+
{
183+
if (currentThreadHoldsGil())
184+
_release.emplace();
185+
QI_ASSERT(!currentThreadHoldsGil());
186+
}
187+
188+
GILRelease(const GILRelease&) = delete;
189+
GILRelease& operator=(const GILRelease&) = delete;
190+
191+
inline ~GILRelease()
192+
{
193+
const auto optIsFinalizing = interpreterIsFinalizing();
194+
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
195+
if (_release && definitelyFinalizing)
196+
detail::pybind11GuardDisarm(*_release);
197+
}
198+
199+
private:
200+
boost::optional<pybind11::gil_scoped_release> _release;
201+
};
202+
126203
/// Wraps a pybind11::object value and locks the GIL on copy and destruction.
127204
///
128205
/// This is useful for instance to put pybind11 objects in lambda functions so
129206
/// that they can be copied around safely.
130-
using GILGuardedObject = detail::Guarded<pybind11::gil_scoped_acquire, pybind11::object>;
207+
using GILGuardedObject = detail::Guarded<GILAcquire, pybind11::object>;
208+
209+
/// Deleter that deletes the pointer outside the GIL.
210+
///
211+
/// Useful for types that might deadlock on destruction if they keep the GIL
212+
/// locked.
213+
struct DeleteOutsideGIL
214+
{
215+
template<typename T>
216+
void operator()(T* ptr) const
217+
{
218+
GILRelease unlock;
219+
delete ptr;
220+
}
221+
};
222+
223+
/// Deleter that delays the destruction of an object to another thread.
224+
struct DeleteInOtherThread
225+
{
226+
template<typename T>
227+
void operator()(T* ptr) const
228+
{
229+
GILRelease unlock;
230+
// `std::async` returns an object, that unless moved from will block when
231+
// destroyed until the task is complete, which is unwanted behavior here.
232+
// Therefore we just take the result of the `std::async` call into a local
233+
// variable and then ignore it.
234+
auto fut = std::async(std::launch::async, [](std::unique_ptr<T>) {},
235+
std::unique_ptr<T>(ptr));
236+
QI_IGNORE_UNUSED(fut);
237+
}
238+
};
131239

132240
} // namespace py
133241
} // namespace qi

src/pyapplication.cpp

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <qi/log.hpp>
1616

1717
#include <qipython/common.hpp>
18+
#include <qipython/pyguard.hpp>
1819
#include <qipython/pyapplication.hpp>
1920
#include <qipython/pysession.hpp>
2021

@@ -80,25 +81,25 @@ void exportApplication(::py::module& m)
8081
using namespace ::py;
8182
using namespace ::py::literals;
8283

83-
gil_scoped_acquire lock;
84+
GILAcquire lock;
8485

8586
class_<Application, std::unique_ptr<Application, DeleteInOtherThread>>(
8687
m, "Application")
8788
.def(init(withArgcArgv<>([](int& argc, char**& argv) {
88-
gil_scoped_release unlock;
89+
GILRelease unlock;
8990
return new Application(argc, argv);
9091
})),
9192
"args"_a)
92-
.def_static("run", &Application::run, call_guard<gil_scoped_release>())
93-
.def_static("stop", &Application::stop, call_guard<gil_scoped_release>());
93+
.def_static("run", &Application::run, call_guard<GILRelease>())
94+
.def_static("stop", &Application::stop, call_guard<GILRelease>());
9495

9596
class_<ApplicationSession,
9697
std::unique_ptr<ApplicationSession, DeleteInOtherThread>>(
9798
m, "ApplicationSession")
9899

99100
.def(init(withArgcArgv<bool, const std::string&>(
100101
[](int& argc, char**& argv, bool autoExit, const std::string& url) {
101-
gil_scoped_release unlock;
102+
GILRelease unlock;
102103
ApplicationSession::Config config;
103104
if (!autoExit)
104105
config.setOption(qi::ApplicationSession::Option_NoAutoExit);
@@ -108,30 +109,30 @@ void exportApplication(::py::module& m)
108109
})),
109110
"args"_a, "autoExit"_a, "url"_a)
110111

111-
.def("run", &ApplicationSession::run, call_guard<gil_scoped_release>(),
112+
.def("run", &ApplicationSession::run, call_guard<GILRelease>(),
112113
doc("Block until the end of the program (call "
113114
":py:func:`qi.ApplicationSession.stop` to end the program)."))
114115

115116
.def_static("stop", &ApplicationSession::stop,
116-
call_guard<gil_scoped_release>(),
117+
call_guard<GILRelease>(),
117118
doc(
118119
"Ask the application to stop, the run function will return."))
119120

120121
.def("start", &ApplicationSession::startSession,
121-
call_guard<gil_scoped_release>(),
122+
call_guard<GILRelease>(),
122123
doc("Start the connection of the session, once this function is "
123124
"called everything is fully initialized and working."))
124125

125126
.def_static("atRun", &ApplicationSession::atRun,
126-
call_guard<gil_scoped_release>(), "func"_a,
127+
call_guard<GILRelease>(), "func"_a,
127128
doc(
128129
"Add a callback that will be executed when run() is called."))
129130

130131
.def_property_readonly("url",
131132
[](const ApplicationSession& app) {
132133
return app.url().str();
133134
},
134-
call_guard<gil_scoped_release>(),
135+
call_guard<GILRelease>(),
135136
doc("The url given to the Application. It's the url "
136137
"used to connect the session."))
137138

src/pyasync.cpp

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <qipython/pysignal.hpp>
77
#include <qipython/common.hpp>
8+
#include <qipython/pyguard.hpp>
89
#include <qipython/pyfuture.hpp>
910
#include <qipython/pyobject.hpp>
1011
#include <qipython/pystrand.hpp>
@@ -29,7 +30,7 @@ ::py::object async(::py::function pyCallback,
2930
::py::args args,
3031
::py::kwargs kwargs)
3132
{
32-
::py::gil_scoped_acquire lock;
33+
GILAcquire lock;
3334

3435
qi::uint64_t usDelay = 0;
3536
if (const auto optUsDelay = extractKeywordArg<qi::uint64_t>(kwargs, delayArgName))
@@ -42,7 +43,7 @@ ::py::object async(::py::function pyCallback,
4243
GILGuardedObject guardKwargs(std::move(kwargs));
4344

4445
auto invokeCallback = [=]() mutable {
45-
::py::gil_scoped_acquire lock;
46+
GILAcquire lock;
4647
auto res = ::py::object((*guardCb)(**guardArgs, ***guardKwargs)).cast<AnyValue>();
4748
// Release references immediately while we hold the GIL, instead of waiting
4849
// for the lambda destructor to relock the GIL.
@@ -79,7 +80,7 @@ void exportAsync(::py::module& m)
7980
using namespace ::py;
8081
using namespace ::py::literals;
8182

82-
gil_scoped_acquire lock;
83+
GILAcquire lock;
8384

8485
m.def("runAsync", &async,
8586
"callback"_a,
@@ -106,7 +107,7 @@ void exportAsync(::py::module& m)
106107
[](PeriodicTask& task, qi::int64_t usPeriod) {
107108
task.setPeriod(qi::MicroSeconds(usPeriod));
108109
},
109-
call_guard<gil_scoped_release>(), "usPeriod"_a,
110+
call_guard<GILRelease>(), "usPeriod"_a,
110111
doc("Set the call interval in microseconds.\n"
111112
"This call will wait until next callback invocation to apply the "
112113
"change.\n"
@@ -118,7 +119,7 @@ void exportAsync(::py::module& m)
118119
" task.setUsPeriod(100)\n"
119120
" task.start()\n"
120121
":param usPeriod: the period in microseconds"))
121-
.def("start", &PeriodicTask::start, call_guard<gil_scoped_release>(),
122+
.def("start", &PeriodicTask::start, call_guard<GILRelease>(),
122123
"immediate"_a,
123124
doc("Start the periodic task at specified period. No effect if "
124125
"already running.\n"
@@ -127,20 +128,20 @@ void exportAsync(::py::module& m)
127128
".. warning::\n"
128129
" concurrent calls to start() and stop() will result in "
129130
"undefined behavior."))
130-
.def("stop", &PeriodicTask::stop, call_guard<gil_scoped_release>(),
131+
.def("stop", &PeriodicTask::stop, call_guard<GILRelease>(),
131132
doc("Stop the periodic task. When this function returns, the callback "
132133
"will not be called "
133134
"anymore. Can be called from within the callback function\n"
134135
".. warning::\n"
135136
" concurrent calls to start() and stop() will result in "
136137
"undefined behavior."))
137138
.def("asyncStop", &PeriodicTask::asyncStop,
138-
call_guard<gil_scoped_release>(),
139+
call_guard<GILRelease>(),
139140
doc("Request for periodic task to stop asynchronously.\n"
140141
"Can be called from within the callback function."))
141142
.def(
142143
"compensateCallbackTime", &PeriodicTask::compensateCallbackTime,
143-
call_guard<gil_scoped_release>(), "compensate"_a,
144+
call_guard<GILRelease>(), "compensate"_a,
144145
doc(
145146
":param compensate: boolean. True to activate the compensation.\n"
146147
"When compensation is activated, call interval will take into account "
@@ -149,7 +150,7 @@ void exportAsync(::py::module& m)
149150
" when the callback is longer than the specified period, "
150151
"compensation will result in the callback being called successively "
151152
"without pause."))
152-
.def("setName", &PeriodicTask::setName, call_guard<gil_scoped_release>(),
153+
.def("setName", &PeriodicTask::setName, call_guard<GILRelease>(),
153154
"name"_a, doc("Set name for debugging and tracking purpose"))
154155
.def("isRunning", &PeriodicTask::isRunning,
155156
doc(":returns: true if task is running"))

src/pyclock.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <qipython/pyclock.hpp>
77
#include <qipython/common.hpp>
8+
#include <qipython/pyguard.hpp>
89
#include <qi/clock.hpp>
910
#include <pybind11/pybind11.h>
1011

@@ -31,7 +32,7 @@ void exportClock(::py::module& m)
3132
using namespace ::py;
3233
using namespace ::py::literals;
3334

34-
gil_scoped_acquire lock;
35+
GILAcquire lock;
3536

3637
m.def("clockNow", &now<Clock>,
3738
doc(":returns: current timestamp on qi::Clock, as a number of nanoseconds"));

0 commit comments

Comments
 (0)