Skip to content

feat: scoped_critical_section #5684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 23, 2025
Merged

Conversation

henryiii
Copy link
Collaborator

@henryiii henryiii commented May 23, 2025

Description

This adds a wrapper, py::scoped_critical_section. Simplifies the usage in our tests. I think this will likely allow us to fix our GIL tests, as long as we find the right place to include it. Updated the docs, which I believe were missing a .ptr() before.

Suggested changelog entry:

  • Added py::scoped_critical_section for free-threaded use.

📚 Documentation preview 📚: https://pybind11--5684.org.readthedocs.build/

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)

class scoped_critical_section {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion to move this to gil_simple.h, but with this name (scoped_critical_section).

Why: It will be usable without bringing in #include "detail/internals.h"

Copy link
Collaborator Author

@henryiii henryiii May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just make a new file (edit: done). What's the status of gil_simple? Should we have changed the default? Don't remember if there were drawbacks. @tonybaloney was talking to me at PyCon about GIL management issues, I wonder if that would help.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just make a new file (edit: done).

I really like that!

The code looks good to me as-is. I didn't look at the test failures, I guess you're it it?

Should we have changed the default?

That crossed my mind, too, but I came down on the side: it's best to not open that can of worms.

With v3.0.0, we can tell users to explicitly switch to gil_scoped_acquire_simple or gil_scoped_release_simple if they have a reason.

I.e.: We're not causing a stir by changing the default, and it's very easy to opt-in to the simple variants (simply append _simple, no other changes required, I think).

@henryiii henryiii force-pushed the henryiii/chore/pycrit branch 2 times, most recently from ff9280f to 198f04b Compare May 23, 2025 16:47
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
@henryiii henryiii force-pushed the henryiii/chore/pycrit branch from 1496dff to 70852d5 Compare May 23, 2025 16:51
pre-commit-ci bot and others added 2 commits May 23, 2025 16:52
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
@henryiii henryiii force-pushed the henryiii/chore/pycrit branch from e767cc7 to 4db3dba Compare May 23, 2025 19:11
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
@henryiii henryiii requested a review from Copilot May 23, 2025 21:14
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new wrapper, py::scoped_critical_section, to simplify and improve thread synchronization in tests that require free‐threaded Python usage. Key changes include adding the new critical section header and its implementation, updating test files to use the new wrapper, and revising documentation and build configuration accordingly.

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/test_embed/test_interpreter.cpp Updated thread test to use py::scoped_critical_section.
tests/extra_python_package/test_files.py Added the new critical_section header to the file list.
include/pybind11/critical_section.h New implementation of the scoped_critical_section class.
docs/advanced/misc.rst Revised docs to reflect usage and emphasize correct lines.
CMakeLists.txt Added critical_section.h to header installation list.

Comment on lines 371 to +373
# endif
#else
locals["count"] = locals["count"].cast<int>() + 1;
#endif
locals["count"] = locals["count"].cast<int>() + 1;
Copy link
Preview

Copilot AI May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the conditional block for Py_GIL_DISABLED, the update to locals['count'] appears inside the lock for the older branch but is performed outside for the new branch. To ensure consistent thread safety, consider moving the update inside the critical section scope in all branches.

Copilot uses AI. Check for mistakes.

@henryiii henryiii merged commit d7769de into pybind:master May 23, 2025
74 checks passed
@henryiii henryiii deleted the henryiii/chore/pycrit branch May 23, 2025 21:41
@github-actions github-actions bot added the needs changelog Possibly needs a changelog entry label May 23, 2025
Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The production code looks great, but I'm still worried about sending inexperienced users into deadlock pitfalls. See suggestions.

@@ -365,15 +366,11 @@ TEST_CASE("Threads") {
#ifdef Py_GIL_DISABLED
# if PY_VERSION_HEX < 0x030E0000
std::lock_guard<std::mutex> lock(mutex);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're touching this code, I think it'll be prudent to warn readers about the classic pitfall:

                 py::gil_scoped_acquire gil{};
 #ifdef Py_GIL_DISABLED
 #    if PY_VERSION_HEX < 0x030E0000
+                // WARNING: This assumes that the Python C API call below does
+                // not release and reacquire the GIL. Guarding Python object
+                // access with a C++ mutex (while holding the GIL) can
+                // deadlock if that assumption is violated.
                 std::lock_guard<std::mutex> lock(mutex);
 #    else
+                // Safe: uses CPython's thread-safe API in no-GIL mode.
                 py::scoped_critical_section lock(locals);
 #    endif
 #endif

Python. You can have it lock one or two Python objects. You cannot nest it.
(Note: In Python 3.13t, Python re-locks if you enter a critical section again,
which happens in various places. This was optimized away in 3.14+. Use a
``std::mutex`` instead if this is a problem).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern: I feel like this could send inexperienced users into the deadlock pitfall.

Please take a look here for background:

https://chatgpt.com/share/682a07ec-dd70-8008-92ea-e722ff92a055

Concrete suggestions from the chat:

Add a comment to clarify:

            py::scoped_critical_section guard(g); // Safe only if this is not nested and no GIL is held.

Suggested addition to the explanatory text below the code block:


⚠️ When using py::scoped_critical_section, make sure it is not nested and that no other synchronization primitives (such as the GIL or a std::mutex) are held that could lead to deadlocks. This RAII guard wraps Python’s PyCriticalSection_* APIs, which are only active in free-threaded builds (Py_GIL_DISABLED). In standard (GIL-enabled) Python builds, the guard is a no-op.

On Python 3.13, nested use of critical sections may re-lock underlying internal structures; this was optimized away in Python 3.14+. If needed, consider using a std::mutex to avoid such cases.


My own refinement of the last point: Depending on the situation, you may have to resort to a std::mutex.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

py::scoped_critical_section is safe WRT the GIL, because it is GIL aware, so "such as the GIL" doesn't apply to it. But std::mutex could deadlock.

@rwgk
Copy link
Collaborator

rwgk commented May 23, 2025

Oh, merged already? I hope you can still consider the doc and comment suggestions.

@henryiii
Copy link
Collaborator Author

Ahh, sorry, I thought you said it looked good above. I can make a new PR with suggestions.

@henryiii henryiii removed the needs changelog Possibly needs a changelog entry label May 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants