| #include <pybind11/critical_section.h> | |
| #include "pybind11_tests.h" | |
| #include <atomic> | |
| #include <chrono> | |
| #include <thread> | |
| #include <utility> | |
| #if defined(PYBIND11_HAS_STD_BARRIER) | |
| # 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{}}; | |
| } | |
| // 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_STD_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_STD_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); | |
| }); | |
| } |