Skip to main content

low_latency_data_structures/seqlock/
lock.rs

1use std::cell::UnsafeCell;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering, fence};
4
5use crate::seqlock::reader::Reader;
6use crate::seqlock::writer::Writer;
7
8/// Creates a new seqlock initialised with `initial_value`.
9///
10/// Returns a paired `(Writer, Reader)`. Additional readers can be obtained
11/// by cloning the [`Reader`].
12///
13/// # Examples
14///
15/// ```
16/// use low_latency_data_structures::seqlock::new;
17///
18/// let (writer, reader) = new(0u64);
19/// assert_eq!(reader.read(), 0);
20/// writer.write(42);
21/// assert_eq!(reader.read(), 42);
22/// ```
23pub fn new<T: bytemuck::AnyBitPattern>(initial_value: T) -> (Writer<T>, Reader<T>) {
24    let sl = Arc::new(SeqLock {
25        seq: AtomicU64::new(0),
26        data: UnsafeCell::new(initial_value),
27    });
28    let writer = Writer::new(sl.clone());
29    let reader = Reader::new(sl);
30    (writer, reader)
31}
32
33/// 128 = adjacent-line prefetcher granularity, not cache line size
34#[repr(C, align(128))]
35pub(super) struct SeqLock<T: bytemuck::AnyBitPattern> {
36    seq: AtomicU64,
37    data: UnsafeCell<T>,
38}
39
40// SAFETY: UnsafeCell is not Sync but access is synchronised through the
41// write()/read() methods using the seq-number protocol. Torn reads of `data`
42// would be undefined under the abstract C11/C++11 model, but `T:
43// AnyBitPattern` guarantees every bit pattern materialises a valid T, so the
44// worst-case observed value is stale, never UB. The seq check around the
45// read rejects stale or torn values before they leave `Reader::read`.
46unsafe impl<T: bytemuck::AnyBitPattern> Sync for SeqLock<T> {}
47
48impl<T: bytemuck::AnyBitPattern> SeqLock<T> {
49    #[inline]
50    pub(super) fn write(&self, value: T) {
51        let s = self.seq.load(Ordering::Relaxed);
52        self.seq.store(s.wrapping_add(1), Ordering::Relaxed);
53        // ARM: prevents the write from reordering above the s load
54        fence(Ordering::Release);
55        // SAFETY: see the unsafe impl Sync for SeqLock above. write_volatile
56        // also pins the store: it cannot be reordered with the surrounding
57        // seq stores by the compiler.
58        unsafe { self.data.get().write_volatile(value) };
59        self.seq.store(s.wrapping_add(2), Ordering::Release);
60    }
61
62    #[inline]
63    pub(super) fn read(&self) -> T {
64        loop {
65            let s1 = self.seq.load(Ordering::Acquire);
66            if s1 & 1 == 1 {
67                continue;
68            }
69            // SAFETY: see the unsafe impl Sync for SeqLock above. The torn
70            // read is filtered by the s1==s2 check below.
71            let read_attempt = unsafe { self.data.get().read_volatile() };
72            // ARM: prevents the read from reordering below the s2 load
73            fence(Ordering::Acquire);
74            let s2 = self.seq.load(Ordering::Relaxed);
75            if s1 == s2 {
76                return read_attempt;
77            }
78        }
79    }
80}
81
82#[cfg(feature = "tests_basic")]
83#[cfg(test)]
84mod tests {
85    use std::sync::Barrier;
86    use std::thread;
87
88    use super::*;
89
90    #[test]
91    fn first_read_returns_initial_value() {
92        for init_value in [42, 1, 23] {
93            let (_, reader) = new(init_value);
94            assert_eq!(reader.read(), init_value);
95        }
96    }
97
98    #[test]
99    fn single_thread_reads_what_it_has_written() {
100        let (writer, reader) = new(0);
101        for value in [42, 1, 23] {
102            writer.write(value);
103            assert_eq!(reader.read(), value);
104        }
105    }
106
107    #[test]
108    fn single_writer_multi_reader() {
109        const READERS: usize = 3;
110        const VALUES: [u64; 4] = [1, 4, 7, 9];
111        let (writer, reader) = new(0);
112        let reader = Arc::new(reader);
113        let barrier = Arc::new(Barrier::new(READERS + 1));
114        let writer_h = {
115            let barrier = barrier.clone();
116            thread::spawn(move || {
117                // wait for readers to read the initial value
118                barrier.wait();
119                for value in VALUES {
120                    writer.write(value);
121                    // waits for all readers to get the value
122                    barrier.wait();
123                }
124            })
125        };
126        let readers_hs: Vec<_> = (0..READERS)
127            .map(|_| {
128                let barrier = barrier.clone();
129                let reader = reader.clone();
130                thread::spawn(move || {
131                    let mut collected: Vec<u64> = Vec::with_capacity(VALUES.len() + 1);
132                    loop {
133                        if collected.len() == VALUES.len() + 1 {
134                            break;
135                        }
136                        let item = reader.read();
137                        if let Some(&last) = collected.last()
138                            && last == item
139                        {
140                            continue;
141                        } else {
142                            collected.push(item);
143                            barrier.wait();
144                        }
145                    }
146                    collected
147                })
148            })
149            .collect();
150        writer_h.join().unwrap();
151        let readers_results: Vec<Vec<u64>> =
152            readers_hs.into_iter().map(|h| h.join().unwrap()).collect();
153        assert!(
154            readers_results
155                .iter()
156                .all(|result| result.len() == VALUES.len() + 1 && &result[1..] == &VALUES[..])
157        );
158    }
159}
160
161#[cfg(test)]
162#[cfg(feature = "tests_dhat")]
163mod tests_dhat {
164    use std::hint::black_box;
165
166    use super::*;
167
168    #[test]
169    fn hot_path_without_allocations() {
170        let _profiler = dhat::Profiler::builder().testing().build();
171        let (writer, reader) = new(0u64);
172        const ITERS: u64 = 10_000_000;
173
174        // Warm up to absorb any one-time platform allocations: lazy symbol
175        // resolution in the dynamic linker, libstd TLS init, debug-build
176        // slow-path stubs that LLVM emits for overflow/panic messages. None
177        // of these scale with iteration count; the per-iteration regression
178        // we actually care about would dwarf the slack budget below.
179        for i in 0..10_000u64 {
180            writer.write(black_box(i));
181            black_box(reader.read());
182        }
183
184        let before = dhat::HeapStats::get();
185        for i in 0..ITERS {
186            writer.write(black_box(i));
187            black_box(reader.read());
188        }
189        let after = dhat::HeapStats::get();
190
191        let allocs = after.total_blocks - before.total_blocks;
192        assert!(
193            allocs < 64,
194            "hot path allocated {allocs} blocks over {ITERS} iterations; \
195             a real regression would be O(ITERS), this slack absorbs O(1) \
196             platform noise (lazy linker, debug stubs)"
197        );
198    }
199}