use core::sync::atomic::{AtomicU32, Ordering, fence};
pub(crate) struct SeqU64s<const COUNT: usize> {
sequence: AtomicU32,
halves: [[AtomicU32; 2]; COUNT],
}
impl<const COUNT: usize> SeqU64s<COUNT> {
pub(crate) fn new(values: [u64; COUNT]) -> Self {
Self {
sequence: AtomicU32::new(0),
halves: core::array::from_fn(|index| {
let [high, low] = split(values[index]);
[AtomicU32::new(high), AtomicU32::new(low)]
}),
}
}
pub(crate) fn store(&self, values: [u64; COUNT]) {
let sequence = self.sequence.load(Ordering::Relaxed);
self.sequence
.store(sequence.wrapping_add(1), Ordering::Relaxed);
fence(Ordering::Release);
for (half, value) in self.halves.iter().zip(values) {
let [high, low] = split(value);
half[0].store(high, Ordering::Relaxed);
half[1].store(low, Ordering::Relaxed);
}
self.sequence
.store(sequence.wrapping_add(2), Ordering::Release);
}
pub(crate) fn load(&self) -> [u64; COUNT] {
loop {
let before = self.sequence.load(Ordering::Acquire);
if before & 1 != 0 {
core::hint::spin_loop();
continue;
}
let values = core::array::from_fn(|index| {
let half = &self.halves[index];
join(
half[0].load(Ordering::Relaxed),
half[1].load(Ordering::Relaxed),
)
});
fence(Ordering::Acquire);
if before == self.sequence.load(Ordering::Relaxed) {
return values;
}
core::hint::spin_loop();
}
}
}
#[allow(clippy::cast_possible_truncation)]
const fn split(value: u64) -> [u32; 2] {
[(value >> 32) as u32, value as u32]
}
const fn join(high: u32, low: u32) -> u64 {
(high as u64) << 32 | low as u64
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::SeqU64s;
#[test]
fn round_trips_a_value_spanning_both_halves() {
let cell = SeqU64s::new([0]);
cell.store([0xFFFF_FFFF_0000_0001]);
assert_eq!(cell.load(), [0xFFFF_FFFF_0000_0001]);
}
#[test]
fn round_trips_each_value_of_a_multi_value_payload_independently() {
let cell = SeqU64s::new([0, 0]);
cell.store([24_000_000, 1_753_500_000_000_000_000]);
assert_eq!(cell.load(), [24_000_000, 1_753_500_000_000_000_000]);
}
#[cfg(feature = "std")]
mod properties {
use super::SeqU64s;
use proptest::prelude::{ProptestConfig, any, prop_assert_eq, proptest};
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
#[test]
fn every_value_survives_the_32_bit_halving(
first in any::<u64>(),
second in any::<u64>(),
) {
let cell = SeqU64s::new([0, 0]);
cell.store([first, second]);
prop_assert_eq!(cell.load(), [first, second]);
}
#[test]
fn construction_and_store_agree(value in any::<u64>()) {
let constructed = SeqU64s::new([value]);
let stored = SeqU64s::new([0]);
stored.store([value]);
prop_assert_eq!(constructed.load(), stored.load());
}
}
}
#[cfg(feature = "std")]
mod concurrent {
use super::SeqU64s;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
#[test]
fn never_observes_a_torn_update() {
let cell = Arc::new(SeqU64s::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");
}
}
}