Skip to main content

pyo3/sync/
nonpoison.rs

1//! Contains a non-poisoning variant of [`PyMutex`].
2
3use core::cell::UnsafeCell;
4use core::marker::PhantomData;
5use core::ops::{Deref, DerefMut};
6
7/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex), exposing an RAII guard interface.
8///
9/// Compared with `std::sync::Mutex` or `parking_lot::Mutex`, this is a very
10/// stripped-down locking primitive that only supports blocking lock and unlock
11/// operations and does not support `try_lock` or APIs that depend on
12/// `try_lock`.  For this reason, it is not possible to avoid the possibility of
13/// possibly blocking when calling `lock` and extreme care must be taken to avoid
14/// introducing a deadlock.
15///
16/// This type is most useful when arbitrary Python code might execute while the
17/// lock is held. On the GIL-enabled build, PyMutex will release the GIL if the
18/// thread is blocked on acquiring the lock. On the free-threaded build, threads
19/// blocked on acquiring a PyMutex will not prevent the garbage collector from
20/// running.
21pub struct PyMutex<T: ?Sized> {
22    pub(crate) mutex: UnsafeCell<crate::ffi::PyMutex>,
23    pub(crate) data: UnsafeCell<T>,
24}
25
26/// RAII guard to handle releasing a PyMutex lock.
27///
28/// The lock is released when `PyMutexGuard` is dropped.
29pub struct PyMutexGuard<'a, T: ?Sized> {
30    inner: &'a PyMutex<T>,
31    // this is equivalent to impl !Send, which we can't do
32    // because negative trait bounds aren't supported yet
33    _phantom: PhantomData<*const ()>,
34}
35
36/// SAFETY: `T` must be `Sync` for a [`PyMutexGuard<T>`] to be `Sync`
37/// because it is possible to get a `&T` from `&MutexGuard` (via `Deref`).
38unsafe impl<T: ?Sized + Sync> Sync for PyMutexGuard<'_, T> {}
39
40/// SAFETY: `T` must be `Send` for a [`PyMutex`] to be `Send` because it is possible to acquire
41/// the owned `T` from the `PyMutex` via [`into_inner`].
42///
43/// [`into_inner`]: PyMutex::into_inner
44unsafe impl<T: ?Sized + Send> Send for PyMutex<T> {}
45
46/// SAFETY: `T` must be `Send` for [`PyMutex`] to be `Sync`.
47/// This ensures that the protected data can be accessed safely from multiple threads
48/// without causing data races or other unsafe behavior.
49///
50/// [`PyMutex<T>`] provides mutable access to `T` to one thread at a time. However, it's essential
51/// for `T` to be `Send` because it's not safe for non-`Send` structures to be accessed in
52/// this manner. For instance, consider [`Rc`], a non-atomic reference counted smart pointer,
53/// which is not `Send`. With `Rc`, we can have multiple copies pointing to the same heap
54/// allocation with a non-atomic reference count. If we were to use `Mutex<Rc<_>>`, it would
55/// only protect one instance of `Rc` from shared access, leaving other copies vulnerable
56/// to potential data races.
57///
58/// Also note that it is not necessary for `T` to be `Sync` as `&T` is only made available
59/// to one thread at a time if `T` is not `Sync`.
60///
61/// [`Rc`]: alloc::rc::Rc
62unsafe impl<T: ?Sized + Send> Sync for PyMutex<T> {}
63
64impl<T> PyMutex<T> {
65    /// Acquire the mutex, blocking the current thread until it is able to do so.
66    pub fn lock(&self) -> PyMutexGuard<'_, T> {
67        // SAFETY: valid pointer to mutex passed to `PyMutex_Lock`
68        // and the mutex is not moved while locked
69        unsafe { crate::ffi::PyMutex_Lock(self.mutex.get()) };
70        PyMutexGuard::new(self)
71    }
72
73    /// Create a new mutex in an unlocked state ready for use.
74    pub const fn new(value: T) -> Self {
75        Self {
76            mutex: UnsafeCell::new(crate::ffi::PyMutex::new()),
77            data: UnsafeCell::new(value),
78        }
79    }
80
81    /// Check if the mutex is locked.
82    ///
83    /// Note that this is only useful for debugging or test purposes and should
84    /// not be used to make concurrency control decisions, as the lock state may
85    /// change immediately after the check.
86    #[cfg(Py_3_14)]
87    pub fn is_locked(&self) -> bool {
88        // SAFETY: valid pointer to mutex passed to `PyMutex_IsLocked`
89        let ret = unsafe { crate::ffi::PyMutex_IsLocked(self.mutex.get()) };
90        ret != 0
91    }
92
93    /// Consumes this mutex, returning the underlying data.
94    pub fn into_inner(self) -> T
95    where
96        T: Sized,
97    {
98        self.data.into_inner()
99    }
100}
101
102impl<'mutex, T: ?Sized> PyMutexGuard<'mutex, T> {
103    fn new(lock: &'mutex PyMutex<T>) -> PyMutexGuard<'mutex, T> {
104        PyMutexGuard {
105            inner: lock,
106            _phantom: PhantomData,
107        }
108    }
109}
110
111impl<'a, T: ?Sized> Drop for PyMutexGuard<'a, T> {
112    fn drop(&mut self) {
113        // SAFETY: valid pointer to mutex passed to `PyMutex_Unlock`
114        unsafe { crate::ffi::PyMutex_Unlock(self.inner.mutex.get()) };
115    }
116}
117
118impl<'a, T> Deref for PyMutexGuard<'a, T> {
119    type Target = T;
120
121    fn deref(&self) -> &T {
122        // safety: cannot be null pointer because PyMutex::new always
123        // creates a valid PyMutex pointer
124        unsafe { &*self.inner.data.get() }
125    }
126}
127
128impl<'a, T> DerefMut for PyMutexGuard<'a, T> {
129    fn deref_mut(&mut self) -> &mut T {
130        // safety: cannot be null pointer because PyMutex::new always
131        // creates a valid PyMutex pointer
132        unsafe { &mut *self.inner.data.get() }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    extern crate std;
139
140    #[cfg(not(target_arch = "wasm32"))]
141    use core::sync::atomic::{AtomicBool, Ordering};
142    #[cfg(not(target_arch = "wasm32"))]
143    use std::sync::Barrier;
144
145    use super::*;
146    #[cfg(not(target_arch = "wasm32"))]
147    use crate::Py;
148    #[cfg(not(target_arch = "wasm32"))]
149    use crate::Python;
150    #[cfg(not(target_arch = "wasm32"))]
151    use crate::types::{PyAnyMethods, PyDict, PyDictMethods, PyNone};
152
153    #[cfg(not(target_arch = "wasm32"))]
154    #[test]
155    fn test_pymutex() {
156        let mutex = Python::attach(|py| -> PyMutex<Py<PyDict>> {
157            let d = PyDict::new(py);
158            PyMutex::new(d.unbind())
159        });
160        #[cfg_attr(not(Py_3_14), allow(unused_variables))]
161        let mutex = Python::attach(|py| {
162            let mutex = py.detach(|| -> PyMutex<Py<PyDict>> {
163                std::thread::spawn(|| {
164                    let dict_guard = mutex.lock();
165                    Python::attach(|py| {
166                        let dict = dict_guard.bind(py);
167                        dict.set_item(PyNone::get(py), PyNone::get(py)).unwrap();
168                    });
169                    #[cfg(Py_3_14)]
170                    assert!(mutex.is_locked());
171                    drop(dict_guard);
172                    #[cfg(Py_3_14)]
173                    assert!(!mutex.is_locked());
174                    mutex
175                })
176                .join()
177                .unwrap()
178            });
179
180            let dict_guard = mutex.lock();
181            #[cfg(Py_3_14)]
182            assert!(mutex.is_locked());
183            let d = dict_guard.bind(py);
184
185            assert!(
186                d.get_item(PyNone::get(py))
187                    .unwrap()
188                    .unwrap()
189                    .eq(PyNone::get(py))
190                    .unwrap()
191            );
192            #[cfg(Py_3_14)]
193            assert!(mutex.is_locked());
194            drop(dict_guard);
195            #[cfg(Py_3_14)]
196            assert!(!mutex.is_locked());
197            mutex
198        });
199        #[cfg(Py_3_14)]
200        assert!(!mutex.is_locked());
201    }
202
203    #[cfg(not(target_arch = "wasm32"))]
204    #[test]
205    fn test_pymutex_blocks() {
206        let mutex = PyMutex::new(());
207        let first_thread_locked_once = AtomicBool::new(false);
208        let second_thread_locked_once = AtomicBool::new(false);
209        let finished = AtomicBool::new(false);
210        let barrier = Barrier::new(2);
211
212        std::thread::scope(|s| {
213            s.spawn(|| {
214                let guard = mutex.lock();
215                first_thread_locked_once.store(true, Ordering::SeqCst);
216                while !finished.load(Ordering::SeqCst) {
217                    if second_thread_locked_once.load(Ordering::SeqCst) {
218                        // Wait a little to guard against the unlikely event that
219                        // the other thread isn't blocked on acquiring the mutex yet.
220                        // If PyMutex had a try_lock implementation this would be
221                        // unnecessary
222                        std::thread::sleep(core::time::Duration::from_millis(10));
223                        // block (and hold the mutex) until the receiver actually receives something
224                        barrier.wait();
225                        finished.store(true, Ordering::SeqCst);
226                    }
227                }
228                drop(guard);
229            });
230
231            s.spawn(|| {
232                while !first_thread_locked_once.load(Ordering::SeqCst) {
233                    core::hint::spin_loop();
234                }
235                second_thread_locked_once.store(true, Ordering::SeqCst);
236                let guard = mutex.lock();
237                assert!(finished.load(Ordering::SeqCst));
238                drop(guard);
239            });
240
241            barrier.wait();
242        });
243    }
244
245    #[test]
246    fn test_send_not_send() {
247        use crate::impl_::pyclass::{IsSend, IsSync, value_of};
248
249        assert!(!value_of!(IsSend, PyMutexGuard<'_, i32>));
250        assert!(value_of!(IsSync, PyMutexGuard<'_, i32>));
251
252        assert!(value_of!(IsSend, PyMutex<i32>));
253        assert!(value_of!(IsSync, PyMutex<i32>));
254    }
255}