Skip to main content

concurrent/
seqlock.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use core::marker::PhantomPinned;
6use core::sync::atomic::{AtomicU32, Ordering, fence};
7
8use crate::common::{SYNC_OPT_ACQ_REL_OPS, SYNC_OPT_FENCE, SyncOpt};
9
10#[cfg(feature = "kernel")]
11unsafe extern "C" {
12    fn cpp_arch_yield();
13}
14
15/// Corresponds to `Osal::ArchYield()` in the C++ implementation.
16#[inline(always)]
17fn arch_yield() {
18    #[cfg(feature = "kernel")]
19    // SAFETY: `cpp_arch_yield` has no preconditions; it only issues the architecture's
20    // yield/pause hint instruction.
21    unsafe {
22        cpp_arch_yield()
23    }
24}
25
26pub type SequenceNumber = u32;
27
28const SEQ_NUM_WRITE_IN_FLIGHT: SequenceNumber = 0x1;
29
30const fn write_in_flight(seq_num: SequenceNumber) -> bool {
31    (seq_num & SEQ_NUM_WRITE_IN_FLIGHT) != 0
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct ReadTransactionToken(SequenceNumber);
36
37impl ReadTransactionToken {
38    pub const fn new() -> Self {
39        Self(1)
40    }
41
42    pub const fn seq_num(&self) -> SequenceNumber {
43        self.0
44    }
45}
46
47impl Default for ReadTransactionToken {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53// Stands in for the C++ __TA_CAPABILITY / __TA_ACQUIRE / __TA_RELEASE
54// annotations; dropping the guard releases the lock.
55#[must_use = "the write cycle ends as soon as the guard is dropped"]
56#[derive(Debug)]
57pub struct WriteGuard<'a, const SYNC_OPT: u8 = SYNC_OPT_FENCE> {
58    lock: &'a SeqLock<SYNC_OPT>,
59}
60
61impl<const SYNC_OPT: u8> Drop for WriteGuard<'_, SYNC_OPT> {
62    #[inline]
63    fn drop(&mut self) {
64        self.lock.release();
65    }
66}
67
68#[repr(transparent)]
69pub struct SeqLock<const SYNC_OPT: u8 = SYNC_OPT_FENCE> {
70    seq_num: AtomicU32,
71    // No copy, no move
72    _pin: PhantomPinned,
73}
74
75impl<const SYNC_OPT: u8> SeqLock<SYNC_OPT> {
76    // Mirrors the C++ kSyncOpt.
77    pub const SYNC_OPT: SyncOpt = SyncOpt::from_u8(SYNC_OPT);
78
79    // Mirrors the C++ kCopyWrapperSyncOpt.
80    pub const COPY_WRAPPER_SYNC_OPT: SyncOpt =
81        if SYNC_OPT == SYNC_OPT_ACQ_REL_OPS { SyncOpt::AcqRelOps } else { SyncOpt::None };
82
83    const VALID_SYNC_OPT: () = assert!(
84        (SYNC_OPT == SYNC_OPT_ACQ_REL_OPS) || (SYNC_OPT == SYNC_OPT_FENCE),
85        "The synchronization options chosen for a SeqLock must be either Acquire/Release, or Fence"
86    );
87
88    pub const fn new() -> Self {
89        let () = Self::VALID_SYNC_OPT;
90        Self { seq_num: AtomicU32::new(0), _pin: PhantomPinned }
91    }
92
93    // Provide read access to the current seq_num state for testing.
94    #[cfg(test)]
95    #[inline]
96    #[must_use]
97    pub fn seq_num(&self) -> SequenceNumber {
98        self.seq_num_with_order(Ordering::Relaxed)
99    }
100
101    // Provide read access to the current seq_num state with a specified memory order for testing.
102    #[cfg(test)]
103    #[inline]
104    #[must_use]
105    pub fn seq_num_with_order(&self, order: Ordering) -> SequenceNumber {
106        self.seq_num.load(order)
107    }
108
109    // Read Transactions (eg; "locking" for read)
110
111    #[inline]
112    #[must_use]
113    pub fn begin_read_transaction(&self) -> ReadTransactionToken {
114        loop {
115            let seq_num = self.seq_num.load(Ordering::Acquire);
116            if !write_in_flight(seq_num) {
117                return ReadTransactionToken(seq_num);
118            }
119            arch_yield();
120        }
121    }
122
123    // The zero-timeout form of the C++ TryBeginReadTransaction.
124    #[inline]
125    #[must_use]
126    pub fn try_begin_read_transaction(&self) -> Option<ReadTransactionToken> {
127        let seq_num = self.seq_num.load(Ordering::Acquire);
128        if write_in_flight(seq_num) {
129            return None;
130        }
131        Some(ReadTransactionToken(seq_num))
132    }
133
134    #[inline]
135    #[must_use = "a read transaction which was not validated may have observed a torn payload"]
136    pub fn end_read_transaction(&self, token: ReadTransactionToken) -> bool {
137        if write_in_flight(token.0) {
138            return false;
139        }
140
141        // If we are using fence-to-fence synchronization, this is the place we
142        // need to put our acquire fence.
143        if Self::SYNC_OPT == SyncOpt::Fence {
144            fence(Ordering::Acquire);
145        }
146
147        self.seq_num.load(Ordering::Relaxed) == token.0
148    }
149
150    // The Rust form of the retry loop which C++ readers write out by hand.
151    #[inline]
152    pub fn read_transaction<T>(&self, mut read_payload: impl FnMut() -> T) -> T {
153        loop {
154            let token = self.begin_read_transaction();
155            let payload = read_payload();
156            if self.end_read_transaction(token) {
157                return payload;
158            }
159        }
160    }
161
162    // Exclusive locking.
163
164    #[inline]
165    pub fn acquire(&self) -> WriteGuard<'_, SYNC_OPT> {
166        loop {
167            // Wait until we observe an even sequence number.
168            let mut expected = self.seq_num.load(Ordering::Relaxed);
169            while write_in_flight(expected) {
170                arch_yield();
171                expected = self.seq_num.load(Ordering::Relaxed);
172            }
173
174            // Attempt to increment the even number we observed to be an odd number,
175            // with Acquire semantics on the RMW if we succeed.
176            if self
177                .seq_num
178                .compare_exchange(
179                    expected,
180                    expected.wrapping_add(1),
181                    Ordering::Acquire,
182                    Ordering::Relaxed,
183                )
184                .is_ok()
185            {
186                // If we are using fence-to-fence synchronization, this is the place we
187                // need to put our release fence.
188                if Self::SYNC_OPT == SyncOpt::Fence {
189                    fence(Ordering::Release);
190                }
191                return WriteGuard { lock: self };
192            }
193        }
194    }
195
196    // The zero-timeout form of the C++ TryAcquire.
197    #[inline]
198    #[must_use]
199    pub fn try_acquire(&self) -> Option<WriteGuard<'_, SYNC_OPT>> {
200        // Wait until we observe an even sequence number.
201        let expected = self.seq_num.load(Ordering::Relaxed);
202        if write_in_flight(expected) {
203            return None;
204        }
205
206        // Attempt to increment the even number we observed to be an odd number,
207        // with Acquire semantics on the RMW if we succeed.
208        if self
209            .seq_num
210            .compare_exchange(
211                expected,
212                expected.wrapping_add(1),
213                Ordering::Acquire,
214                Ordering::Relaxed,
215            )
216            .is_ok()
217        {
218            // If we are using fence-to-fence synchronization, this is the place we
219            // need to put our release fence.
220            if Self::SYNC_OPT == SyncOpt::Fence {
221                fence(Ordering::Release);
222            }
223            Some(WriteGuard { lock: self })
224        } else {
225            None
226        }
227    }
228
229    #[inline]
230    pub(crate) fn release(&self) {
231        let before = self.seq_num.fetch_add(1, Ordering::Release);
232        debug_assert!(
233            write_in_flight(before),
234            "SeqLock was not held when the write guard was dropped"
235        );
236    }
237}
238
239impl<const SYNC_OPT: u8> Default for SeqLock<SYNC_OPT> {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245impl<const SYNC_OPT: u8> core::fmt::Debug for SeqLock<SYNC_OPT> {
246    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
247        f.debug_struct("SeqLock")
248            .field("sync_opt", &Self::SYNC_OPT)
249            .field("seq_num", &self.seq_num.load(Ordering::Relaxed))
250            .finish()
251    }
252}
253
254// Matches the static_asserts in //zircon/system/ulib/concurrent/tests/seqlock.cc.
255zr::static_assert!(core::mem::size_of::<SeqLock>() == 4);
256zr::static_assert!(core::mem::align_of::<SeqLock>() == 4);
257zr::static_assert!(core::mem::size_of::<SeqLock<SYNC_OPT_ACQ_REL_OPS>>() == 4);
258zr::static_assert!(core::mem::align_of::<SeqLock<SYNC_OPT_ACQ_REL_OPS>>() == 4);
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    type AcqRelSeqLock = SeqLock<SYNC_OPT_ACQ_REL_OPS>;
265
266    #[test]
267    fn test_initial_state() {
268        let lock: SeqLock = SeqLock::new();
269        assert_eq!(lock.seq_num(), 0);
270        assert_eq!(SeqLock::<SYNC_OPT_FENCE>::default().seq_num(), 0);
271        assert_eq!(AcqRelSeqLock::new().seq_num(), 0);
272    }
273
274    #[test]
275    fn test_sync_opt() {
276        assert_eq!(SeqLock::<SYNC_OPT_FENCE>::SYNC_OPT, SyncOpt::Fence);
277        assert_eq!(SeqLock::<SYNC_OPT_FENCE>::COPY_WRAPPER_SYNC_OPT, SyncOpt::None);
278        assert_eq!(AcqRelSeqLock::SYNC_OPT, SyncOpt::AcqRelOps);
279        assert_eq!(AcqRelSeqLock::COPY_WRAPPER_SYNC_OPT, SyncOpt::AcqRelOps);
280    }
281
282    #[test]
283    fn test_uncontested_read() {
284        let lock: SeqLock = SeqLock::new();
285
286        // With no writer, read transactions should always succeed.
287        let token1 = lock.begin_read_transaction();
288        assert!(lock.end_read_transaction(token1));
289
290        // A second transaction with no write in-between should also succeed, and the
291        // reported sequence number should be unchanged.
292        let token2 = lock.begin_read_transaction();
293        assert!(lock.end_read_transaction(token2));
294        assert_eq!(token1.seq_num(), token2.seq_num());
295
296        // After a write cycle, further subsequent read transactions should also
297        // succeed, but with a different sequence number.
298        drop(lock.acquire());
299        let token3 = lock.begin_read_transaction();
300        assert!(lock.end_read_transaction(token3));
301        assert_ne!(token1.seq_num(), token3.seq_num());
302    }
303
304    #[test]
305    fn test_contested_read() {
306        let lock: SeqLock = SeqLock::new();
307
308        // Any write cycle which happens during a read should cause the read
309        // transaction to fail.
310        let token = lock.begin_read_transaction();
311
312        // Note that to keep life simple, and single threaded, we go through the
313        // write cycle on this thread.
314        let guard = lock.acquire();
315        assert_eq!(lock.seq_num(), 1);
316        assert!(!lock.end_read_transaction(token));
317
318        drop(guard);
319        assert_eq!(lock.seq_num(), 2);
320        assert!(!lock.end_read_transaction(token));
321    }
322
323    #[test]
324    fn test_read_non_blocking() {
325        let lock: SeqLock = SeqLock::new();
326
327        // Trying to begin a read transaction when there is no write-cycle in flight
328        // should always succeed, even with a timeout of zero.
329        let token = lock.try_begin_read_transaction().expect("no write cycle is in flight");
330        assert!(lock.end_read_transaction(token));
331
332        // Attempting to start a transaction while a write cycle is in progress should
333        // always fail.
334        let guard = lock.acquire();
335        assert!(lock.try_begin_read_transaction().is_none());
336
337        drop(guard);
338        let token = lock.try_begin_read_transaction().expect("no write cycle is in flight");
339        assert!(lock.end_read_transaction(token));
340    }
341
342    #[test]
343    fn test_invalid_token() {
344        let lock: SeqLock = SeqLock::new();
345        let token = ReadTransactionToken::new();
346        assert!(write_in_flight(token.seq_num()));
347        assert!(!lock.end_read_transaction(token));
348    }
349
350    #[test]
351    fn test_read_transaction_helper() {
352        use core::sync::atomic::AtomicU64;
353
354        let lock: SeqLock = SeqLock::new();
355        let payload = AtomicU64::new(0);
356
357        {
358            let _guard = lock.acquire();
359            payload.store(42, Ordering::Relaxed);
360        }
361
362        assert_eq!(lock.read_transaction(|| payload.load(Ordering::Relaxed)), 42);
363    }
364
365    #[test]
366    fn test_uncontested_write() {
367        let lock: SeqLock = SeqLock::new();
368
369        // This one seems pretty trivial.  As long as there is only one writer,
370        // acquire operations should always immediately succeed (including the
371        // non-blocking version).
372        const TRIALS: u32 = 1000;
373        for i in 0..TRIALS {
374            assert_eq!(lock.seq_num(), i * 4);
375
376            drop(lock.acquire());
377            assert_eq!(lock.seq_num(), (i * 4) + 2);
378
379            let guard = lock.try_acquire().expect("uncontested try_acquire must succeed");
380            assert_eq!(lock.seq_num(), (i * 4) + 3);
381            drop(guard);
382            assert_eq!(lock.seq_num(), (i * 4) + 4);
383        }
384    }
385
386    #[test]
387    fn test_try_acquire_is_contested() {
388        let lock: SeqLock = SeqLock::new();
389
390        let guard = lock.try_acquire().expect("uncontested try_acquire must succeed");
391        assert_eq!(lock.seq_num(), 1);
392
393        assert!(lock.try_acquire().is_none());
394        assert_eq!(lock.seq_num(), 1);
395
396        drop(guard);
397        assert_eq!(lock.seq_num(), 2);
398    }
399
400    #[test]
401    fn test_contested_write() {
402        use core::sync::atomic::AtomicU32;
403        use std::sync::Arc;
404        use std::thread;
405        use std::time::Duration;
406
407        // Simulate contention, then make sure that the non-blocking form of
408        // acquire fails.
409        //
410        // Make a best-effort attempt to validate a normal acquire.
411        //
412        // Note that this can never be a conclusive test.  In addition to never being
413        // able to absolutely guarantee that our test thread has actually started the
414        // acquire operation after signaling to us that it has (via the shared state),
415        // no matter how long we wait, we can never actually prove that it the test
416        // thread _wouldn't_ have eventually entered the exclusive portion of the lock
417        // had we simply waited a bit longer.
418        const NOT_STARTED: u32 = 0;
419        const ATTEMPTING_ACQUIRE: u32 = 1;
420        const ACQUIRE_SUCCEEDED: u32 = 2;
421
422        let lock = Arc::new(SeqLock::<SYNC_OPT_FENCE>::new());
423        let state = Arc::new(AtomicU32::new(NOT_STARTED));
424
425        let guard = lock.acquire();
426        assert!(lock.try_acquire().is_none());
427
428        let acquire_thread = {
429            let lock = lock.clone();
430            let state = state.clone();
431            thread::spawn(move || {
432                state.store(ATTEMPTING_ACQUIRE, Ordering::SeqCst);
433                let guard = lock.acquire();
434                state.store(ACQUIRE_SUCCEEDED, Ordering::SeqCst);
435                drop(guard);
436            })
437        };
438
439        // Wait forever for the thread start it's acquire attempt.
440        while state.load(Ordering::SeqCst) != ATTEMPTING_ACQUIRE {
441            // empty body.  Just spinning.
442            arch_yield();
443        }
444
445        // Wait just a bit, then verify that the test thread has still not acquired
446        // the lock.
447        thread::sleep(Duration::from_millis(500));
448        assert_eq!(state.load(Ordering::SeqCst), ATTEMPTING_ACQUIRE);
449
450        // Release the lock and verify that the test thread successfully acquires and
451        // release it.
452        drop(guard);
453        while state.load(Ordering::SeqCst) != ACQUIRE_SUCCEEDED {
454            // empty body.  Just spinning.
455            arch_yield();
456        }
457
458        // We should now be able to bounce through the lock without any significant
459        // delay. The test thread may still be in the process of releasing the lock,
460        // but it should eventually succeed.
461        drop(lock.acquire());
462
463        // The acquire_thread may not have exited yet, but it should do so in short
464        // order.
465        acquire_thread.join().unwrap();
466    }
467
468    #[test]
469    fn test_concurrent_readers_writers() {
470        use core::sync::atomic::AtomicU64;
471        use std::sync::Arc;
472        use std::thread;
473
474        // `b` is always the complement of `a`, so a torn read is detectable.
475        struct Payload {
476            lock: SeqLock,
477            a: AtomicU64,
478            b: AtomicU64,
479        }
480
481        const ITERATIONS: u64 = 5000;
482
483        let payload =
484            Arc::new(Payload { lock: SeqLock::new(), a: AtomicU64::new(0), b: AtomicU64::new(!0) });
485
486        let writer = {
487            let payload = payload.clone();
488            thread::spawn(move || {
489                for i in 1..=ITERATIONS {
490                    let _guard = payload.lock.acquire();
491                    payload.a.store(i, Ordering::Relaxed);
492                    payload.b.store(!i, Ordering::Relaxed);
493                }
494            })
495        };
496
497        let mut successful_transactions: u64 = 0;
498        let mut observed: u64 = 0;
499        while observed < ITERATIONS {
500            let token = payload.lock.begin_read_transaction();
501            let a = payload.a.load(Ordering::Relaxed);
502            let b = payload.b.load(Ordering::Relaxed);
503            if payload.lock.end_read_transaction(token) {
504                assert_eq!(a, !b, "observed a torn payload in a successful transaction");
505                successful_transactions += 1;
506                observed = observed.max(a);
507            }
508        }
509
510        let (a, b) = payload.lock.read_transaction(|| {
511            (payload.a.load(Ordering::Relaxed), payload.b.load(Ordering::Relaxed))
512        });
513        assert_eq!(a, !b);
514
515        writer.join().unwrap();
516        assert!(successful_transactions > 0, "no read transaction ever succeeded");
517    }
518}