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}