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}