use std::cell::UnsafeCell;
use std::ptr::{read_volatile, write_volatile};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering, fence};
use crate::events::TransportInfo;
pub struct TransportSlot {
seq: AtomicU64,
data: UnsafeCell<TransportInfo>,
}
unsafe impl Sync for TransportSlot {}
unsafe impl Send for TransportSlot {}
impl TransportSlot {
#[must_use]
pub fn new() -> Arc<Self> {
Arc::new(Self {
seq: AtomicU64::new(0),
data: UnsafeCell::new(TransportInfo::default()),
})
}
pub fn write(&self, info: &TransportInfo) {
let s = self.seq.load(Ordering::Relaxed);
self.seq.store(s.wrapping_add(1), Ordering::Relaxed);
fence(Ordering::Release);
unsafe {
write_volatile(self.data.get(), *info);
}
self.seq.store(s.wrapping_add(2), Ordering::Release);
}
pub fn read(&self) -> Option<TransportInfo> {
for _ in 0..8 {
let s1 = self.seq.load(Ordering::Acquire);
if s1 == 0 {
return None;
}
if s1 & 1 == 1 {
std::hint::spin_loop();
continue;
}
let snapshot = unsafe { read_volatile(self.data.get()) };
fence(Ordering::Acquire);
let s2 = self.seq.load(Ordering::Acquire);
if s1 == s2 {
return Some(snapshot);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::TransportSlot;
use crate::events::TransportInfo;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[allow(clippy::float_cmp, clippy::cast_precision_loss)]
#[cfg_attr(
miri,
ignore = "concurrency soak - too slow under Miri, no weak-memory repro"
)]
#[test]
fn concurrent_reads_never_observe_a_torn_write() {
let slot = TransportSlot::new();
let stop = Arc::new(AtomicBool::new(false));
let writer = {
let slot = Arc::clone(&slot);
let stop = Arc::clone(&stop);
std::thread::spawn(move || {
for n in 1i64..2_000_000 {
let f = n as f64;
slot.write(&TransportInfo {
playing: n % 2 == 0,
recording: n % 2 == 1,
tempo: f,
time_sig_num: 0,
time_sig_den: 0,
position_samples: n,
position_seconds: f,
position_beats: f,
bar_start_beats: f,
loop_active: n % 2 == 0,
loop_start_beats: f,
loop_end_beats: f,
});
}
stop.store(true, Ordering::Relaxed);
})
};
let readers: Vec<_> = (0..3)
.map(|_| {
let slot = Arc::clone(&slot);
let stop = Arc::clone(&stop);
std::thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
if let Some(info) = slot.read() {
let n = info.position_samples;
let f = n as f64;
assert_eq!(info.tempo, f, "torn read: tempo");
assert_eq!(info.position_seconds, f, "torn read: seconds");
assert_eq!(info.position_beats, f, "torn read: beats");
assert_eq!(info.bar_start_beats, f, "torn read: bar");
assert_eq!(info.loop_start_beats, f, "torn read: loop start");
assert_eq!(info.loop_end_beats, f, "torn read: loop end");
assert_eq!(info.playing, n % 2 == 0, "torn read: playing");
assert_eq!(info.recording, n % 2 == 1, "torn read: recording");
}
}
})
})
.collect();
writer.join().unwrap();
for r in readers {
r.join().unwrap();
}
}
}