Skip to content

Commit 3af2d84

Browse files
committed
feat: scoped_critical_section
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 98bd78f commit 3af2d84

File tree

3 files changed

+46
-9
lines changed

3 files changed

+46
-9
lines changed

docs/advanced/misc.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,10 @@ This module is sub-interpreter safe, for both ``shared_gil`` ("legacy") and
295295
function concurrently from different threads. This is safe because each sub-interpreter's GIL
296296
protects it's own Python objects from concurrent access.
297297

298-
However, the module is no longer free-threading safe, for the same reason as before, because the
299-
calculation is not synchronized. We can synchronize it using a Python critical section.
298+
However, the module is no longer free-threading safe, for the same reason as
299+
before, because the calculation is not synchronized. We can synchronize it
300+
using a Python critical section. This will do nothing if not in free-threaded
301+
Python. You can have it lock one or two Python objects. You cannot nest it.
300302

301303
.. code-block:: cpp
302304
:emphasize-lines: 1,5,10
@@ -305,12 +307,11 @@ calculation is not synchronized. We can synchronize it using a Python critical s
305307
m.def("calc_next", []() {
306308
size_t old;
307309
py::dict g = py::globals();
308-
Py_BEGIN_CRITICAL_SECTION(g);
310+
py::scoped_critical_section guard(g);
309311
if (!g.contains("myseed"))
310312
g["myseed"] = 0;
311313
old = g["myseed"];
312314
g["myseed"] = (old + 1) * 10;
313-
Py_END_CRITICAL_SECTION();
314315
return old;
315316
});
316317
}

include/pybind11/gil.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,43 @@ class gil_scoped_release {
199199
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
200200

201201
#endif // !PYBIND11_SIMPLE_GIL_MANAGEMENT
202+
203+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
204+
205+
class scoped_critical_section {
206+
public:
207+
/// This does not do anything if there's a GIL. On free-threaded Python,
208+
/// it locks an object. This uses the CPython API, which has limits
209+
scoped_critical_section(handle obj) : single(true) {
210+
#ifdef Py_GIL_DISABLED
211+
PyCriticalSection_Begin(&section, obj.ptr());
212+
#endif
213+
}
214+
215+
scoped_critical_section(handle obj1, handle obj2) : single(true) {
216+
#ifdef Py_GIL_DISABLED
217+
PyCriticalSection2_Begin(&section2, obj1.ptr(), obj2.ptr());
218+
#endif
219+
}
220+
221+
~scoped_critical_section() {
222+
#ifdef Py_GIL_DISABLED
223+
if(single) {
224+
PyCriticalSection_End(&section);
225+
} else {
226+
PyCriticalSection2_End(&section2);
227+
}
228+
#endif
229+
}
230+
231+
private:
232+
bool single;
233+
#ifdef Py_GIL_DISABLED
234+
union {
235+
PyCriticalSection section;
236+
PyCriticalSection2 section2;
237+
};
238+
#endif
239+
};
240+
241+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/test_embed/test_interpreter.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -365,13 +365,9 @@ TEST_CASE("Threads") {
365365
#ifdef Py_GIL_DISABLED
366366
# if PY_VERSION_HEX < 0x030E0000
367367
std::lock_guard<std::mutex> lock(mutex);
368-
locals["count"] = locals["count"].cast<int>() + 1;
369368
# else
370-
Py_BEGIN_CRITICAL_SECTION(locals.ptr());
371-
locals["count"] = locals["count"].cast<int>() + 1;
372-
Py_END_CRITICAL_SECTION();
369+
py::scoped_critical_section lock(locals);
373370
# endif
374-
#else
375371
locals["count"] = locals["count"].cast<int>() + 1;
376372
#endif
377373
});

0 commit comments

Comments
 (0)