Skip to content

Commit d7769de

Browse files
feat: scoped_critical_section (#5684)
* feat: scoped_critical_section Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * refactor: pull out to file Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * style: pre-commit fixes * fix: GIL code in some compilers Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * fix: move to correct spot Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> --------- Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e3883dd commit d7769de

File tree

5 files changed

+67
-11
lines changed

5 files changed

+67
-11
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ set(PYBIND11_HEADERS
205205
include/pybind11/conduit/pybind11_conduit_v1.h
206206
include/pybind11/conduit/pybind11_platform_abi_id.h
207207
include/pybind11/conduit/wrap_include_python_h.h
208+
include/pybind11/critical_section.h
208209
include/pybind11/options.h
209210
include/pybind11/eigen.h
210211
include/pybind11/eigen/common.h

docs/advanced/misc.rst

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,22 +295,29 @@ 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.
302+
(Note: In Python 3.13t, Python re-locks if you enter a critical section again,
303+
which happens in various places. This was optimized away in 3.14+. Use a
304+
``std::mutex`` instead if this is a problem).
300305

301306
.. code-block:: cpp
302-
:emphasize-lines: 1,5,10
307+
:emphasize-lines: 1,4,8
308+
309+
#include <pybind11/critical_section.h>
310+
// ...
303311
304312
PYBIND11_MODULE(example, m, py::multiple_interpreters::per_interpreter_gil(), py::mod_gil_not_used()) {
305313
m.def("calc_next", []() {
306314
size_t old;
307315
py::dict g = py::globals();
308-
Py_BEGIN_CRITICAL_SECTION(g);
316+
py::scoped_critical_section guard(g);
309317
if (!g.contains("myseed"))
310318
g["myseed"] = 0;
311319
old = g["myseed"];
312320
g["myseed"] = (old + 1) * 10;
313-
Py_END_CRITICAL_SECTION();
314321
return old;
315322
});
316323
}

include/pybind11/critical_section.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2016-2025 The Pybind Development Team.
2+
// All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
#pragma once
6+
7+
#include "pytypes.h"
8+
9+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
10+
11+
/// This does not do anything if there's a GIL. On free-threaded Python,
12+
/// it locks an object. This uses the CPython API, which has limits
13+
class scoped_critical_section {
14+
public:
15+
#ifdef Py_GIL_DISABLED
16+
explicit scoped_critical_section(handle obj) : has2(false) {
17+
PyCriticalSection_Begin(&section, obj.ptr());
18+
}
19+
20+
scoped_critical_section(handle obj1, handle obj2) : has2(true) {
21+
PyCriticalSection2_Begin(&section2, obj1.ptr(), obj2.ptr());
22+
}
23+
24+
~scoped_critical_section() {
25+
if (has2) {
26+
PyCriticalSection2_End(&section2);
27+
} else {
28+
PyCriticalSection_End(&section);
29+
}
30+
}
31+
#else
32+
explicit scoped_critical_section(handle) {};
33+
scoped_critical_section(handle, handle) {};
34+
~scoped_critical_section() = default;
35+
#endif
36+
37+
scoped_critical_section(const scoped_critical_section &) = delete;
38+
scoped_critical_section &operator=(const scoped_critical_section &) = delete;
39+
40+
private:
41+
#ifdef Py_GIL_DISABLED
42+
bool has2;
43+
union {
44+
PyCriticalSection section;
45+
PyCriticalSection2 section2;
46+
};
47+
#endif
48+
};
49+
50+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/extra_python_package/test_files.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"include/pybind11/chrono.h",
4545
"include/pybind11/common.h",
4646
"include/pybind11/complex.h",
47+
"include/pybind11/critical_section.h",
4748
"include/pybind11/eigen.h",
4849
"include/pybind11/embed.h",
4950
"include/pybind11/eval.h",

tests/test_embed/test_interpreter.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include <pybind11/critical_section.h>
12
#include <pybind11/embed.h>
23

34
// Silence MSVC C++17 deprecation warning from Catch regarding std::uncaught_exceptions (up to
@@ -365,15 +366,11 @@ TEST_CASE("Threads") {
365366
#ifdef Py_GIL_DISABLED
366367
# if PY_VERSION_HEX < 0x030E0000
367368
std::lock_guard<std::mutex> lock(mutex);
368-
locals["count"] = locals["count"].cast<int>() + 1;
369369
# else
370-
Py_BEGIN_CRITICAL_SECTION(locals.ptr());
371-
locals["count"] = locals["count"].cast<int>() + 1;
372-
Py_END_CRITICAL_SECTION();
370+
py::scoped_critical_section lock(locals);
373371
# endif
374-
#else
375-
locals["count"] = locals["count"].cast<int>() + 1;
376372
#endif
373+
locals["count"] = locals["count"].cast<int>() + 1;
377374
});
378375
}
379376

0 commit comments

Comments
 (0)