fuel_core_services/
seqlock.rs

1//! A simple implementation of a sequential lock.
2//! More details: <https://docs.kernel.org/locking/seqlock.html>
3
4use std::{
5    cell::UnsafeCell,
6    panic::UnwindSafe,
7    sync::atomic::{
8        fence,
9        AtomicU64,
10        Ordering,
11    },
12};
13
14/// A simple implementation of a sequential lock.
15/// some usage of unsafe, T must be Copy
16#[derive(Debug)]
17pub struct SeqLock<T: Copy> {
18    sequence: AtomicU64,
19    data: UnsafeCell<T>,
20}
21
22unsafe impl<T: Send + Copy> Sync for SeqLock<T> {}
23
24/// The writer handle for the `SeqLock`.
25/// Only one writer exists for a `SeqLock`.
26/// There is no Clone bound since we want to enforce only one writer.
27#[derive(Debug)]
28pub struct SeqLockWriter<T: Copy> {
29    lock: std::sync::Arc<SeqLock<T>>,
30}
31
32impl<T: Copy> SeqLockWriter<T> {
33    /// Modifies the data within the lock.
34    pub fn write<F>(&self, f: F)
35    where
36        F: FnOnce(&mut T) + UnwindSafe,
37    {
38        let lock = &self.lock;
39
40        // Indicate that a write operation is starting.
41        lock.sequence.fetch_add(1, Ordering::AcqRel);
42        // reordering safety
43        fence(Ordering::Acquire);
44
45        // attempt to perform the write, and catch any panics
46        // we won't have partial write problems since data <= 64 bytes
47        // safety: panics are caught and resumed
48        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
49            let data = &mut *lock.data.get();
50            f(data);
51        }));
52
53        // reordering safety
54        fence(Ordering::Release);
55        // Indicate that the write operation has finished.
56        lock.sequence.fetch_add(1, Ordering::Release);
57
58        // resume unwinding if there was an error
59        if let Err(e) = result {
60            std::panic::resume_unwind(e);
61        }
62    }
63}
64
65/// The reader handle for the `SeqLock`.
66/// Multiple readers can be created for a `SeqLock`.
67#[derive(Clone, Debug)]
68pub struct SeqLockReader<T: Copy> {
69    lock: std::sync::Arc<SeqLock<T>>,
70}
71
72impl<T: Copy> SeqLockReader<T> {
73    /// Reads the data within the lock.
74    pub fn read(&self) -> T {
75        let lock = &self.lock;
76
77        loop {
78            // check starting guard
79            let start = lock.sequence.load(Ordering::Acquire);
80
81            // if odd, write in progress
82            if start % 2 != 0 {
83                std::thread::yield_now();
84                continue;
85            }
86
87            // reordering safety
88            fence(Ordering::Acquire);
89
90            // safety: when the data <=64 bytes, it fits in a single cache line
91            // and cannot be subject to torn reads
92            let data = unsafe { *lock.data.get() };
93
94            // reordering safety
95            fence(Ordering::Acquire);
96
97            // check starting/ending guard
98            let end = lock.sequence.load(Ordering::Acquire);
99
100            // if value changed, retry
101            if start == end && start % 2 == 0 {
102                return data;
103            }
104        }
105    }
106}
107
108impl<T: Copy> SeqLock<T> {
109    /// Creates a new `SeqLock` and returns a writer and a reader handle.
110    /// Optimized for occasional writes and frequent reads
111    ///  !!WARNING!!
112    /// ONLY USE IF ALL THE BELOW CRITERIA ARE MET
113    ///  1. Internal data <= 64 bytes
114    ///  2. VERY frequent reads
115    /// # Safety
116    /// The data must be `Copy`
117    #[allow(clippy::new_ret_no_self)]
118    pub unsafe fn new(data: T) -> (SeqLockWriter<T>, SeqLockReader<T>) {
119        let lock = Self {
120            sequence: AtomicU64::new(0),
121            data: UnsafeCell::new(data),
122        };
123        let shared = std::sync::Arc::new(lock);
124        (
125            SeqLockWriter {
126                lock: std::sync::Arc::clone(&shared),
127            },
128            SeqLockReader { lock: shared },
129        )
130    }
131}
132
133#[allow(non_snake_case)]
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use std::thread;
138
139    #[test]
140    fn test_seqlock__provides_correct_values_in_order() {
141        let (writer, reader) = unsafe { SeqLock::new(42) };
142        let iterations = 100;
143
144        let writer = {
145            thread::spawn(move || {
146                for i in 0..iterations {
147                    writer.write(|data| *data = i);
148                }
149            })
150        };
151
152        let reader = {
153            let lock = reader.clone();
154            thread::spawn(move || {
155                let seen = 0;
156
157                for _ in 0..iterations {
158                    let value = lock.read();
159                    assert!(value >= seen);
160                }
161            })
162        };
163
164        writer.join().unwrap();
165        reader.join().unwrap();
166    }
167
168    #[test]
169    fn test_seqlock__single_threaded() {
170        let (writer, reader) = unsafe { SeqLock::new(42) };
171
172        writer.write(|data| {
173            *data = 100;
174        });
175
176        let value = reader.read();
177        assert_eq!(value, 100);
178    }
179}