Skip to main content

pyo3/sync/
mutex.rs

1use core::cell::UnsafeCell;
2use core::marker::PhantomData;
3use core::ops::{Deref, DerefMut};
4#[cfg(panic = "unwind")]
5use core::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{LockResult, PoisonError};
7#[cfg(panic = "unwind")]
8use std::thread;
9
10// See core::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    pub(crate) mutex: UnsafeCell<crate::ffi::PyMutex>,
121    poison: Flag,
122    pub(crate) 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/// SAFETY: `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/// SAFETY: `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/// SAFETY: `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`]: alloc::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        // SAFETY: valid pointer to mutex passed to `PyMutex_Lock`
168        // and the mutex is not moved while locked
169        unsafe { crate::ffi::PyMutex_Lock(self.mutex.get()) };
170        PyMutexGuard::new(self)
171    }
172
173    /// Create a new mutex in an unlocked state ready for use.
174    pub const fn new(value: T) -> Self {
175        Self {
176            mutex: UnsafeCell::new(crate::ffi::PyMutex::new()),
177            data: UnsafeCell::new(value),
178            poison: Flag::new(),
179        }
180    }
181
182    /// Check if the mutex is locked.
183    ///
184    /// Note that this is only useful for debugging or test purposes and should
185    /// not be used to make concurrency control decisions, as the lock state may
186    /// change immediately after the check.
187    #[cfg(Py_3_14)]
188    pub fn is_locked(&self) -> bool {
189        // SAFETY: valid pointer to mutex passed to `PyMutex_IsLocked`
190        let ret = unsafe { crate::ffi::PyMutex_IsLocked(self.mutex.get()) };
191        ret != 0
192    }
193
194    /// Consumes this mutex, returning the underlying data.
195    ///
196    /// # Errors
197    ///
198    /// If another user of this mutex panicked while holding the mutex, then
199    /// this call will return an error containing the underlying data
200    /// instead.
201    pub fn into_inner(self) -> LockResult<T>
202    where
203        T: Sized,
204    {
205        let data = self.data.into_inner();
206        map_result(self.poison.borrow(), |()| data)
207    }
208
209    /// Clear the poisoned state from a mutex.
210    ///
211    /// If the mutex is poisoned, it will remain poisoned until this function is called. This
212    /// allows recovering from a poisoned state and marking that it has recovered. For example, if
213    /// the value is overwritten by a known-good value, then the mutex can be marked as
214    /// un-poisoned. Or possibly, the value could be inspected to determine if it is in a
215    /// consistent state, and if so the poison is removed.
216    pub fn clear_poison(&self) {
217        self.poison.clear();
218    }
219}
220
221#[cfg_attr(not(panic = "unwind"), allow(clippy::unnecessary_wraps))]
222fn map_result<T, U, F>(result: LockResult<T>, f: F) -> LockResult<U>
223where
224    F: FnOnce(T) -> U,
225{
226    match result {
227        Ok(t) => Ok(f(t)),
228        #[cfg(panic = "unwind")]
229        Err(e) => Err(PoisonError::new(f(e.into_inner()))),
230        #[cfg(not(panic = "unwind"))]
231        Err(_) => {
232            unreachable!();
233        }
234    }
235}
236
237impl<'mutex, T: ?Sized> PyMutexGuard<'mutex, T> {
238    fn new(lock: &'mutex PyMutex<T>) -> LockResult<PyMutexGuard<'mutex, T>> {
239        map_result(lock.poison.guard(), |guard| PyMutexGuard {
240            inner: lock,
241            poison: guard,
242            _phantom: PhantomData,
243        })
244    }
245}
246
247impl<'a, T: ?Sized> Drop for PyMutexGuard<'a, T> {
248    fn drop(&mut self) {
249        self.inner.poison.done(&self.poison);
250        // SAFETY: valid pointer to mutex passed to `PyMutex_Unlock`
251        unsafe { crate::ffi::PyMutex_Unlock(self.inner.mutex.get()) };
252    }
253}
254
255impl<'a, T> Deref for PyMutexGuard<'a, T> {
256    type Target = T;
257
258    fn deref(&self) -> &T {
259        // safety: cannot be null pointer because PyMutex::new always
260        // creates a valid PyMutex pointer
261        unsafe { &*self.inner.data.get() }
262    }
263}
264
265impl<'a, T> DerefMut for PyMutexGuard<'a, T> {
266    fn deref_mut(&mut self) -> &mut T {
267        // safety: cannot be null pointer because PyMutex::new always
268        // creates a valid PyMutex pointer
269        unsafe { &mut *self.inner.data.get() }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    #[cfg(not(target_arch = "wasm32"))]
276    use alloc::sync::Arc;
277    #[cfg(not(target_arch = "wasm32"))]
278    use core::sync::atomic::{AtomicBool, Ordering};
279    #[cfg(not(target_arch = "wasm32"))]
280    use std::sync::Barrier;
281
282    use super::*;
283    #[cfg(not(target_arch = "wasm32"))]
284    use crate::types::{PyAnyMethods, PyDict, PyDictMethods, PyNone};
285    #[cfg(not(target_arch = "wasm32"))]
286    use crate::Py;
287    #[cfg(not(target_arch = "wasm32"))]
288    use crate::Python;
289
290    #[cfg(not(target_arch = "wasm32"))]
291    #[test]
292    fn test_pymutex() {
293        let mutex = Python::attach(|py| -> PyMutex<Py<PyDict>> {
294            let d = PyDict::new(py);
295            PyMutex::new(d.unbind())
296        });
297        #[cfg_attr(not(Py_3_14), allow(unused_variables))]
298        let mutex = Python::attach(|py| {
299            let mutex = py.detach(|| -> PyMutex<Py<PyDict>> {
300                std::thread::spawn(|| {
301                    let dict_guard = mutex.lock().unwrap();
302                    Python::attach(|py| {
303                        let dict = dict_guard.bind(py);
304                        dict.set_item(PyNone::get(py), PyNone::get(py)).unwrap();
305                    });
306                    #[cfg(Py_3_14)]
307                    assert!(mutex.is_locked());
308                    drop(dict_guard);
309                    #[cfg(Py_3_14)]
310                    assert!(!mutex.is_locked());
311                    mutex
312                })
313                .join()
314                .unwrap()
315            });
316
317            let dict_guard = mutex.lock().unwrap();
318            #[cfg(Py_3_14)]
319            assert!(mutex.is_locked());
320            let d = dict_guard.bind(py);
321
322            assert!(d
323                .get_item(PyNone::get(py))
324                .unwrap()
325                .unwrap()
326                .eq(PyNone::get(py))
327                .unwrap());
328            #[cfg(Py_3_14)]
329            assert!(mutex.is_locked());
330            drop(dict_guard);
331            #[cfg(Py_3_14)]
332            assert!(!mutex.is_locked());
333            mutex
334        });
335        #[cfg(Py_3_14)]
336        assert!(!mutex.is_locked());
337    }
338
339    #[cfg(not(target_arch = "wasm32"))]
340    #[test]
341    fn test_pymutex_blocks() {
342        let mutex = PyMutex::new(());
343        let first_thread_locked_once = AtomicBool::new(false);
344        let second_thread_locked_once = AtomicBool::new(false);
345        let finished = AtomicBool::new(false);
346        let barrier = Barrier::new(2);
347
348        std::thread::scope(|s| {
349            s.spawn(|| {
350                let guard = mutex.lock();
351                first_thread_locked_once.store(true, Ordering::SeqCst);
352                while !finished.load(Ordering::SeqCst) {
353                    if second_thread_locked_once.load(Ordering::SeqCst) {
354                        // Wait a little to guard against the unlikely event that
355                        // the other thread isn't blocked on acquiring the mutex yet.
356                        // If PyMutex had a try_lock implementation this would be
357                        // unnecessary
358                        std::thread::sleep(core::time::Duration::from_millis(10));
359                        // block (and hold the mutex) until the receiver actually receives something
360                        barrier.wait();
361                        finished.store(true, Ordering::SeqCst);
362                    }
363                }
364                drop(guard);
365            });
366
367            s.spawn(|| {
368                while !first_thread_locked_once.load(Ordering::SeqCst) {
369                    core::hint::spin_loop();
370                }
371                second_thread_locked_once.store(true, Ordering::SeqCst);
372                let guard = mutex.lock();
373                assert!(finished.load(Ordering::SeqCst));
374                drop(guard);
375            });
376
377            barrier.wait();
378        });
379    }
380
381    #[cfg(not(target_arch = "wasm32"))]
382    #[test]
383    fn test_recover_poison() {
384        let mutex = Python::attach(|py| -> PyMutex<Py<PyDict>> {
385            let d = PyDict::new(py);
386            d.set_item("hello", "world").unwrap();
387            PyMutex::new(d.unbind())
388        });
389
390        let lock = Arc::new(mutex);
391        let lock2 = Arc::clone(&lock);
392
393        let _ = thread::spawn(move || {
394            let _guard = lock2.lock().unwrap();
395
396            // poison the mutex
397            panic!();
398        })
399        .join();
400
401        // by now the lock is poisoned, use into_inner to recover the value despite that
402        let guard = match lock.lock() {
403            Ok(_) => {
404                unreachable!();
405            }
406            Err(poisoned) => poisoned.into_inner(),
407        };
408
409        Python::attach(|py| {
410            assert!(
411                (*guard)
412                    .bind(py)
413                    .get_item("hello")
414                    .unwrap()
415                    .unwrap()
416                    .extract::<&str>()
417                    .unwrap()
418                    == "world"
419            );
420        });
421
422        // now test recovering via PyMutex::into_inner
423        let mutex = PyMutex::new(0);
424        assert_eq!(mutex.into_inner().unwrap(), 0);
425
426        let mutex = PyMutex::new(0);
427        let _ = std::thread::scope(|s| {
428            s.spawn(|| {
429                let _guard = mutex.lock().unwrap();
430
431                // poison the mutex
432                panic!();
433            })
434            .join()
435        });
436
437        match mutex.into_inner() {
438            Ok(_) => {
439                unreachable!()
440            }
441            Err(e) => {
442                assert!(e.into_inner() == 0)
443            }
444        }
445
446        // now test recovering via PyMutex::clear_poison
447        let mutex = PyMutex::new(0);
448        let _ = std::thread::scope(|s| {
449            s.spawn(|| {
450                let _guard = mutex.lock().unwrap();
451
452                // poison the mutex
453                panic!();
454            })
455            .join()
456        });
457        mutex.clear_poison();
458        assert_eq!(*mutex.lock().unwrap(), 0);
459    }
460
461    #[test]
462    fn test_send_not_send() {
463        use crate::impl_::pyclass::{value_of, IsSend, IsSync};
464
465        assert!(!value_of!(IsSend, PyMutexGuard<'_, i32>));
466        assert!(value_of!(IsSync, PyMutexGuard<'_, i32>));
467
468        assert!(value_of!(IsSend, PyMutex<i32>));
469        assert!(value_of!(IsSync, PyMutex<i32>));
470    }
471}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here