use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use crate::hazard;
pub(crate) struct SnapCell<T> {
current: AtomicPtr<T>,
retired_len: AtomicUsize,
writer: Mutex<Vec<Arc<T>>>,
}
unsafe impl<T: Send + Sync> Send for SnapCell<T> {}
unsafe impl<T: Send + Sync> Sync for SnapCell<T> {}
impl<T> SnapCell<T> {
pub(crate) fn new(value: T) -> Self {
Self {
current: AtomicPtr::new(Arc::into_raw(Arc::new(value)).cast_mut()),
retired_len: AtomicUsize::new(0),
writer: Mutex::new(Vec::new()),
}
}
#[inline]
pub(crate) fn read<R>(&self, f: impl FnOnce(&T) -> R) -> R {
let Some((pin, p)) = self.protect() else {
return self.read_fallback(f);
};
let out = f(unsafe { &*p });
self.release(pin);
out
}
#[inline]
fn protect(&self) -> Option<(hazard::Pin, *mut T)> {
let mut p = self.current.load(Ordering::Acquire);
let pin = hazard::pin(p.cast_const().cast())?;
loop {
let q = self.current.load(Ordering::SeqCst);
if q == p {
return Some((pin, p));
}
p = q;
pin.protect(p.cast_const().cast());
}
}
#[inline]
fn release(&self, pin: hazard::Pin) {
drop(pin);
if self.retired_len.load(Ordering::Relaxed) != 0 {
self.try_reclaim();
}
}
#[cold]
#[inline(never)]
fn read_fallback<R>(&self, f: impl FnOnce(&T) -> R) -> R {
let value = self.load_arc_locked();
f(&value)
}
#[cold]
fn load_arc_locked(&self) -> Arc<T> {
let _writer = self.lock();
let raw = self.current.load(Ordering::Acquire);
unsafe {
Arc::increment_strong_count(raw);
Arc::from_raw(raw)
}
}
pub(crate) fn update<R>(&self, f: impl FnOnce(&T) -> Option<(T, R)>) -> Option<R> {
let mut writer = self.lock();
let raw = self.current.load(Ordering::Acquire);
let (new, result) = f(unsafe { &*raw })?;
let new = Arc::into_raw(Arc::new(new)).cast_mut();
self.current.store(new, Ordering::SeqCst);
writer.push(unsafe { Arc::from_raw(raw) });
let freed = Self::collect_unprotected(&mut writer);
self.retired_len.store(writer.len(), Ordering::Relaxed);
drop(writer);
drop(freed);
Some(result)
}
#[cfg(test)]
pub(crate) fn replace(&self, value: T) {
let _unit = self.update(|_| Some((value, ())));
}
#[cold]
fn try_reclaim(&self) {
let Ok(mut writer) = self.writer.try_lock() else {
return;
};
let freed = Self::collect_unprotected(&mut writer);
self.retired_len.store(writer.len(), Ordering::Relaxed);
drop(writer);
drop(freed);
}
fn collect_unprotected(retired: &mut Vec<Arc<T>>) -> Vec<Arc<T>> {
if retired.is_empty() {
return Vec::new();
}
let announced = hazard::announced();
let mut freed = Vec::new();
let mut i = 0;
while i < retired.len() {
let p: *const () = Arc::as_ptr(&retired[i]).cast();
if announced.contains(&p) {
i += 1;
} else {
freed.push(retired.swap_remove(i));
}
}
freed
}
fn lock(&self) -> MutexGuard<'_, Vec<Arc<T>>> {
self.writer.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl<T> Drop for SnapCell<T> {
fn drop(&mut self) {
let raw = *self.current.get_mut();
drop(unsafe { Arc::from_raw(raw) });
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::sync::atomic::AtomicBool;
struct DropFlag(Arc<AtomicBool>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
#[test]
fn read_sees_latest_value() {
let cell = SnapCell::new(1u32);
assert_eq!(cell.read(|v| *v), 1);
cell.replace(2);
assert_eq!(cell.read(|v| *v), 2);
}
#[test]
fn update_none_leaves_value_and_allocates_nothing() {
let cell = SnapCell::new(5u32);
let before = cell.current.load(Ordering::SeqCst);
assert_eq!(cell.update(|_| None::<(u32, ())>), None);
assert_eq!(before, cell.current.load(Ordering::SeqCst));
}
#[test]
fn replaced_value_is_freed_immediately_when_unread() {
let dropped = Arc::new(AtomicBool::new(false));
let cell = SnapCell::new(Some(DropFlag(Arc::clone(&dropped))));
cell.replace(None);
assert!(dropped.load(Ordering::SeqCst));
assert_eq!(cell.retired_len.load(Ordering::SeqCst), 0);
}
#[test]
fn value_read_during_replace_survives_until_reader_finishes() {
let dropped = Arc::new(AtomicBool::new(false));
let cell = SnapCell::new(Some(DropFlag(Arc::clone(&dropped))));
cell.read(|v| {
assert!(v.is_some());
cell.replace(None);
assert!(!dropped.load(Ordering::SeqCst));
assert!(v.is_some());
});
assert!(dropped.load(Ordering::SeqCst));
}
#[test]
fn nested_reads_beyond_slot_count_use_fallback() {
fn nest(cell: &SnapCell<u32>, depth: usize) -> u32 {
if depth == 0 {
return cell.read(|v| *v);
}
cell.read(|v| *v + nest(cell, depth - 1))
}
let cell = SnapCell::new(1u32);
let expected = u32::try_from(hazard::SLOTS * 2 + 1).unwrap();
assert_eq!(nest(&cell, hazard::SLOTS * 2), expected);
}
#[test]
fn concurrent_readers_and_writers_never_observe_freed_values() {
const READERS: usize = 4;
let cell = Arc::new(SnapCell::new(vec![0u64; 16]));
let stop = Arc::new(AtomicBool::new(false));
let started = Arc::new(AtomicUsize::new(0));
let overlapped = Arc::new(AtomicUsize::new(0));
let readers: Vec<_> = (0..READERS)
.map(|_| {
let (cell, stop) = (Arc::clone(&cell), Arc::clone(&stop));
let (started, overlapped) = (Arc::clone(&started), Arc::clone(&overlapped));
std::thread::spawn(move || {
let mut reads = 0u64;
let mut saw_write = false;
while !stop.load(Ordering::SeqCst) {
let first = cell.read(|v| {
let first = v[0];
assert!(v.iter().all(|x| *x == first), "torn snapshot");
first
});
if reads == 0 {
let _ = started.fetch_add(1, Ordering::SeqCst);
}
if first != 0 && !saw_write {
saw_write = true;
let _ = overlapped.fetch_add(1, Ordering::SeqCst);
}
reads += 1;
}
reads
})
})
.collect();
while started.load(Ordering::SeqCst) < READERS {
std::thread::yield_now();
}
let min_writes = if cfg!(miri) { 40 } else { 2000u64 };
let mut i = 0u64;
while i < min_writes || overlapped.load(Ordering::SeqCst) < READERS {
i += 1;
cell.replace(vec![i; 16]);
}
stop.store(true, Ordering::SeqCst);
for r in readers {
assert!(r.join().unwrap() > 0);
}
cell.read(|v| assert_eq!(v[0], i));
}
}