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}