pyo3/types/
mutex.rs

1use std::cell::UnsafeCell;
2use std::marker::PhantomData;
3use std::ops::{Deref, DerefMut};
4#[cfg(panic = "unwind")]
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{LockResult, PoisonError};
7#[cfg(panic = "unwind")]
8use std::thread;
9
10// See std::sync::poison in the rust standard library.
11// This is more-or-less copied from there since it is not public.
12// this type detects a panic and poisons the wrapping mutex
13struct Flag {
14    #[cfg(panic = "unwind")]
15    failed: AtomicBool,
16}
17
18impl Flag {
19    #[inline]
20    const fn new() -> Flag {
21        Flag {
22            #[cfg(panic = "unwind")]
23            failed: AtomicBool::new(false),
24        }
25    }
26
27    /// Checks the flag for an unguarded borrow, where we only care about existing poison.
28    #[inline]
29    fn borrow(&self) -> LockResult<()> {
30        if self.get() {
31            Err(PoisonError::new(()))
32        } else {
33            Ok(())
34        }
35    }
36
37    /// Checks the flag for a guarded borrow, where we may also set poison when `done`.
38    #[inline]
39    fn guard(&self) -> LockResult<Guard> {
40        let ret = Guard {
41            #[cfg(panic = "unwind")]
42            panicking: thread::panicking(),
43        };
44        if self.get() {
45            Err(PoisonError::new(ret))
46        } else {
47            Ok(ret)
48        }
49    }
50
51    #[inline]
52    #[cfg(panic = "unwind")]
53    fn done(&self, guard: &Guard) {
54        if !guard.panicking && thread::panicking() {
55            self.failed.store(true, Ordering::Relaxed);
56        }
57    }
58
59    #[inline]
60    #[cfg(not(panic = "unwind"))]
61    fn done(&self, _guard: &Guard) {}
62
63    #[inline]
64    #[cfg(panic = "unwind")]
65    fn get(&self) -> bool {
66        self.failed.load(Ordering::Relaxed)
67    }
68
69    #[inline(always)]
70    #[cfg(not(panic = "unwind"))]
71    fn get(&self) -> bool {
72        false
73    }
74
75    #[inline]
76    fn clear(&self) {
77        #[cfg(panic = "unwind")]
78        self.failed.store(false, Ordering::Relaxed)
79    }
80}
81
82#[derive(Clone)]
83pub(crate) struct Guard {
84    #[cfg(panic = "unwind")]
85    panicking: bool,
86}
87
88/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex), exposing an RAII guard interface.
89///
90/// Compared with `std::sync::Mutex` or `parking_lot::Mutex`, this is a very
91/// stripped-down locking primitive that only supports blocking lock and unlock
92/// operations and does not support `try_lock` or APIs that depend on
93/// `try_lock`.  For this reason, it is not possible to avoid the possibility of
94/// possibly blocking when calling `lock` and extreme care must be taken to avoid
95/// introducing a deadlock.
96///
97/// This type is most useful when arbitrary Python code might execute while the
98/// lock is held. On the GIL-enabled build, PyMutex will release the GIL if the
99/// thread is blocked on acquiring the lock. On the free-threaded build, threads
100/// blocked on acquiring a PyMutex will not prevent the garbage collector from
101/// running.
102///
103/// ## Poisoning
104///
105/// Like `std::sync::Mutex`, `PyMutex` implements poisoning. A mutex
106/// is considered poisoned whenever a thread panics while holding the mutex. Once
107/// a mutex is poisoned, all other threads are unable to access the data by
108/// default as it is likely to be tainted (some invariant is not being held).
109///
110/// This means that the `lock` method returns a `Result` which indicated whether
111/// the mutex has been poisoned or not. Must usage will simple `unwrap()` these
112/// results, propagating panics among threads to ensure a possible invalid
113/// invariant is not being observed.
114///
115/// A poisoned mutex, however, does not prevent all access to the underlying
116/// data. The `PoisonError` type has an `into_inner` method which will return
117/// the guard that would have otherwise been returned on a successful lock. This
118/// allows access to the data, despite the lock being poisoned.
119pub struct PyMutex<T: ?Sized> {
120    mutex: UnsafeCell<crate::ffi::PyMutex>,
121    poison: Flag,
122    data: UnsafeCell<T>,
123}
124
125/// RAII guard to handle releasing a PyMutex lock.
126///
127/// The lock is released when `PyMutexGuard` is dropped.
128pub struct PyMutexGuard<'a, T: ?Sized> {
129    inner: &'a PyMutex<T>,
130    poison: Guard,
131    // this is equivalent to impl !Send, which we can't do
132    // because negative trait bounds aren't supported yet
133    _phantom: PhantomData<*const ()>,
134}
135
136/// `T` must be `Sync` for a [`PyMutexGuard<T>`] to be `Sync`
137/// because it is possible to get a `&T` from `&MutexGuard` (via `Deref`).
138unsafe impl<T: ?Sized + Sync> Sync for PyMutexGuard<'_, T> {}
139
140/// `T` must be `Send` for a [`PyMutex`] to be `Send` because it is possible to acquire
141/// the owned `T` from the `PyMutex` via [`into_inner`].
142///
143/// [`into_inner`]: PyMutex::into_inner
144unsafe impl<T: ?Sized + Send> Send for PyMutex<T> {}
145
146/// `T` must be `Send` for [`PyMutex`] to be `Sync`.
147/// This ensures that the protected data can be accessed safely from multiple threads
148/// without causing data races or other unsafe behavior.
149///
150/// [`PyMutex<T>`] provides mutable access to `T` to one thread at a time. However, it's essential
151/// for `T` to be `Send` because it's not safe for non-`Send` structures to be accessed in
152/// this manner. For instance, consider [`Rc`], a non-atomic reference counted smart pointer,
153/// which is not `Send`. With `Rc`, we can have multiple copies pointing to the same heap
154/// allocation with a non-atomic reference count. If we were to use `Mutex<Rc<_>>`, it would
155/// only protect one instance of `Rc` from shared access, leaving other copies vulnerable
156/// to potential data races.
157///
158/// Also note that it is not necessary for `T` to be `Sync` as `&T` is only made available
159/// to one thread at a time if `T` is not `Sync`.
160///
161/// [`Rc`]: std::rc::Rc
162unsafe impl<T: ?Sized + Send> Sync for PyMutex<T> {}
163
164impl<T> PyMutex<T> {
165    /// Acquire the mutex, blocking the current thread until it is able to do so.
166    pub fn lock(&self) -> LockResult<PyMutexGuard<'_, T>> {
167        unsafe { crate::ffi::PyMutex_Lock(UnsafeCell::raw_get(&self.mutex)) };
168        PyMutexGuard::new(self)
169    }
170
171    /// Create a new mutex in an unlocked state ready for use.
172    pub const fn new(value: T) -> Self {
173        Self {
174            mutex: UnsafeCell::new(crate::ffi::PyMutex::new()),
175            data: UnsafeCell::new(value),
176            poison: Flag::new(),
177        }
178    }
179
180    /// Check if the mutex is locked.
181    ///
182    /// Note that this is only useful for debugging or test purposes and should
183    /// not be used to make concurrency control decisions, as the lock state may
184    /// change immediately after the check.
185    #[cfg(Py_3_14)]
186    pub fn is_locked(&self) -> bool {
187        let ret = unsafe { crate::ffi::PyMutex_IsLocked(UnsafeCell::raw_get(&self.mutex)) };
188        ret != 0
189    }
190
191    /// Consumes this mutex, returning the underlying data.
192    ///
193    /// # Errors
194    ///
195    /// If another user of this mutex panicked while holding the mutex, then
196    /// this call will return an error containing the underlying data
197    /// instead.
198    pub fn into_inner(self) -> LockResult<T>
199    where
200        T: Sized,
201    {
202        let data = self.data.into_inner();
203        map_result(self.poison.borrow(), |()| data)
204    }
205
206    /// Clear the poisoned state from a mutex.
207    ///
208    /// If the mutex is poisoned, it will remain poisoned until this function is called. This
209    /// allows recovering from a poisoned state and marking that it has recovered. For example, if
210    /// the value is overwritten by a known-good value, then the mutex can be marked as
211    /// un-poisoned. Or possibly, the value could be inspected to determine if it is in a
212    /// consistent state, and if so the poison is removed.
213    pub fn clear_poison(&self) {
214        self.poison.clear();
215    }
216}
217
218#[cfg_attr(not(panic = "unwind"), allow(clippy::unnecessary_wraps))]
219fn map_result<T, U, F>(result: LockResult<T>, f: F) -> LockResult<U>
220where
221    F: FnOnce(T) -> U,
222{
223    match result {
224        Ok(t) => Ok(f(t)),
225        #[cfg(panic = "unwind")]
226        Err(e) => Err(PoisonError::new(f(e.into_inner()))),
227        #[cfg(not(panic = "unwind"))]
228        Err(_) => {
229            unreachable!();
230        }
231    }
232}
233
234impl<'mutex, T: ?Sized> PyMutexGuard<'mutex, T> {
235    fn new(lock: &'mutex PyMutex<T>) -> LockResult<PyMutexGuard<'mutex, T>> {
236        map_result(lock.poison.guard(), |guard| PyMutexGuard {
237            inner: lock,
238            poison: guard,
239            _phantom: PhantomData,
240        })
241    }
242}
243
244impl<'a, T: ?Sized> Drop for PyMutexGuard<'a, T> {
245    fn drop(&mut self) {
246        unsafe {
247            self.inner.poison.done(&self.poison);
248            crate::ffi::PyMutex_Unlock(UnsafeCell::raw_get(&self.inner.mutex))
249        };
250    }
251}
252
253impl<'a, T> Deref for PyMutexGuard<'a, T> {
254    type Target = T;
255
256    fn deref(&self) -> &T {
257        // safety: cannot be null pointer because PyMutex::new always
258        // creates a valid PyMutex pointer
259        unsafe { &*self.inner.data.get() }
260    }
261}
262
263impl<'a, T> DerefMut for PyMutexGuard<'a, T> {
264    fn deref_mut(&mut self) -> &mut T {
265        // safety: cannot be null pointer because PyMutex::new always
266        // creates a valid PyMutex pointer
267        unsafe { &mut *self.inner.data.get() }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    #[cfg(not(target_arch = "wasm32"))]
274    use std::sync::{
275        atomic::{AtomicBool, Ordering},
276        Arc, Barrier,
277    };
278
279    use super::*;
280    #[cfg(not(target_arch = "wasm32"))]
281    use crate::types::{PyAnyMethods, PyDict, PyDictMethods, PyNone};
282    #[cfg(not(target_arch = "wasm32"))]
283    use crate::Py;
284    #[cfg(not(target_arch = "wasm32"))]
285    use crate::Python;
286
287    #[cfg(not(target_arch = "wasm32"))]
288    #[test]
289    fn test_pymutex() {
290        let mutex = Python::attach(|py| -> PyMutex<Py<PyDict>> {
291            let d = PyDict::new(py);
292            PyMutex::new(d.unbind())
293        });
294        #[cfg_attr(not(Py_3_14), allow(unused_variables))]
295        let mutex = Python::attach(|py| {
296            let mutex = py.detach(|| -> PyMutex<Py<PyDict>> {
297                std::thread::spawn(|| {
298                    let dict_guard = mutex.lock().unwrap();
299                    Python::attach(|py| {
300                        let dict = dict_guard.bind(py);
301                        dict.set_item(PyNone::get(py), PyNone::get(py)).unwrap();
302                    });
303                    #[cfg(Py_3_14)]
304                    assert!(mutex.is_locked());
305                    drop(dict_guard);
306                    #[cfg(Py_3_14)]
307                    assert!(!mutex.is_locked());
308                    mutex
309                })
310                .join()
311                .unwrap()
312            });
313
314            let dict_guard = mutex.lock().unwrap();
315            #[cfg(Py_3_14)]
316            assert!(mutex.is_locked());
317            let d = dict_guard.bind(py);
318
319            assert!(d
320                .get_item(PyNone::get(py))
321                .unwrap()
322                .unwrap()
323                .eq(PyNone::get(py))
324                .unwrap());
325            #[cfg(Py_3_14)]
326            assert!(mutex.is_locked());
327            drop(dict_guard);
328            #[cfg(Py_3_14)]
329            assert!(!mutex.is_locked());
330            mutex
331        });
332        #[cfg(Py_3_14)]
333        assert!(!mutex.is_locked());
334    }
335
336    #[cfg(not(target_arch = "wasm32"))]
337    #[test]
338    fn test_pymutex_blocks() {
339        let mutex = PyMutex::new(());
340        let first_thread_locked_once = AtomicBool::new(false);
341        let second_thread_locked_once = AtomicBool::new(false);
342        let finished = AtomicBool::new(false);
343        let barrier = Barrier::new(2);
344
345        std::thread::scope(|s| {
346            s.spawn(|| {
347                let guard = mutex.lock();
348                first_thread_locked_once.store(true, Ordering::SeqCst);
349                while !finished.load(Ordering::SeqCst) {
350                    if second_thread_locked_once.load(Ordering::SeqCst) {
351                        // Wait a little to guard against the unlikely event that
352                        // the other thread isn't blocked on acquiring the mutex yet.
353                        // If PyMutex had a try_lock implementation this would be
354                        // unnecessary
355                        std::thread::sleep(std::time::Duration::from_millis(10));
356                        // block (and hold the mutex) until the receiver actually receives something
357                        barrier.wait();
358                        finished.store(true, Ordering::SeqCst);
359                    }
360                }
361                drop(guard);
362            });
363
364            s.spawn(|| {
365                while !first_thread_locked_once.load(Ordering::SeqCst) {
366                    std::hint::spin_loop();
367                }
368                second_thread_locked_once.store(true, Ordering::SeqCst);
369                let guard = mutex.lock();
370                assert!(finished.load(Ordering::SeqCst));
371                drop(guard);
372            });
373
374            barrier.wait();
375        });
376    }
377
378    #[cfg(not(target_arch = "wasm32"))]
379    #[test]
380    fn test_recover_poison() {
381        let mutex = Python::attach(|py| -> PyMutex<Py<PyDict>> {
382            let d = PyDict::new(py);
383            d.set_item("hello", "world").unwrap();
384            PyMutex::new(d.unbind())
385        });
386
387        let lock = Arc::new(mutex);
388        let lock2 = Arc::clone(&lock);
389
390        let _ = thread::spawn(move || {
391            let _guard = lock2.lock().unwrap();
392
393            // poison the mutex
394            panic!();
395        })
396        .join();
397
398        // by now the lock is poisoned, use into_inner to recover the value despite that
399        let guard = match lock.lock() {
400            Ok(_) => {
401                unreachable!();
402            }
403            Err(poisoned) => poisoned.into_inner(),
404        };
405
406        Python::attach(|py| {
407            assert!(
408                (*guard)
409                    .bind(py)
410                    .get_item("hello")
411                    .unwrap()
412                    .unwrap()
413                    .extract::<&str>()
414                    .unwrap()
415                    == "world"
416            );
417        });
418
419        // now test recovering via PyMutex::into_inner
420        let mutex = PyMutex::new(0);
421        assert_eq!(mutex.into_inner().unwrap(), 0);
422
423        let mutex = PyMutex::new(0);
424        let _ = std::thread::scope(|s| {
425            s.spawn(|| {
426                let _guard = mutex.lock().unwrap();
427
428                // poison the mutex
429                panic!();
430            })
431            .join()
432        });
433
434        match mutex.into_inner() {
435            Ok(_) => {
436                unreachable!()
437            }
438            Err(e) => {
439                assert!(e.into_inner() == 0)
440            }
441        }
442
443        // now test recovering via PyMutex::clear_poison
444        let mutex = PyMutex::new(0);
445        let _ = std::thread::scope(|s| {
446            s.spawn(|| {
447                let _guard = mutex.lock().unwrap();
448
449                // poison the mutex
450                panic!();
451            })
452            .join()
453        });
454        mutex.clear_poison();
455        assert_eq!(*mutex.lock().unwrap(), 0);
456    }
457
458    #[test]
459    fn test_send_not_send() {
460        use crate::impl_::pyclass::{value_of, IsSend, IsSync};
461
462        assert!(!value_of!(IsSend, PyMutexGuard<'_, i32>));
463        assert!(value_of!(IsSync, PyMutexGuard<'_, i32>));
464
465        assert!(value_of!(IsSend, PyMutex<i32>));
466        assert!(value_of!(IsSync, PyMutex<i32>));
467    }
468}