diff --git a/guide/src/faq.md b/guide/src/faq.md index b2f89051ec1..cdbaa1ecf1d 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -13,7 +13,7 @@ PyO3 provides a struct [`GILOnceCell`] which works equivalently to `OnceCell` but relies solely on the Python GIL for thread safety. This means it can be used in place of `lazy_static` or `once_cell` where you are experiencing the deadlock described above. See the documentation for [`GILOnceCell`] for an example how to use it. -[`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/once_cell/struct.GILOnceCell.html +[`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html ## I can't run `cargo test`; or I can't build in a Cargo workspace: I'm having linker issues like "Symbol not found" or "Undefined reference to _PyExc_SystemError"! diff --git a/newsfragments/2975.added.md b/newsfragments/2975.added.md new file mode 100644 index 00000000000..7a3da624f2a --- /dev/null +++ b/newsfragments/2975.added.md @@ -0,0 +1 @@ +Add `GILProtected` to mediate concurrent access to a value using Python's global interpreter lock (GIL). diff --git a/src/exceptions.rs b/src/exceptions.rs index 1e2572d1d38..2faf7ae6df7 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -102,7 +102,7 @@ macro_rules! import_exception { impl $name { fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { - use $crate::once_cell::GILOnceCell; + use $crate::sync::GILOnceCell; use $crate::AsPyPointer; static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = GILOnceCell::new(); @@ -241,7 +241,7 @@ macro_rules! create_exception_type_object { impl $name { fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { - use $crate::once_cell::GILOnceCell; + use $crate::sync::GILOnceCell; use $crate::AsPyPointer; static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = GILOnceCell::new(); diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index a7f54cf22e0..5430bae2c25 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -802,7 +802,7 @@ unsafe fn bpo_35810_workaround(_py: Python<'_>, ty: *mut ffi::PyTypeObject) { { // Must check version at runtime for abi3 wheels - they could run against a higher version // than the build config suggests. - use crate::once_cell::GILOnceCell; + use crate::sync::GILOnceCell; static IS_PYTHON_3_8: GILOnceCell = GILOnceCell::new(); if *IS_PYTHON_3_8.get_or_init(_py, || _py.version_info() >= (3, 8)) { diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index 8990bba3717..d9277a7681d 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -1,16 +1,18 @@ use std::{ borrow::Cow, + cell::RefCell, ffi::CStr, marker::PhantomData, thread::{self, ThreadId}, }; -use parking_lot::{const_mutex, Mutex}; - use crate::{ - exceptions::PyRuntimeError, ffi, once_cell::GILOnceCell, pyclass::create_type_object, - types::PyType, AsPyPointer, IntoPyPointer, Py, PyClass, PyErr, PyMethodDefType, PyObject, - PyResult, Python, + exceptions::PyRuntimeError, + ffi, + pyclass::create_type_object, + sync::{GILOnceCell, GILProtected}, + types::PyType, + AsPyPointer, IntoPyPointer, Py, PyClass, PyErr, PyMethodDefType, PyObject, PyResult, Python, }; use super::PyClassItemsIter; @@ -24,7 +26,7 @@ struct LazyTypeObjectInner { value: GILOnceCell>, // Threads which have begun initialization of the `tp_dict`. Used for // reentrant initialization detection. - initializing_threads: Mutex>, + initializing_threads: GILProtected>>, tp_dict_filled: GILOnceCell<()>, } @@ -34,7 +36,7 @@ impl LazyTypeObject { LazyTypeObject( LazyTypeObjectInner { value: GILOnceCell::new(), - initializing_threads: const_mutex(Vec::new()), + initializing_threads: GILProtected::new(RefCell::new(Vec::new())), tp_dict_filled: GILOnceCell::new(), }, PhantomData, @@ -109,7 +111,7 @@ impl LazyTypeObjectInner { let thread_id = thread::current().id(); { - let mut threads = self.initializing_threads.lock(); + let mut threads = self.initializing_threads.with_gil(py).borrow_mut(); if threads.contains(&thread_id) { // Reentrant call: just return the type object, even if the // `tp_dict` is not filled yet. @@ -119,18 +121,20 @@ impl LazyTypeObjectInner { } struct InitializationGuard<'a> { - initializing_threads: &'a Mutex>, + initializing_threads: &'a GILProtected>>, + py: Python<'a>, thread_id: ThreadId, } impl Drop for InitializationGuard<'_> { fn drop(&mut self) { - let mut threads = self.initializing_threads.lock(); + let mut threads = self.initializing_threads.with_gil(self.py).borrow_mut(); threads.retain(|id| *id != self.thread_id); } } let guard = InitializationGuard { initializing_threads: &self.initializing_threads, + py, thread_id, }; @@ -170,7 +174,7 @@ impl LazyTypeObjectInner { // Initialization successfully complete, can clear the thread list. // (No further calls to get_or_init() will try to init, on any thread.) std::mem::forget(guard); - *self.initializing_threads.lock() = Vec::new(); + self.initializing_threads.with_gil(py).replace(Vec::new()); result }); diff --git a/src/lib.rs b/src/lib.rs index 940f061e6b3..523acfca668 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -409,7 +409,7 @@ mod instance; pub mod marker; pub mod marshal; #[macro_use] -pub mod once_cell; +pub mod sync; pub mod panic; pub mod prelude; pub mod pycell; diff --git a/src/once_cell.rs b/src/sync.rs similarity index 89% rename from src/once_cell.rs rename to src/sync.rs index 3cd86f2837b..c83c0412bef 100644 --- a/src/once_cell.rs +++ b/src/sync.rs @@ -1,7 +1,30 @@ -//! A write-once cell mediated by the Python GIL. +//! Synchronization mechanisms based on the Python GIL. use crate::{types::PyString, Py, Python}; use std::cell::UnsafeCell; +/// Value with concurrent access protected by the GIL. +/// +/// This is a synchronization primitive based on Python's global interpreter lock (GIL). +/// It ensures that only one thread at a time can access the inner value via shared references. +/// It can be combined with interior mutability to obtain mutable references. +pub struct GILProtected { + value: T, +} + +impl GILProtected { + /// Place the given value under the protection of the GIL. + pub const fn new(value: T) -> Self { + Self { value } + } + + /// Gain access to the inner value by giving proof of having acquired the GIL. + pub fn with_gil<'py>(&'py self, _py: Python<'py>) -> &'py T { + &self.value + } +} + +unsafe impl Sync for GILProtected where T: Send {} + /// A write-once cell similar to [`once_cell::OnceCell`](https://docs.rs/once_cell/latest/once_cell/). /// /// Unlike `once_cell::sync` which blocks threads to achieve thread safety, this implementation @@ -25,7 +48,7 @@ use std::cell::UnsafeCell; /// between threads: /// /// ``` -/// use pyo3::once_cell::GILOnceCell; +/// use pyo3::sync::GILOnceCell; /// use pyo3::prelude::*; /// use pyo3::types::PyList; /// @@ -170,7 +193,7 @@ impl GILOnceCell { #[macro_export] macro_rules! intern { ($py: expr, $text: expr) => {{ - static INTERNED: $crate::once_cell::Interned = $crate::once_cell::Interned::new($text); + static INTERNED: $crate::sync::Interned = $crate::sync::Interned::new($text); INTERNED.get($py) }}; } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 59300ac702b..602302a953b 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -1,7 +1,7 @@ // Copyright (c) 2017-present PyO3 Project and Contributors use crate::err::{PyDowncastError, PyErr, PyResult}; -use crate::once_cell::GILOnceCell; +use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::{PyAny, PyDict, PySequence, PyType}; use crate::{ffi, AsPyPointer, IntoPyPointer, Py, PyNativeType, PyTryFrom, Python, ToPyObject}; diff --git a/src/types/sequence.rs b/src/types/sequence.rs index b604bf1c763..ce673a47443 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -4,7 +4,7 @@ use crate::exceptions::PyTypeError; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::internal_tricks::get_ssize_index; -use crate::once_cell::GILOnceCell; +use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::{PyAny, PyList, PyString, PyTuple, PyType}; use crate::{ffi, PyNativeType, ToPyObject};