use core::sync::atomic::{AtomicU32, Ordering};
struct SeqWords<const N: usize> {
sequence: AtomicU32,
words: [AtomicU32; N],
}
impl<const N: usize> SeqWords<N> {
fn new(words: [u32; N]) -> Self {
Self {
sequence: AtomicU32::new(0),
words: core::array::from_fn(|index| AtomicU32::new(words[index])),
}
}
fn store(&self, new_words: [u32; N]) {
let sequence = self.sequence.load(Ordering::Relaxed);
self.sequence.store(sequence.wrapping_add(1), Ordering::Release);
for (word, new_word) in self.words.iter().zip(new_words) {
word.store(new_word, Ordering::Relaxed);
}
self.sequence.store(sequence.wrapping_add(2), Ordering::Release);
}
fn load(&self) -> [u32; N] {
loop {
let before = self.sequence.load(Ordering::Acquire);
if before & 1 != 0 {
core::hint::spin_loop();
continue;
}
let mut words = [0u32; N];
for (slot, word) in words.iter_mut().zip(&self.words) {
*slot = word.load(Ordering::Relaxed);
}
let after = self.sequence.load(Ordering::Acquire);
if before == after {
return words;
}
core::hint::spin_loop();
}
}
}
#[allow(clippy::cast_possible_truncation)]
const fn split_u64(value: u64) -> [u32; 2] {
let hi = (value >> 32) as u32;
let lo = value as u32;
[hi, lo]
}
const fn join_u64(words: [u32; 2]) -> u64 {
(words[0] as u64) << 32 | words[1] as u64
}
pub(crate) struct SeqU64 {
inner: SeqWords<2>,
}
impl SeqU64 {
pub(crate) fn new(value: u64) -> Self {
Self {
inner: SeqWords::new(split_u64(value)),
}
}
pub(crate) fn store(&self, value: u64) {
self.inner.store(split_u64(value));
}
pub(crate) fn load(&self) -> u64 {
join_u64(self.inner.load())
}
}
pub(crate) struct SeqU64Pair {
inner: SeqWords<4>,
}
impl SeqU64Pair {
pub(crate) fn new(first: u64, second: u64) -> Self {
let [first_hi, first_lo] = split_u64(first);
let [second_hi, second_lo] = split_u64(second);
Self {
inner: SeqWords::new([first_hi, first_lo, second_hi, second_lo]),
}
}
pub(crate) fn store(&self, first: u64, second: u64) {
let [first_hi, first_lo] = split_u64(first);
let [second_hi, second_lo] = split_u64(second);
self.inner
.store([first_hi, first_lo, second_hi, second_lo]);
}
pub(crate) fn load(&self) -> (u64, u64) {
let [first_hi, first_lo, second_hi, second_lo] = self.inner.load();
(
join_u64([first_hi, first_lo]),
join_u64([second_hi, second_lo]),
)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::{SeqU64, SeqU64Pair};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
#[test]
fn seq_u64_round_trips_values_spanning_both_halves() {
let cell = SeqU64::new(0);
cell.store(0xFFFF_FFFF_0000_0001);
assert_eq!(cell.load(), 0xFFFF_FFFF_0000_0001);
}
#[test]
fn seq_u64_pair_never_observes_a_torn_update() {
let cell = Arc::new(SeqU64Pair::new(0, 0));
let stop = Arc::new(AtomicBool::new(false));
let writer_cell = Arc::clone(&cell);
let writer_stop = Arc::clone(&stop);
let writer = thread::spawn(move || {
for generation in 1..200_000u64 {
if writer_stop.load(Ordering::Relaxed) {
break;
}
writer_cell.store(generation, generation.wrapping_mul(3));
}
});
let reader_cell = Arc::clone(&cell);
for _ in 0..500_000 {
let (first, second) = reader_cell.load();
assert_eq!(
second,
first.wrapping_mul(3),
"reader observed a torn (first, second) pair: {first}, {second}"
);
}
stop.store(true, Ordering::Relaxed);
writer.join().expect("writer thread panicked");
}
}