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}