Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: add pybind11/gil_safe_call_once.h (to fix deadlocks in pybind11/numpy.h)#4877
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
38317c3e7b8c4f74ac0d9109a16588cec111ce2715e7be9c2f07b28b36be6457bc16a678f4e936d9441da864f216689b06d965f29398a42cab2cf8e6d5bdd84c5dd1b8633c5b82f3efc3ebd139c33712ddcf2b924557dce704fe13b2f87a8fad1017845330266bbc677cd9390File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // Copyright (c) 2023 The pybind Community. | ||
| #pragma once | ||
| #include "detail/common.h" | ||
| #include "gil.h" | ||
| #include <cassert> | ||
| #include <mutex> | ||
| PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) | ||
| // Use the `gil_safe_call_once_and_store` class below instead of the naive | ||
| // | ||
| // static auto imported_obj = py::module_::import("module_name"); // BAD, DO NOT USE! | ||
| // | ||
| // which has two serious issues: | ||
| // | ||
| // 1. Py_DECREF() calls potentially after the Python interpreter was finalized already, and | ||
| // 2. deadlocks in multi-threaded processes (because of missing lock ordering). | ||
| // | ||
| // The following alternative avoids both problems: | ||
| // | ||
| // PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> storage; | ||
| // auto &imported_obj = storage // Do NOT make this `static`! | ||
| // .call_once_and_store_result([]() { | ||
| // return py::module_::import("module_name"); | ||
| // }) | ||
| // .get_stored(); | ||
| // | ||
| // The parameter of `call_once_and_store_result()` must be callable. It can make | ||
| // CPython API calls, and in particular, it can temporarily release the GIL. | ||
| // | ||
| // `T` can be any C++ type, it does not have to involve CPython API types. | ||
| // | ||
| // The behavior with regard to signals, e.g. `SIGINT` (`KeyboardInterrupt`), | ||
| // is not ideal. If the main thread is the one to actually run the `Callable`, | ||
| // then a `KeyboardInterrupt` will interrupt it if it is running normal Python | ||
| // code. The situation is different if a non-main thread runs the | ||
| // `Callable`, and then the main thread starts waiting for it to complete: | ||
| // a `KeyboardInterrupt` will not interrupt the non-main thread, but it will | ||
| // get processed only when it is the main thread's turn again and it is running | ||
| // normal Python code. However, this will be unnoticeable for quick call-once | ||
| // functions, which is usually the case. | ||
| template <typename T> | ||
| class gil_safe_call_once_and_store { | ||
| public: | ||
| // PRECONDITION: The GIL must be held when `call_once_and_store_result()` is called. | ||
| template <typename Callable> | ||
| gil_safe_call_once_and_store &call_once_and_store_result(Callable &&fn) { | ||
| if (!is_initialized_) { // This read is guarded by the GIL. | ||
| // Multiple threads may enter here, because the GIL is released in the next line and | ||
| // CPython API calls in the `fn()` call below may release and reacquire the GIL. | ||
| gil_scoped_release gil_rel; // Needed to establish lock ordering. | ||
| std::call_once(once_flag_, [&] { | ||
| // Only one thread will ever enter here. | ||
| gil_scoped_acquire gil_acq; | ||
| ::new (storage_) T(fn()); // fn may release, but will reacquire, the GIL. | ||
| is_initialized_ = true; // This write is guarded by the GIL. | ||
| }); | ||
| // All threads will observe `is_initialized_` as true here. | ||
| } | ||
| // Intentionally not returning `T &` to ensure the calling code is self-documenting. | ||
| return *this; | ||
| } | ||
| // This must only be called after `call_once_and_store_result()` was called. | ||
| T &get_stored() { | ||
| assert(is_initialized_); | ||
| PYBIND11_WARNING_PUSH | ||
| #if !defined(__clang__) && defined(__GNUC__) && __GNUC__ < 5 | ||
| // Needed for gcc 4.8.5 | ||
| PYBIND11_WARNING_DISABLE_GCC("-Wstrict-aliasing") | ||
| #endif | ||
| return *reinterpret_cast<T *>(storage_); | ||
| PYBIND11_WARNING_POP | ||
| } | ||
| constexpr gil_safe_call_once_and_store() = default; | ||
| PYBIND11_DTOR_CONSTEXPR ~gil_safe_call_once_and_store() = default; | ||
| private: | ||
| alignas(T) char storage_[sizeof(T)] = {}; | ||
Collaborator 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. I would make this a std::optional and avoid both the atomic flag and the storage here. You don't need the atomic since call_once is an inherent mutex that asserts ordering.
| ||
| std::once_flag once_flag_ = {}; | ||
| bool is_initialized_ = false; | ||
| // The `is_initialized_`-`storage_` pair is very similar to `std::optional`, | ||
| // but the latter does not have the triviality properties of former, | ||
| // therefore `std::optional` is not a viable alternative here. | ||
| }; | ||
| PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be a good idea to add a debug assertion that the GIL is actually held here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks! I actually globally tested that change in isolation about a week ago already and it passed. Which makes me think: if the precondition is not met the failures are critical, therefore such code never actually makes it into the code base. However, the extra
assert()is a great and easy way to guard against accidents when developing new code.