-
Notifications
You must be signed in to change notification settings - Fork 2.2k
fix: handle null py::handle
and add tests for py::scoped_critical_section
#5706
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
henryiii
merged 19 commits into
pybind:master
from
XuehaiPan:handle-null-for-scoped_critical_section
Jun 4, 2025
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
3d20d9d
chore: handle null for `py::scoped_critical_section`
XuehaiPan 7f0b6ff
test: add tests for `py::scoped_critical_section`
XuehaiPan f7006c4
test: use assert instead of REQUIRE
XuehaiPan 543ff87
feat: enable faulthandler for pytest
XuehaiPan 43e7294
chore: use `__has_include(<barrier>)`
XuehaiPan 3da3d28
fix: fix segmentation fault in test
XuehaiPan 5051829
fix: test critical_section for no-gil only
XuehaiPan adbc2b2
test: run new tests only
XuehaiPan 11e037b
test: ensure non-empty test selection
XuehaiPan e2e11fd
fix: fix test critical_section
XuehaiPan 39d0aa4
fix: change Python 3.14.0b1/2 xfail tests to non-strict
XuehaiPan 8b48a0c
test: trigger gc manually
XuehaiPan 009756f
test: mark xfail to `DynamicClass`
XuehaiPan 58f155c
Use `namespace test_scoped_critical_section_ns` (standard approach to…
rwgk 2ce1f8a
Simplify changes in pybind11/critical_section.h and add test_nullptr_…
rwgk ad4203f
test: disable Python devmode in pytest
XuehaiPan 883ac69
test: add comprehensive comments for the tests
XuehaiPan 1b76b0b
test: add a summary comment for tests
XuehaiPan 4b24109
refactor: simpler impl
henryiii File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
#include <pybind11/critical_section.h> | ||
|
||
#include "pybind11_tests.h" | ||
|
||
#include <atomic> | ||
#include <chrono> | ||
#include <thread> | ||
#include <utility> | ||
|
||
#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include(<barrier>) | ||
# define PYBIND11_HAS_BARRIER 1 | ||
# include <barrier> | ||
#endif | ||
|
||
namespace test_scoped_critical_section_ns { | ||
|
||
void test_one_nullptr() { py::scoped_critical_section lock{py::handle{}}; } | ||
|
||
void test_two_nullptrs() { py::scoped_critical_section lock{py::handle{}, py::handle{}}; } | ||
|
||
void test_first_nullptr() { | ||
py::dict d; | ||
py::scoped_critical_section lock{py::handle{}, d}; | ||
} | ||
|
||
void test_second_nullptr() { | ||
py::dict d; | ||
py::scoped_critical_section lock{d, py::handle{}}; | ||
} | ||
Comment on lines
+15
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I was suggesting to add tests, all I had in mind was something simple like this. The additional tests look interesting though. Could you please add comments to explain what the tests are for? (I'd ask my favorite LLM for suggestions, usually it's really quick that way.) |
||
|
||
// Referenced test implementation: https://github.com/PyO3/pyo3/blob/v0.25.0/src/sync.rs#L874 | ||
class BoolWrapper { | ||
public: | ||
explicit BoolWrapper(bool value) : value_{value} {} | ||
bool get() const { return value_.load(std::memory_order_acquire); } | ||
void set(bool value) { value_.store(value, std::memory_order_release); } | ||
|
||
private: | ||
std::atomic_bool value_{false}; | ||
}; | ||
|
||
#if defined(PYBIND11_HAS_BARRIER) | ||
|
||
// Modifying the C/C++ members of a Python object from multiple threads requires a critical section | ||
// to ensure thread safety and data integrity. | ||
// These tests use a scoped critical section to ensure that the Python object is accessed in a | ||
// thread-safe manner. | ||
|
||
void test_scoped_critical_section(const py::handle &cls) { | ||
auto barrier = std::barrier(2); | ||
auto bool_wrapper = cls(false); | ||
bool output = false; | ||
|
||
{ | ||
// Release the GIL to allow run threads in parallel. | ||
py::gil_scoped_release gil_release{}; | ||
|
||
std::thread t1([&]() { | ||
// Use gil_scoped_acquire to ensure we have a valid Python thread state | ||
// before entering the critical section. Otherwise, the critical section | ||
// will cause a segmentation fault. | ||
py::gil_scoped_acquire ensure_tstate{}; | ||
// Enter the critical section with the same object as the second thread. | ||
py::scoped_critical_section lock{bool_wrapper}; | ||
// At this point, the object is locked by this thread via the scoped_critical_section. | ||
// This barrier will ensure that the second thread waits until this thread has released | ||
// the critical section before proceeding. | ||
barrier.arrive_and_wait(); | ||
// Sleep for a short time to simulate some work in the critical section. | ||
// This sleep is necessary to test the locking mechanism properly. | ||
std::this_thread::sleep_for(std::chrono::milliseconds(10)); | ||
auto *bw = bool_wrapper.cast<BoolWrapper *>(); | ||
bw->set(true); | ||
}); | ||
|
||
std::thread t2([&]() { | ||
// This thread will wait until the first thread has entered the critical section due to | ||
// the barrier. | ||
barrier.arrive_and_wait(); | ||
{ | ||
// Use gil_scoped_acquire to ensure we have a valid Python thread state | ||
// before entering the critical section. Otherwise, the critical section | ||
// will cause a segmentation fault. | ||
py::gil_scoped_acquire ensure_tstate{}; | ||
// Enter the critical section with the same object as the first thread. | ||
py::scoped_critical_section lock{bool_wrapper}; | ||
// At this point, the critical section is released by the first thread, the value | ||
// is set to true. | ||
auto *bw = bool_wrapper.cast<BoolWrapper *>(); | ||
output = bw->get(); | ||
} | ||
}); | ||
|
||
t1.join(); | ||
t2.join(); | ||
} | ||
|
||
if (!output) { | ||
throw std::runtime_error("Scoped critical section test failed: output is false"); | ||
} | ||
} | ||
|
||
void test_scoped_critical_section2(const py::handle &cls) { | ||
auto barrier = std::barrier(3); | ||
auto bool_wrapper1 = cls(false); | ||
auto bool_wrapper2 = cls(false); | ||
std::pair<bool, bool> output{false, false}; | ||
|
||
{ | ||
// Release the GIL to allow run threads in parallel. | ||
py::gil_scoped_release gil_release{}; | ||
|
||
std::thread t1([&]() { | ||
// Use gil_scoped_acquire to ensure we have a valid Python thread state | ||
// before entering the critical section. Otherwise, the critical section | ||
// will cause a segmentation fault. | ||
py::gil_scoped_acquire ensure_tstate{}; | ||
// Enter the critical section with two different objects. | ||
// This will ensure that the critical section is locked for both objects. | ||
py::scoped_critical_section lock{bool_wrapper1, bool_wrapper2}; | ||
// At this point, objects are locked by this thread via the scoped_critical_section. | ||
// This barrier will ensure that other threads wait until this thread has released | ||
// the critical section before proceeding. | ||
barrier.arrive_and_wait(); | ||
// Sleep for a short time to simulate some work in the critical section. | ||
// This sleep is necessary to test the locking mechanism properly. | ||
std::this_thread::sleep_for(std::chrono::milliseconds(10)); | ||
auto *bw1 = bool_wrapper1.cast<BoolWrapper *>(); | ||
auto *bw2 = bool_wrapper2.cast<BoolWrapper *>(); | ||
bw1->set(true); | ||
bw2->set(true); | ||
}); | ||
|
||
std::thread t2([&]() { | ||
// This thread will wait until the first thread has entered the critical section due to | ||
// the barrier. | ||
barrier.arrive_and_wait(); | ||
{ | ||
// Use gil_scoped_acquire to ensure we have a valid Python thread state | ||
// before entering the critical section. Otherwise, the critical section | ||
// will cause a segmentation fault. | ||
py::gil_scoped_acquire ensure_tstate{}; | ||
// Enter the critical section with the same object as the first thread. | ||
py::scoped_critical_section lock{bool_wrapper1}; | ||
// At this point, the critical section is released by the first thread, the value | ||
// is set to true. | ||
auto *bw1 = bool_wrapper1.cast<BoolWrapper *>(); | ||
output.first = bw1->get(); | ||
} | ||
}); | ||
|
||
std::thread t3([&]() { | ||
// This thread will wait until the first thread has entered the critical section due to | ||
// the barrier. | ||
barrier.arrive_and_wait(); | ||
{ | ||
// Use gil_scoped_acquire to ensure we have a valid Python thread state | ||
// before entering the critical section. Otherwise, the critical section | ||
// will cause a segmentation fault. | ||
py::gil_scoped_acquire ensure_tstate{}; | ||
// Enter the critical section with the same object as the first thread. | ||
py::scoped_critical_section lock{bool_wrapper2}; | ||
// At this point, the critical section is released by the first thread, the value | ||
// is set to true. | ||
auto *bw2 = bool_wrapper2.cast<BoolWrapper *>(); | ||
output.second = bw2->get(); | ||
} | ||
}); | ||
|
||
t1.join(); | ||
t2.join(); | ||
t3.join(); | ||
} | ||
|
||
if (!output.first || !output.second) { | ||
throw std::runtime_error( | ||
"Scoped critical section test with two objects failed: output is false"); | ||
} | ||
} | ||
|
||
void test_scoped_critical_section2_same_object_no_deadlock(const py::handle &cls) { | ||
auto barrier = std::barrier(2); | ||
auto bool_wrapper = cls(false); | ||
bool output = false; | ||
|
||
{ | ||
// Release the GIL to allow run threads in parallel. | ||
py::gil_scoped_release gil_release{}; | ||
|
||
std::thread t1([&]() { | ||
// Use gil_scoped_acquire to ensure we have a valid Python thread state | ||
// before entering the critical section. Otherwise, the critical section | ||
// will cause a segmentation fault. | ||
py::gil_scoped_acquire ensure_tstate{}; | ||
// Enter the critical section with the same object as the second thread. | ||
py::scoped_critical_section lock{bool_wrapper, bool_wrapper}; // same object used here | ||
// At this point, the object is locked by this thread via the scoped_critical_section. | ||
// This barrier will ensure that the second thread waits until this thread has released | ||
// the critical section before proceeding. | ||
barrier.arrive_and_wait(); | ||
// Sleep for a short time to simulate some work in the critical section. | ||
// This sleep is necessary to test the locking mechanism properly. | ||
std::this_thread::sleep_for(std::chrono::milliseconds(10)); | ||
auto *bw = bool_wrapper.cast<BoolWrapper *>(); | ||
bw->set(true); | ||
}); | ||
|
||
std::thread t2([&]() { | ||
// This thread will wait until the first thread has entered the critical section due to | ||
// the barrier. | ||
barrier.arrive_and_wait(); | ||
{ | ||
// Use gil_scoped_acquire to ensure we have a valid Python thread state | ||
// before entering the critical section. Otherwise, the critical section | ||
// will cause a segmentation fault. | ||
py::gil_scoped_acquire ensure_tstate{}; | ||
// Enter the critical section with the same object as the first thread. | ||
py::scoped_critical_section lock{bool_wrapper}; | ||
// At this point, the critical section is released by the first thread, the value | ||
// is set to true. | ||
auto *bw = bool_wrapper.cast<BoolWrapper *>(); | ||
output = bw->get(); | ||
} | ||
}); | ||
|
||
t1.join(); | ||
t2.join(); | ||
} | ||
|
||
if (!output) { | ||
throw std::runtime_error( | ||
"Scoped critical section test with same object failed: output is false"); | ||
} | ||
} | ||
|
||
#else | ||
|
||
void test_scoped_critical_section(const py::handle &) {} | ||
void test_scoped_critical_section2(const py::handle &) {} | ||
void test_scoped_critical_section2_same_object_no_deadlock(const py::handle &) {} | ||
|
||
#endif | ||
|
||
} // namespace test_scoped_critical_section_ns | ||
|
||
TEST_SUBMODULE(scoped_critical_section, m) { | ||
using namespace test_scoped_critical_section_ns; | ||
|
||
m.def("test_one_nullptr", test_one_nullptr); | ||
m.def("test_two_nullptrs", test_two_nullptrs); | ||
m.def("test_first_nullptr", test_first_nullptr); | ||
m.def("test_second_nullptr", test_second_nullptr); | ||
|
||
auto BoolWrapperClass = py::class_<BoolWrapper>(m, "BoolWrapper") | ||
.def(py::init<bool>()) | ||
.def("get", &BoolWrapper::get) | ||
.def("set", &BoolWrapper::set); | ||
auto BoolWrapperHandle = py::handle(BoolWrapperClass); | ||
(void) BoolWrapperHandle.ptr(); // suppress unused variable warning | ||
|
||
m.attr("has_barrier") = | ||
#ifdef PYBIND11_HAS_BARRIER | ||
true; | ||
#else | ||
false; | ||
#endif | ||
|
||
m.def("test_scoped_critical_section", | ||
[BoolWrapperHandle]() -> void { test_scoped_critical_section(BoolWrapperHandle); }); | ||
m.def("test_scoped_critical_section2", | ||
[BoolWrapperHandle]() -> void { test_scoped_critical_section2(BoolWrapperHandle); }); | ||
m.def("test_scoped_critical_section2_same_object_no_deadlock", [BoolWrapperHandle]() -> void { | ||
test_scoped_critical_section2_same_object_no_deadlock(BoolWrapperHandle); | ||
}); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from __future__ import annotations | ||
|
||
import pytest | ||
|
||
from pybind11_tests import scoped_critical_section as m | ||
|
||
|
||
def test_nullptr_combinations(): | ||
m.test_one_nullptr() | ||
m.test_two_nullptrs() | ||
m.test_first_nullptr() | ||
m.test_second_nullptr() | ||
|
||
|
||
@pytest.mark.skipif(not m.has_barrier, reason="no <barrier>") | ||
def test_scoped_critical_section() -> None: | ||
for _ in range(64): | ||
m.test_scoped_critical_section() | ||
|
||
|
||
@pytest.mark.skipif(not m.has_barrier, reason="no <barrier>") | ||
def test_scoped_critical_section2() -> None: | ||
for _ in range(64): | ||
m.test_scoped_critical_section2() | ||
|
||
|
||
@pytest.mark.skipif(not m.has_barrier, reason="no <barrier>") | ||
def test_scoped_critical_section2_same_object_no_deadlock() -> None: | ||
for _ in range(64): | ||
m.test_scoped_critical_section2_same_object_no_deadlock() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.