Skip to main content

moirai_sync/sync/
futex_mutex.rs

1//! Mutex backed by a Linux futex, with a condvar fallback elsewhere.
2//!
3//! Both builds present the same API and the same guarantees; only the blocking
4//! mechanism differs, so the two protocols are described separately below.
5//!
6//! # Linux: three-state futex
7//!
8//! `state` is the classic Drepper encoding — 0 unlocked, 1 locked, 2 locked with
9//! waiters. The one rule that is easy to get wrong is stated at `lock_slow`:
10//! every acquisition through the slow path takes the lock with `swap(2)` rather
11//! than `CAS 0 -> 1`, because unlock only wakes when it observes 2. Acquiring at
12//! 1 after a wakeup would erase that marker while other threads are still
13//! parked, and the next unlock would skip the wake and strand them.
14//!
15//! # Elsewhere: `locked` flag plus condvar
16//!
17//! A waiter registers in `waiters`, then blocks on `condvar` while holding
18//! `fallback`; the unlocker clears `locked` and notifies only if `waiters` is
19//! non-zero. That pair of accesses is a store-buffer pattern — the waiter stores
20//! `waiters` then loads `locked`, the unlocker stores `locked` then loads
21//! `waiters` — so both sides carry a `SeqCst` fence between their store and
22//! their load. Without them each load may miss the other's store, and the
23//! unlocker skips a notify for a waiter that is about to sleep forever. Holding
24//! `fallback` across the `locked` check and the `condvar.wait` is what keeps a
25//! notify from landing in between.
26//!
27//! # `Send` and `Sync`
28//!
29//! `FutexMutex<T>` is `Send + Sync` for `T: Send`, matching `std::sync::Mutex`:
30//! the lock hands `&mut T` to one thread at a time, so `T` must be able to move
31//! between threads, but never needs to be shared by two at once.
32//!
33//! `FutexMutexGuard` is deliberately `Send`, unlike `std::sync::MutexGuard`.
34//! Neither protocol requires the releasing thread to be the acquiring one: the
35//! futex state is a plain atomic, and `fallback` is only ever held inside
36//! `lock_slow`/`unlock`, never across the guard's lifetime.
37//!
38//! The guard's `PhantomData<T>` is load-bearing rather than decorative — see the
39//! note on the field.
40
41#![cfg_attr(
42    not(target_os = "linux"),
43    expect(
44        clippy::unwrap_used,
45        reason = "ratchet MOIRAI-UNWRAP-1: pre-existing debt"
46    )
47)]
48
49use std::cell::UnsafeCell;
50use std::fmt;
51use std::hint;
52use std::ops::{Deref, DerefMut};
53use std::sync::atomic::Ordering;
54
55#[cfg(not(target_os = "linux"))]
56use std::sync::atomic::AtomicBool;
57
58#[cfg(target_os = "linux")]
59use std::sync::atomic::AtomicI32;
60
61/// Maximum generic spin attempts before falling back to blocking
62const MAX_SPIN_ATTEMPTS: usize = 64;
63
64#[cfg(target_os = "linux")]
65mod futex {
66    // Linux futex operations
67    const FUTEX_WAIT: i32 = 0;
68    const FUTEX_WAKE: i32 = 1;
69
70    /// Wait on a futex if the value matches expected
71    pub fn futex_wait(addr: *const i32, expected: i32) -> i32 {
72        // SAFETY: `addr` points at the caller's live `AtomicI32`. The kernel
73        // reads that word, compares it with `expected`, and blocks only while
74        // they match — the comparison is atomic with the sleep, so an unlock
75        // landing in between cannot be missed. The null timeout means no
76        // deadline, and the trailing arguments are unused by FUTEX_WAIT.
77        unsafe {
78            libc::syscall(
79                libc::SYS_futex,
80                addr,
81                FUTEX_WAIT,
82                expected,
83                std::ptr::null::<libc::timespec>(),
84                std::ptr::null::<i32>(),
85                0,
86            ) as i32
87        }
88    }
89
90    /// Wake up waiters on a futex
91    pub fn futex_wake(addr: *const i32, num_waiters: i32) -> i32 {
92        // SAFETY: `addr` points at the caller's live `AtomicI32`. FUTEX_WAKE
93        // only reads the address as a queue key; it neither loads nor stores
94        // the word, and the trailing arguments are unused by this operation.
95        unsafe {
96            libc::syscall(
97                libc::SYS_futex,
98                addr,
99                FUTEX_WAKE,
100                num_waiters,
101                std::ptr::null::<libc::timespec>(),
102                std::ptr::null::<i32>(),
103                0,
104            ) as i32
105        }
106    }
107}
108
109/// A futex-backed mutex on Linux with adaptive spinning; falls back to atomic spin on non-Linux.
110pub struct FutexMutex<T> {
111    #[cfg(target_os = "linux")]
112    state: AtomicI32, // 0 = unlocked, 1 = locked, 2 = locked with waiters
113    #[cfg(not(target_os = "linux"))]
114    locked: AtomicBool,
115    #[cfg(not(target_os = "linux"))]
116    waiters: std::sync::atomic::AtomicUsize,
117    #[cfg(not(target_os = "linux"))]
118    fallback: std::sync::Mutex<()>,
119    #[cfg(not(target_os = "linux"))]
120    condvar: std::sync::Condvar,
121    data: UnsafeCell<T>,
122}
123
124impl<T> fmt::Debug for FutexMutex<T> {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        #[cfg(target_os = "linux")]
127        let state = self.state.load(Ordering::Relaxed);
128        #[cfg(not(target_os = "linux"))]
129        let locked = self.locked.load(Ordering::Relaxed);
130
131        let mut d = f.debug_struct("FutexMutex");
132        #[cfg(target_os = "linux")]
133        d.field("state", &state);
134        #[cfg(not(target_os = "linux"))]
135        d.field("locked", &locked);
136
137        d.finish_non_exhaustive()
138    }
139}
140
141// SAFETY: the only non-`Sync` field is the `UnsafeCell<T>`, and it is reachable
142// solely through a guard, which the lock protocol hands to one thread at a time.
143// `T: Send` is therefore the exact bound — ownership of the data moves between
144// threads, but is never shared by two at once — and it is the same bound
145// `std::sync::Mutex` carries for the same reason.
146unsafe impl<T: Send> Send for FutexMutex<T> {}
147unsafe impl<T: Send> Sync for FutexMutex<T> {}
148
149impl<T> FutexMutex<T> {
150    /// Create a new fast mutex.
151    pub const fn new(data: T) -> Self {
152        Self {
153            #[cfg(target_os = "linux")]
154            state: AtomicI32::new(0),
155            #[cfg(not(target_os = "linux"))]
156            locked: AtomicBool::new(false),
157            #[cfg(not(target_os = "linux"))]
158            waiters: std::sync::atomic::AtomicUsize::new(0),
159            #[cfg(not(target_os = "linux"))]
160            fallback: std::sync::Mutex::new(()),
161            #[cfg(not(target_os = "linux"))]
162            condvar: std::sync::Condvar::new(),
163            data: UnsafeCell::new(data),
164        }
165    }
166
167    /// Lock the mutex with adaptive spinning.
168    pub fn lock(&self) -> FutexMutexGuard<'_, T> {
169        // Try to acquire the lock with spinning first
170        for _ in 0..MAX_SPIN_ATTEMPTS {
171            if self.try_lock_immediate() {
172                return FutexMutexGuard {
173                    mutex: self,
174                    _phantom: std::marker::PhantomData,
175                };
176            }
177            hint::spin_loop();
178        }
179
180        // Fall back to blocking
181        self.lock_slow();
182        FutexMutexGuard {
183            mutex: self,
184            _phantom: std::marker::PhantomData,
185        }
186    }
187
188    /// Try to lock the mutex without spinning or blocking.
189    /// Returns `Some(FutexMutexGuard)` if successful, `None` otherwise.
190    pub fn try_lock(&self) -> Option<FutexMutexGuard<'_, T>> {
191        if self.try_lock_immediate() {
192            Some(FutexMutexGuard {
193                mutex: self,
194                _phantom: std::marker::PhantomData,
195            })
196        } else {
197            None
198        }
199    }
200
201    #[inline]
202    fn try_lock_immediate(&self) -> bool {
203        #[cfg(target_os = "linux")]
204        {
205            // Strong, not weak: `try_lock` reports contention, and a weak
206            // exchange may fail on an unlocked mutex (load-linked/store-
207            // conditional targets, and Miri's injected failures), which would
208            // report a lock nobody holds.
209            self.state
210                .compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
211                .is_ok()
212        }
213        #[cfg(not(target_os = "linux"))]
214        {
215            if self.locked.load(Ordering::Relaxed) {
216                false
217            } else {
218                !self.locked.swap(true, Ordering::Acquire)
219            }
220        }
221    }
222
223    #[cold]
224    fn lock_slow(&self) {
225        #[cfg(target_os = "linux")]
226        {
227            // Three-state futex mutex (Drepper / Rust std `futex` mutex):
228            // 0 = unlocked, 1 = locked (no waiters), 2 = locked with waiters.
229            //
230            // A single uncontended attempt may leave the state at 1; every other
231            // acquisition through this slow path acquires by `swap(2)`, which
232            // conservatively preserves the "waiters present" marker. Acquiring
233            // via `CAS 0 -> 1` after a wakeup would erase that marker while other
234            // waiters are still parked, so the next `unlock` (which only wakes
235            // when `swap(0)` observes 2) would skip the wake and strand them — a
236            // lost-wakeup deadlock.
237            let mut state = self.state.load(Ordering::Relaxed);
238            if state == 0 {
239                match self
240                    .state
241                    .compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
242                {
243                    Ok(_) => return,
244                    Err(s) => state = s,
245                }
246            }
247
248            loop {
249                // Mark as contended and check whether it was actually free.
250                if state != 2 {
251                    state = self.state.swap(2, Ordering::Acquire);
252                    if state == 0 {
253                        return;
254                    }
255                }
256
257                // Sleep only while the state is still 2; `futex_wait` rechecks
258                // the value atomically, so a concurrent unlock to 0 cannot be
259                // missed here.
260                futex::futex_wait(self.state.as_ptr(), 2);
261                state = self.state.load(Ordering::Relaxed);
262            }
263        }
264        #[cfg(not(target_os = "linux"))]
265        {
266            self.waiters.fetch_add(1, Ordering::Relaxed);
267            // SeqCst fence pairs with the one in `unlock`: it separates this
268            // waiter's `waiters` store from its `locked` load (the swap below) in
269            // the global SeqCst order. Without it the two sides form an unguarded
270            // store-buffer pattern (waiter stores `waiters`/loads `locked`,
271            // unlocker stores `locked`/loads `waiters`) in which both loads may
272            // observe stale values: the unlocker sees `waiters == 0` and skips the
273            // notify while this waiter sees `locked == true` and sleeps forever.
274            std::sync::atomic::fence(Ordering::SeqCst);
275            let mut guard = self.fallback.lock().unwrap();
276            loop {
277                if !self.locked.swap(true, Ordering::Acquire) {
278                    self.waiters.fetch_sub(1, Ordering::Relaxed);
279                    return;
280                }
281                guard = self.condvar.wait(guard).unwrap();
282            }
283        }
284    }
285
286    fn unlock(&self) {
287        #[cfg(target_os = "linux")]
288        {
289            if self.state.swap(0, Ordering::Release) == 2 {
290                futex::futex_wake(self.state.as_ptr(), 1);
291            }
292        }
293        #[cfg(not(target_os = "linux"))]
294        {
295            self.locked.store(false, Ordering::Release);
296            // SeqCst fence pairs with the one in `lock_slow`: it separates this
297            // unlock's `locked` store from the `waiters` load below so the pair of
298            // accesses participates in the global SeqCst order. Without it, a
299            // StoreLoad reorder could let this load observe `waiters == 0` while a
300            // concurrently-registering waiter has not yet been made visible,
301            // skipping the wakeup and stranding that waiter on the condvar.
302            std::sync::atomic::fence(Ordering::SeqCst);
303            if self.waiters.load(Ordering::Acquire) > 0 {
304                let _guard = self.fallback.lock().unwrap();
305                self.condvar.notify_one();
306            }
307        }
308    }
309}
310
311/// Guard for FutexMutex that automatically unlocks on drop.
312pub struct FutexMutexGuard<'a, T> {
313    mutex: &'a FutexMutex<T>,
314    /// Ties the guard's auto traits to `T`, which is what makes `Sync` require
315    /// `T: Sync`.
316    ///
317    /// Without this field the guard's only member would be
318    /// `&FutexMutex<T>`, and that is `Sync` for any `T: Send` — so the guard
319    /// would be `Sync` too, and `&guard` would hand out `&T` to several threads
320    /// for a `T` that cannot be shared. Removing this is a soundness change, not
321    /// a cleanup; `guard_sync_requires_sync_data` fails if it goes.
322    _phantom: std::marker::PhantomData<T>,
323}
324
325impl<'a, T> Drop for FutexMutexGuard<'a, T> {
326    fn drop(&mut self) {
327        self.mutex.unlock();
328    }
329}
330
331impl<'a, T> Deref for FutexMutexGuard<'a, T> {
332    type Target = T;
333
334    fn deref(&self) -> &Self::Target {
335        // SAFETY: holding the guard means this thread holds the lock, so no
336        // other thread can hold a reference to the data at the same time.
337        unsafe { &*self.mutex.data.get() }
338    }
339}
340
341impl<'a, T> DerefMut for FutexMutexGuard<'a, T> {
342    fn deref_mut(&mut self) -> &mut Self::Target {
343        // SAFETY: as `deref`, and `&mut self` rules out any other borrow taken
344        // through this guard, so the returned reference is unique.
345        unsafe { &mut *self.mutex.data.get() }
346    }
347}
348
349#[cfg(test)]
350mod auto_traits {
351    use super::{FutexMutex, FutexMutexGuard};
352    use static_assertions::{assert_impl_all, assert_not_impl_any};
353    use std::cell::Cell;
354
355    assert_impl_all!(FutexMutex<u32>: Send, Sync);
356
357    // Unlike `std::sync::MutexGuard`, this guard may cross threads: neither
358    // protocol requires the releasing thread to be the acquiring one.
359    assert_impl_all!(FutexMutexGuard<'static, u32>: Send, Sync);
360
361    /// `Sync` must follow `T`, not the mutex reference.
362    ///
363    /// `Cell<u32>` is `Send` but not `Sync`, so `&FutexMutex<Cell<u32>>` is
364    /// `Sync` on its own. Only the guard's `PhantomData<T>` stops the guard from
365    /// inheriting that and handing `&Cell<u32>` to several threads at once, so
366    /// this is the assertion that fails if the field is ever dropped.
367    #[allow(dead_code)]
368    fn guard_sync_requires_sync_data() {
369        assert_not_impl_any!(FutexMutexGuard<'static, Cell<u32>>: Sync);
370    }
371}