thread_checked_lock/
mutex.rs

1use std::{
2    fmt::{Display, Formatter, Result as FmtResult},
3    ops::{Deref, DerefMut},
4    sync::{Mutex, MutexGuard, PoisonError, TryLockError as StdTryLockError},
5};
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10use crate::{locked_mutexes, mutex_id};
11use crate::mutex_id::MutexID;
12use crate::error::{AccessResult, LockError, LockResult, TryLockError, TryLockResult};
13
14
15/// A variant of [`std::sync::Mutex`] which gracefully returns an error when a thread attempts
16/// to acquire a `ThreadCheckedMutex` that it already holds.
17///
18/// In such a situation, [`Mutex::lock`] is guaranteed to either lock or panic, while
19/// [`Mutex::try_lock`] checks if *any* thread holds the lock (and cannot distinguish whether the
20/// current thread holds the lock). As such, attempting to lock the same `Mutex` twice on a thread
21/// is potentially a fatal error; `ThreadCheckedMutex` allows for recovery.
22#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23#[derive(Debug)]
24pub struct ThreadCheckedMutex<T: ?Sized> {
25    mutex_id: MutexID,
26    mutex:    Mutex<T>,
27}
28
29impl<T> ThreadCheckedMutex<T> {
30    /// Creates a new mutex in an unlocked state.
31    #[inline]
32    #[must_use]
33    pub fn new(t: T) -> Self {
34        Self {
35            mutex_id: mutex_id::next_id(),
36            mutex:    Mutex::new(t),
37        }
38    }
39}
40
41impl<T: ?Sized> ThreadCheckedMutex<T> {
42    /// Helper function for creating a [`ThreadCheckedMutexGuard`] from a [`MutexGuard`].
43    #[inline]
44    const fn new_guard<'a>(&self, guard: MutexGuard<'a, T>) -> ThreadCheckedMutexGuard<'a, T> {
45        ThreadCheckedMutexGuard {
46            mutex_id: self.mutex_id,
47            guard,
48        }
49    }
50
51    /// Helper function for mapping the type inside a [`PoisonError`] from [`MutexGuard`] to
52    /// [`ThreadCheckedMutexGuard`].
53    #[inline]
54    fn poisoned_guard<'a>(
55        &self,
56        poison: PoisonError<MutexGuard<'a, T>>,
57    ) -> PoisonError<ThreadCheckedMutexGuard<'a, T>> {
58        PoisonError::new(self.new_guard(poison.into_inner()))
59    }
60}
61
62impl<T: ?Sized> ThreadCheckedMutex<T> {
63    /// Attempts to acquire this mutex, blocking the current thread while the mutex is locked in
64    /// other threads.
65    ///
66    /// If the mutex is acquired (either completely successfully or with a poison error), a
67    /// [`ThreadCheckedMutexGuard`] is returned. Only one thread at a time can hold the lock; at
68    /// most one [`ThreadCheckedMutexGuard`] can exist at a time (across any thread); and the mutex
69    /// is unlocked when the returned guard is dropped.
70    ///
71    /// # Errors
72    /// If the mutex was already held by the current thread when this call was made, then a
73    /// [`LockedByCurrentThread`] error is returned.
74    ///
75    /// If another user of this mutex panicked while holding the mutex, then this call will still
76    /// acquire the mutex but wrap the returned guard in a poison error. See the
77    /// [`HandlePoisonResult`] trait for methods to ignore poison errors and treat them as
78    /// successful, or to panic if a poison error was returned.
79    ///
80    /// [`HandlePoisonResult`]: crate::HandlePoisonResult
81    /// [`LockedByCurrentThread`]: LockError::LockedByCurrentThread
82    pub fn lock(&self) -> LockResult<ThreadCheckedMutexGuard<'_, T>> {
83        if locked_mutexes::register_locked(self.mutex_id) {
84            match self.mutex.lock() {
85                Ok(guard)   => Ok(self.new_guard(guard)),
86                Err(poison) => {
87                    let poison = self.poisoned_guard(poison);
88                    Err(LockError::Poisoned(poison))
89                }
90            }
91        } else {
92            Err(LockError::LockedByCurrentThread)
93        }
94    }
95
96    /// Attempts to acquire this mutex without blocking.
97    ///
98    /// If the mutex is acquired (either completely successfully or with a poison error), a
99    /// [`ThreadCheckedMutexGuard`] is returned. Only one thread at a time can hold the lock; at
100    /// most one [`ThreadCheckedMutexGuard`] can exist at a time (across any thread); and the mutex
101    /// is unlocked when the returned guard is dropped.
102    ///
103    /// # Errors
104    /// If the mutex was already held by the current thread when this call was made, then a
105    /// [`LockedByCurrentThread`] error is returned. If the mutex was held by a different thread,
106    /// then a [`WouldBlock`] error is returned.
107    ///
108    /// If another user of this mutex panicked while holding the mutex, then this call will still
109    /// acquire the mutex but wrap the returned guard in a poison error. See the
110    /// [`HandlePoisonResult`] trait for methods to ignore poison errors and treat them as
111    /// successful, or to panic if a poison error was returned.
112    ///
113    /// [`HandlePoisonResult`]: crate::HandlePoisonResult
114    /// [`LockedByCurrentThread`]: TryLockError::LockedByCurrentThread
115    /// [`WouldBlock`]: TryLockError::WouldBlock
116    pub fn try_lock(&self) -> TryLockResult<ThreadCheckedMutexGuard<'_, T>> {
117        if self.locked_by_current_thread() {
118            return Err(TryLockError::LockedByCurrentThread);
119        }
120
121        match self.mutex.try_lock() {
122            Ok(guard) => {
123                #[expect(
124                    clippy::let_underscore_must_use,
125                    clippy::redundant_type_annotations,
126                    reason = "We already checked that the current thread hasn't locked the mutex, \
127                              so this always returns true.",
128                )]
129                let _: bool = locked_mutexes::register_locked(self.mutex_id);
130                Ok(self.new_guard(guard))
131            }
132            Err(StdTryLockError::Poisoned(poison)) => {
133                #[expect(
134                    clippy::let_underscore_must_use,
135                    clippy::redundant_type_annotations,
136                    reason = "We already checked that the current thread hasn't locked the mutex, \
137                              so this always returns true.",
138                )]
139                let _: bool = locked_mutexes::register_locked(self.mutex_id);
140                let poison = self.poisoned_guard(poison);
141                Err(TryLockError::Poisoned(poison))
142            }
143            Err(StdTryLockError::WouldBlock) => Err(TryLockError::WouldBlock),
144        }
145    }
146
147    /// Determines whether this mutex is currently held by the current thread.
148    #[inline]
149    #[must_use]
150    pub fn locked_by_current_thread(&self) -> bool {
151        locked_mutexes::locked_by_current_thread(self.mutex_id)
152    }
153
154    /// Determines whether this mutex is currently poisoned.
155    ///
156    /// If another thread is active, the mutex could become poisoned or have its poison cleared
157    /// at any time; as such, the return value of this function should generally not be depended on
158    /// for program correctness.
159    ///
160    /// [Read more about poison.](crate::HandlePoisonResult#about-poison)
161    #[inline]
162    #[must_use]
163    pub fn is_poisoned(&self) -> bool {
164        self.mutex.is_poisoned()
165    }
166
167    /// Clear any poison from this mutex.
168    ///
169    /// When a [`ThreadCheckedMutexGuard`] is dropped in a thread which is panicking, its associated
170    /// mutex becomes poisoned, and remains poisoned until this function is called (by any thread).
171    ///
172    /// [Read more about poison.](crate::HandlePoisonResult#about-poison)
173    #[inline]
174    pub fn clear_poison(&self) {
175        self.mutex.clear_poison();
176    }
177
178    /// Consumes this mutex and returns the underlying data.
179    ///
180    /// # Errors
181    /// If another user of this mutex panicked while holding the mutex, then the inner data is
182    /// still returned, but wrapped in a poison error.
183    ///
184    /// [Read more about poison.](crate::HandlePoisonResult#about-poison)
185    #[inline]
186    pub fn into_inner(self) -> AccessResult<T>
187    where
188        T: Sized,
189    {
190        self.mutex.into_inner().map_err(Into::into)
191    }
192
193    /// Returns a mutable reference to the underlying data, without locking.
194    ///
195    /// # Errors
196    /// If another user of this mutex panicked while holding the mutex, then a mutable reference is
197    /// still returned, but wrapped in a poison error.
198    ///
199    /// [Read more about poison.](crate::HandlePoisonResult#about-poison)
200    #[inline]
201    pub fn get_mut(&mut self) -> AccessResult<&mut T> {
202        self.mutex.get_mut().map_err(Into::into)
203    }
204}
205
206impl<T: Default> Default for ThreadCheckedMutex<T> {
207    #[inline]
208    fn default() -> Self {
209        Self::new(T::default())
210    }
211}
212
213/// A RAII scoped lock for a [`ThreadCheckedMutex`], analogous to [`MutexGuard`] for [`Mutex`].
214///
215/// When this guard is dropped, the corresponding [`ThreadCheckedMutex`] is unlocked. The guard
216/// provides access to the mutex's protected data via [`Deref`] and [`DerefMut`].
217///
218/// This structure can be created via the [`lock`] and [`try_lock`] methods of
219/// [`ThreadCheckedMutex`].
220///
221/// [`lock`]: ThreadCheckedMutex::lock
222/// [`try_lock`]: ThreadCheckedMutex::try_lock
223#[must_use = "if unused the ThreadCheckedMutex will immediately unlock"]
224#[clippy::has_significant_drop]
225#[derive(Debug)]
226pub struct ThreadCheckedMutexGuard<'a, T: ?Sized> {
227    mutex_id: MutexID,
228    guard:    MutexGuard<'a, T>,
229}
230
231impl<T: ?Sized> Drop for ThreadCheckedMutexGuard<'_, T> {
232    #[inline]
233    fn drop(&mut self) {
234        let was_locked = locked_mutexes::register_unlocked(self.mutex_id);
235
236        // This assertion should not fail unless someone used unsound unsafe code.
237        debug_assert!(
238            was_locked,
239            "a ThreadCheckedMutexGuard was dropped in a thread which it was not locked in",
240        );
241    }
242}
243
244impl<T: ?Sized> Deref for ThreadCheckedMutexGuard<'_, T> {
245    type Target = T;
246
247    #[inline]
248    fn deref(&self) -> &Self::Target {
249        &self.guard
250    }
251}
252
253impl<T: ?Sized> DerefMut for ThreadCheckedMutexGuard<'_, T> {
254    #[inline]
255    fn deref_mut(&mut self) -> &mut Self::Target {
256        &mut self.guard
257    }
258}
259
260impl<T: ?Sized + Display> Display for ThreadCheckedMutexGuard<'_, T> {
261    #[inline]
262    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
263        Display::fmt(&*self.guard, f)
264    }
265}
266
267
268#[cfg(test)]
269mod tests {
270    #![expect(clippy::unwrap_used, reason = "these are tests")]
271
272    use std::{sync::mpsc, thread};
273    use std::{sync::Arc, time::Duration};
274
275    use crate::mutex_id::run_this_before_each_test_that_creates_a_mutex_id;
276    use super::*;
277
278
279    #[test]
280    fn lock_then_is_locked() {
281        run_this_before_each_test_that_creates_a_mutex_id();
282
283        let mutex = ThreadCheckedMutex::new(0_u8);
284
285        assert!(!mutex.locked_by_current_thread());
286
287        let _guard = mutex.lock().unwrap();
288
289        assert!(mutex.locked_by_current_thread());
290    }
291
292    #[test]
293    fn lock_unlock_isnt_locked() {
294        run_this_before_each_test_that_creates_a_mutex_id();
295
296        let mutex = ThreadCheckedMutex::new(0_u8);
297
298        let guard = mutex.lock().unwrap();
299
300        assert!(mutex.locked_by_current_thread());
301
302        drop(guard);
303
304        assert!(!mutex.locked_by_current_thread());
305    }
306
307    #[test]
308    fn lock_unlock_lock() {
309        run_this_before_each_test_that_creates_a_mutex_id();
310
311        let mutex = ThreadCheckedMutex::new(0_u8);
312
313        {
314            let _guard = mutex.lock().unwrap();
315        }
316
317        assert!(!mutex.locked_by_current_thread());
318
319        let _guard = mutex.lock().unwrap();
320
321        assert!(mutex.locked_by_current_thread());
322    }
323
324    #[test]
325    fn lock_lock_unlock_lock() {
326        run_this_before_each_test_that_creates_a_mutex_id();
327
328        let mutex = ThreadCheckedMutex::new(0_u8);
329
330        let guard = mutex.lock().unwrap();
331
332        // An additional attempt to lock should fail.
333        assert!(matches!(
334            mutex.lock(),
335            Err(LockError::LockedByCurrentThread),
336        ));
337
338        drop(guard);
339
340        // Now it should succeed.
341        let _guard = mutex.lock().unwrap();
342    }
343
344    #[test]
345    fn locked_by_current_thread() {
346        run_this_before_each_test_that_creates_a_mutex_id();
347
348        let mutex = Arc::new(ThreadCheckedMutex::new(()));
349        let (sender, receiver) = mpsc::channel();
350
351        let mutex_clone = Arc::clone(&mutex);
352
353        thread::spawn(move || {
354            let guard = mutex_clone.try_lock().unwrap();
355            drop(guard);
356            sender.send(()).unwrap();
357        });
358
359        // Wait to receive something.
360        receiver.recv().unwrap();
361
362        // The mutex should have been unlocked before we received anything.
363        let _guard = mutex.try_lock().unwrap();
364
365        // An additional attempt to lock should fail.
366        assert!(matches!(
367            mutex.try_lock(),
368            Err(TryLockError::LockedByCurrentThread),
369        ));
370    }
371
372    #[test]
373    fn would_block() {
374        run_this_before_each_test_that_creates_a_mutex_id();
375
376        let mutex = Arc::new(ThreadCheckedMutex::new(()));
377        let (locking_sender, locking_receiver) = mpsc::channel();
378        let (unlocking_sender, unlocking_receiver) = mpsc::channel();
379
380        let mutex_clone = Arc::clone(&mutex);
381
382        thread::spawn(move || {
383            let guard = mutex_clone.try_lock().unwrap();
384
385            locking_sender.send(()).unwrap();
386
387            // Wait to receive something.
388            unlocking_receiver.recv().unwrap();
389
390            // Block for a bit, to try to ensure that `lock` is capable of waiting.
391            thread::sleep(Duration::from_millis(50));
392
393            drop(guard);
394        });
395
396        // Wait to receive something.
397        locking_receiver.recv().unwrap();
398
399        // The mutex should have been locked before we received anything, and since we haven't
400        // sent anything, it should still be locked.
401
402        assert!(matches!(
403            mutex.try_lock(),
404            Err(TryLockError::WouldBlock),
405        ));
406
407        unlocking_sender.send(()).unwrap();
408
409        // Now `lock` should work, though `try_lock` might not.
410        let _guard = mutex.lock().unwrap();
411    }
412}