Skip to main content

yo_common/
lock.rs

1//! One owner at a time, for the state that more than one thread can reach.
2//!
3//! The engine is built so that most state has a single owner and needs no lock
4//! at all. The stripes are the exception. A stripe is a piece of the keyspace,
5//! and once a server runs commands on more than one thread the same stripe can
6//! be wanted by two of them at once. This is the thing that decides which one
7//! gets it.
8//!
9//! It is a spin lock, and that is a deliberate choice rather than a shortcut. A
10//! stripe is held for one command, which is tens or hundreds of nanoseconds, so
11//! a waiter that parks in the kernel would spend more time going to sleep and
12//! waking up than it would have spent waiting. The wait here is a short spin
13//! and then a yield, which is the shape that fits a hold time this short. It is
14//! the wrong shape for anything held across a syscall, so nothing held across a
15//! syscall should use it.
16//!
17//! ```
18//! use yo_common::lock::Lock;
19//!
20//! let counter = Lock::new(0u64);
21//! *counter.lock() += 1;
22//! assert_eq!(*counter.lock(), 1);
23//! ```
24//!
25//! # Taking two of them
26//!
27//! Two locks taken at once are a deadlock waiting for the order to disagree,
28//! and the answer is the one the command layer already uses: when a command
29//! names keys in several stripes, the stripes are taken in stripe order, so two
30//! commands that want the same pair want it the same way round. Nothing here
31//! enforces that, because a lock cannot see the other locks.
32//!
33//! What it can see is the other half of the same mistake, which is one thread
34//! taking the same lock twice. That is a hang in a release build and there is
35//! nothing to see when it happens, so a debug build remembers who holds a lock
36//! and panics rather than spinning forever. Tests and the fuzzers run in debug
37//! builds, so the mistake is a failure with a message instead of a test that
38//! never finishes.
39
40use core::hint;
41use core::marker::PhantomData;
42use core::ops::{Deref, DerefMut};
43
44use crate::sync::{AtomicBool, Ordering, UnsafeCell, yield_now};
45
46/// How many times a waiter spins before it starts yielding the core instead.
47///
48/// Short, because the point of the spin is to cover a hold that ends in tens of
49/// nanoseconds. If it did not end that fast the waiter is better off letting
50/// the holder have the core back, and that is what the yield is for.
51#[cfg(not(loom))]
52const SPINS: u32 = 40;
53
54/// Under the model checker there is no such thing as waiting a little, and
55/// every spin is another interleaving to try, so a waiter goes straight to the
56/// yield and the model stays small enough to finish.
57#[cfg(loom)]
58const SPINS: u32 = 0;
59
60/// A value that one thread at a time can reach.
61///
62/// [`lock`](Lock::lock) waits for it and hands back a [`Held`], which derefs
63/// to the value and releases the lock when it is dropped. A caller holding the
64/// lock by exclusive reference skips all of that through
65/// [`get_mut`](Lock::get_mut), which is how single threaded code and setup code
66/// reach the value for free.
67pub struct Lock<T> {
68    held: AtomicBool,
69    /// Who holds it, in debug builds, for the re-entrancy check. Zero is free.
70    #[cfg(debug_assertions)]
71    owner: core::sync::atomic::AtomicU64,
72    value: UnsafeCell<T>,
73}
74
75// SAFETY: the lock is what makes the value safe to share. Only one thread can
76// hold the lock at a time, and a `Held` is the only way to reach the value
77// through a shared reference, so the value is never touched by two threads at
78// once. It has to move between threads for that to be worth anything, which is
79// why the bound is `Send` and not `Sync`.
80unsafe impl<T: Send> Sync for Lock<T> {}
81// SAFETY: sending the lock sends the value, which `Send` already allows.
82unsafe impl<T: Send> Send for Lock<T> {}
83
84impl<T> Lock<T> {
85    /// Put `value` behind a lock that nobody holds yet.
86    #[cfg(not(loom))]
87    pub const fn new(value: T) -> Self {
88        Self {
89            held: AtomicBool::new(false),
90            #[cfg(debug_assertions)]
91            owner: core::sync::atomic::AtomicU64::new(0),
92            value: UnsafeCell::new(value),
93        }
94    }
95
96    /// The same, for the model checker, whose atomics cannot be built in a
97    /// constant because each one registers itself with the running model.
98    #[cfg(loom)]
99    pub fn new(value: T) -> Self {
100        Self {
101            held: AtomicBool::new(false),
102            #[cfg(debug_assertions)]
103            owner: core::sync::atomic::AtomicU64::new(0),
104            value: UnsafeCell::new(value),
105        }
106    }
107
108    /// Wait for the lock and take it.
109    ///
110    /// # Panics
111    ///
112    /// In a debug build, if the calling thread already holds this lock. In a
113    /// release build that case spins forever instead, which is the usual
114    /// bargain for a check that costs something on the hot path.
115    #[inline]
116    pub fn lock(&self) -> Held<'_, T> {
117        if !self.take() {
118            self.wait();
119        }
120        self.claim();
121        Held {
122            lock: self,
123            stays: PhantomData,
124        }
125    }
126
127    /// Take the lock if it is free, and give up rather than wait if it is not.
128    ///
129    /// Returns `None` if another thread holds it, and also if the calling
130    /// thread does, since a lock cannot be taken twice by anyone.
131    #[inline]
132    pub fn try_lock(&self) -> Option<Held<'_, T>> {
133        if !self.take() {
134            return None;
135        }
136        self.claim();
137        Some(Held {
138            lock: self,
139            stays: PhantomData,
140        })
141    }
142
143    /// Reach the value without locking, because the caller owns the lock.
144    ///
145    /// An exclusive reference to the lock is already proof that no other thread
146    /// can be holding it, so this costs nothing at all. Setup, teardown and
147    /// everything a single threaded server does can go through here.
148    #[inline]
149    pub fn get_mut(&mut self) -> &mut T {
150        self.value.get_mut()
151    }
152
153    /// Take the value back out and drop the lock around it.
154    pub fn into_inner(self) -> T {
155        self.value.into_inner()
156    }
157
158    /// Whether somebody holds it, which is only ever a hint.
159    ///
160    /// True can be stale by the time the caller reads it and so can false. It
161    /// is here for statistics and for tests, not for deciding anything.
162    #[inline]
163    pub fn is_held(&self) -> bool {
164        self.held.load(Ordering::Relaxed)
165    }
166
167    /// One attempt at the flag, with no waiting and no bookkeeping.
168    #[inline]
169    fn take(&self) -> bool {
170        self.held
171            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
172            .is_ok()
173    }
174
175    /// The slow path: spin while it is held, then yield, until it is ours.
176    ///
177    /// The load in the middle is what keeps this from being a queue of writes
178    /// to the same cache line. A waiter that keeps trying to swap the flag
179    /// takes the line away from the holder over and over and makes the hold
180    /// longer, so a waiter reads until the flag looks free and only then tries.
181    #[cold]
182    fn wait(&self) {
183        self.mine_already();
184        let mut spins = 0;
185        loop {
186            while self.held.load(Ordering::Relaxed) {
187                if spins < SPINS {
188                    spins += 1;
189                    hint::spin_loop();
190                } else {
191                    yield_now();
192                }
193            }
194            if self.take() {
195                return;
196            }
197        }
198    }
199
200    /// Record that this thread holds it, in the builds that keep track.
201    #[inline]
202    fn claim(&self) {
203        #[cfg(debug_assertions)]
204        self.owner.store(me(), Ordering::Relaxed);
205    }
206
207    /// Forget who holds it, before the flag says anybody can.
208    #[inline]
209    fn disclaim(&self) {
210        #[cfg(debug_assertions)]
211        self.owner.store(0, Ordering::Relaxed);
212    }
213
214    /// Panic instead of spinning forever on a lock this thread already holds.
215    #[inline]
216    fn mine_already(&self) {
217        #[cfg(debug_assertions)]
218        assert!(
219            self.owner.load(Ordering::Relaxed) != me(),
220            "this thread already holds this lock, and waiting for itself will \
221             never end"
222        );
223    }
224}
225
226impl<T: Default> Default for Lock<T> {
227    fn default() -> Self {
228        Self::new(T::default())
229    }
230}
231
232/// The lock, held.
233///
234/// Derefs to the value, and gives it back when it is dropped. It cannot be sent
235/// to another thread, because the thread that took a lock is the thread that
236/// has to release it.
237pub struct Held<'a, T> {
238    lock: &'a Lock<T>,
239    /// A guard is tied to the thread that took the lock, the same way a
240    /// `MutexGuard` is, and a raw pointer in a field is how a type says it does
241    /// not go to another thread.
242    stays: PhantomData<*const ()>,
243}
244
245// SAFETY: `&Held<T>` gives out `&T` and nothing else, so sharing the guard is
246// sharing the value. The raw pointer above took `Sync` away along with `Send`,
247// and this puts back the half that was true.
248unsafe impl<T: Sync> Sync for Held<'_, T> {}
249
250impl<T> Deref for Held<'_, T> {
251    type Target = T;
252
253    #[inline]
254    fn deref(&self) -> &T {
255        // SAFETY: we hold the lock, so no other thread has a reference to the
256        // value, and the guard borrows the lock so it cannot go away first.
257        self.lock.value.with(|p| unsafe { &*p })
258    }
259}
260
261impl<T> DerefMut for Held<'_, T> {
262    #[inline]
263    fn deref_mut(&mut self) -> &mut T {
264        // SAFETY: as above, and the exclusive borrow of the guard is what makes
265        // this the only reference to the value that exists.
266        self.lock.value.with(|p| unsafe { &mut *p })
267    }
268}
269
270impl<T> Drop for Held<'_, T> {
271    #[inline]
272    fn drop(&mut self) {
273        self.lock.disclaim();
274        self.lock.held.store(false, Ordering::Release);
275    }
276}
277
278/// A number that means this thread and no other, for the debug check.
279///
280/// A counter rather than the thread id, because the standard one cannot be had
281/// as a number on stable and getting it touches an `Arc`. Numbers are never
282/// reused, so a thread that has exited cannot be mistaken for a live one, and
283/// zero is kept back to mean nobody. During thread teardown the slot may be
284/// gone, in which case there is no answer and the check quietly does not fire.
285#[cfg(debug_assertions)]
286fn me() -> u64 {
287    use core::cell::Cell;
288    use core::sync::atomic::AtomicU64;
289
290    static NEXT: AtomicU64 = AtomicU64::new(1);
291    thread_local! {
292        static ME: Cell<u64> = const { Cell::new(0) };
293    }
294
295    ME.try_with(|slot| {
296        let mut id = slot.get();
297        if id == 0 {
298            id = NEXT.fetch_add(1, Ordering::Relaxed);
299            slot.set(id);
300        }
301        id
302    })
303    .unwrap_or(u64::MAX)
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    /// How many threads the contended tests run. Four is enough for a waiter
311    /// to find the lock held, and small enough to be four on a laptop too.
312    const HANDS: u64 = 4;
313
314    // Scaled down under Miri, which interprets every instruction and runs the
315    // threads itself. The contention still happens, only the repetition goes.
316    #[cfg(miri)]
317    const ROUNDS: u64 = 20;
318    #[cfg(not(miri))]
319    const ROUNDS: u64 = 250;
320
321    #[cfg(miri)]
322    const HOLDS: u64 = 3;
323    #[cfg(not(miri))]
324    const HOLDS: u64 = 20;
325
326    #[cfg(miri)]
327    const INSIDE: u64 = 50;
328    #[cfg(not(miri))]
329    const INSIDE: u64 = 2_000;
330
331    #[test]
332    fn what_goes_in_is_what_comes_out_the_next_time_it_is_taken() {
333        let lock = Lock::new(Vec::new());
334        lock.lock().push(1u8);
335        lock.lock().push(2);
336        assert_eq!(*lock.lock(), vec![1, 2]);
337        assert_eq!(lock.into_inner(), vec![1, 2]);
338    }
339
340    #[test]
341    fn a_held_lock_cannot_be_taken_and_a_dropped_one_can() {
342        let lock = Lock::new(0u32);
343        let held = lock.lock();
344        assert!(lock.is_held());
345        assert!(lock.try_lock().is_none());
346        drop(held);
347        assert!(!lock.is_held());
348        assert!(lock.try_lock().is_some());
349    }
350
351    #[test]
352    fn an_owner_with_an_exclusive_reference_pays_nothing() {
353        let mut lock = Lock::new(0u32);
354        *lock.get_mut() = 7;
355        assert_eq!(*lock.lock(), 7);
356    }
357
358    /// The one thing a lock is for. Without it the count comes out short,
359    /// because a read and a write from two threads lose one of the writes.
360    #[test]
361    fn every_increment_from_every_thread_lands() {
362        let lock = Lock::new(0u64);
363        std::thread::scope(|s| {
364            for _ in 0..HANDS {
365                s.spawn(|| {
366                    for _ in 0..ROUNDS {
367                        *lock.lock() += 1;
368                    }
369                });
370            }
371        });
372        assert_eq!(lock.into_inner(), HANDS * ROUNDS);
373    }
374
375    /// Contended on purpose: the work inside the lock is long enough that the
376    /// waiters get past the spin and into the yield, which is the path the
377    /// short test above never reaches.
378    #[test]
379    fn a_waiter_that_runs_out_of_spins_still_gets_the_lock() {
380        let lock = Lock::new(0u64);
381        std::thread::scope(|s| {
382            for _ in 0..HANDS {
383                s.spawn(|| {
384                    for _ in 0..HOLDS {
385                        let mut held = lock.lock();
386                        for _ in 0..INSIDE {
387                            *held += 1;
388                            hint::spin_loop();
389                        }
390                    }
391                });
392            }
393        });
394        assert_eq!(lock.into_inner(), HANDS * HOLDS * INSIDE);
395    }
396
397    #[cfg(debug_assertions)]
398    #[test]
399    #[should_panic(expected = "already holds this lock")]
400    fn taking_it_twice_on_one_thread_says_so_instead_of_hanging() {
401        let lock = Lock::new(0u32);
402        let _first = lock.lock();
403        let _second = lock.lock();
404    }
405
406    #[cfg(debug_assertions)]
407    #[test]
408    fn trying_it_twice_on_one_thread_just_fails() {
409        let lock = Lock::new(0u32);
410        let _first = lock.lock();
411        assert!(lock.try_lock().is_none());
412    }
413}
414
415/// The lock, model checked.
416///
417/// The tests above run one interleaving each, whichever one the machine
418/// happened to pick that time. These run all of them: loom takes the two
419/// threads apart at every atomic operation and tries every order they could
420/// have gone in, which is the only way to be sure that an ordering is right
421/// rather than merely never seen to be wrong on the machines it was run on.
422///
423/// Built and run separately, since the whole crate has to be compiled against
424/// loom's atomics for it to see anything: `RUSTFLAGS="--cfg loom" cargo test -p
425/// yo-common --release --lib lock::loom`.
426#[cfg(all(loom, test))]
427mod loom_tests {
428    use super::*;
429
430    /// What the lock is guarding, in a form loom watches.
431    ///
432    /// A plain counter would not do. Loom only sees the reads and writes it is
433    /// told about, and the value inside a lock is reached through a pointer
434    /// that leaves the closure, so an ordinary field would be invisible to it
435    /// and every one of these tests would pass whatever the orderings said.
436    /// This is loom's own cell, so every access through it is recorded and an
437    /// access that raced with another is a failed model rather than a guess.
438    type Guarded = loom::cell::UnsafeCell<usize>;
439
440    fn bump(lock: &Lock<Guarded>) {
441        let held = lock.lock();
442        // SAFETY: the lock is held, so this is the only pointer to the value.
443        held.with_mut(|p| unsafe { *p += 1 });
444    }
445
446    fn read(lock: &Lock<Guarded>) -> usize {
447        let held = lock.lock();
448        // SAFETY: as above, and this one only reads.
449        held.with(|p| unsafe { *p })
450    }
451
452    /// One counter, two threads, one increment each. An increment is a read
453    /// and a write, so any interleaving where both threads are inside at once
454    /// either loses one of them or is a race loom reports outright.
455    #[test]
456    fn two_threads_cannot_both_be_inside() {
457        loom::model(|| {
458            let lock = loom::sync::Arc::new(Lock::new(Guarded::new(0)));
459            let other = lock.clone();
460            let hand = loom::thread::spawn(move || bump(&other));
461            bump(&lock);
462            hand.join().unwrap();
463            assert_eq!(read(&lock), 2, "an increment was lost");
464        });
465    }
466
467    /// What one thread wrote under the lock is what the next thread reads
468    /// under it. This is what the release on the way out and the acquire on
469    /// the way in are for, and without either of them loom finds an order
470    /// where the second thread is looking at the value while the first is
471    /// still writing it.
472    #[test]
473    fn what_the_last_holder_wrote_is_what_the_next_one_sees() {
474        loom::model(|| {
475            let lock = loom::sync::Arc::new(Lock::new(Guarded::new(0)));
476            let other = lock.clone();
477            let hand = loom::thread::spawn(move || bump(&other));
478            let seen = read(&lock);
479            assert!(seen == 0 || seen == 1, "read a value nobody wrote");
480            hand.join().unwrap();
481            assert_eq!(read(&lock), 1);
482        });
483    }
484
485    /// A take that gives up rather than waits leaves the lock as it found it,
486    /// so the thread that does hold it is not disturbed and the next take
487    /// still works.
488    #[test]
489    fn a_take_that_fails_changes_nothing() {
490        loom::model(|| {
491            let lock = loom::sync::Arc::new(Lock::new(Guarded::new(0)));
492            let other = lock.clone();
493            let hand = loom::thread::spawn(move || {
494                if let Some(held) = other.try_lock() {
495                    // SAFETY: the take succeeded, so the lock is held here.
496                    held.with_mut(|p| unsafe { *p += 1 });
497                }
498            });
499            bump(&lock);
500            hand.join().unwrap();
501            let count = read(&lock);
502            assert!(count == 1 || count == 2, "count is {count}");
503        });
504    }
505}