#![cfg(loom)]
use loom::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use loom::sync::Arc;
use loom::thread;
const VACANT: usize = 0;
const PUB0: usize = 11;
const PUB1: usize = 22;
struct Slot {
generation: AtomicU64,
value: AtomicUsize,
}
impl Slot {
fn new() -> Self {
Self {
generation: AtomicU64::new(0),
value: AtomicUsize::new(VACANT),
}
}
fn install(&self, value: usize) {
self.value.store(value, Ordering::Release);
}
fn try_evict_at(&self, expected_gen: u64) -> Option<usize> {
let next = expected_gen + 1;
let cas = self.generation.compare_exchange(
expected_gen,
next,
Ordering::AcqRel,
Ordering::Acquire,
);
if cas.is_err() {
return None;
}
Some(self.value.swap(VACANT, Ordering::AcqRel))
}
fn read_with(&self, expected_gen: u64) -> Option<usize> {
let g1 = self.generation.load(Ordering::Acquire);
if g1 != expected_gen {
return None;
}
let v = self.value.load(Ordering::Acquire);
let g2 = self.generation.load(Ordering::Acquire);
if g2 != g1 {
return None;
}
if v == VACANT {
return None;
}
Some(v)
}
}
#[test]
fn remote_remove_protocol_never_loses_a_live_value() {
let mut builder = loom::model::Builder::new();
builder.preemption_bound = Some(3);
builder.check(|| {
let slot = Arc::new(Slot::new());
let evicts_at_gen0 = Arc::new(AtomicUsize::new(0));
let lost_live_value = Arc::new(AtomicUsize::new(0));
let reader_saw_pub1_at_gen0 = Arc::new(AtomicUsize::new(0));
slot.install(PUB0);
let r_slot = Arc::clone(&slot);
let r_evicts = Arc::clone(&evicts_at_gen0);
let r_lost = Arc::clone(&lost_live_value);
let remover = thread::spawn(move || {
if let Some(swapped) = r_slot.try_evict_at(0) {
r_evicts.fetch_add(1, Ordering::Relaxed);
if swapped == PUB1 {
r_lost.store(1, Ordering::Release);
}
}
let second = r_slot.try_evict_at(0);
let _ = second;
});
let rd_slot = Arc::clone(&slot);
let rd_saw = Arc::clone(&reader_saw_pub1_at_gen0);
let reader = thread::spawn(move || {
if let Some(v) = rd_slot.read_with(0) {
if v == PUB1 {
rd_saw.store(1, Ordering::Release);
}
}
});
let owner_won_gen0 = slot.try_evict_at(0).is_some();
if owner_won_gen0 {
evicts_at_gen0.fetch_add(1, Ordering::Relaxed);
slot.install(PUB1);
}
remover.join().expect("remover panicked");
reader.join().expect("reader panicked");
assert!(
evicts_at_gen0.load(Ordering::Acquire) <= 1,
"double-free: more than one evict won the gen-0 CAS"
);
assert_eq!(
lost_live_value.load(Ordering::Acquire),
0,
"lost-live-value: a stale gen-0 remove destroyed PUB1 (the gen-1 publication)"
);
assert_eq!(
reader_saw_pub1_at_gen0.load(Ordering::Acquire),
0,
"torn read: a gen-0 reader observed PUB1 (the gen-1 publication)"
);
});
}
#[test]
fn stale_remove_after_owner_evict_is_a_noop() {
let mut builder = loom::model::Builder::new();
builder.preemption_bound = Some(3);
builder.check(|| {
let slot = Arc::new(Slot::new());
slot.install(PUB0);
let r_slot = Arc::clone(&slot);
let remover = thread::spawn(move || {
r_slot.try_evict_at(0)
});
let owner_won = slot.try_evict_at(0).is_some();
let remover_outcome = remover.join().expect("remover panicked");
if owner_won {
assert_eq!(
remover_outcome, None,
"a gen-0 remove after the owner evicted gen 0 must be a no-op (CAS stale)"
);
} else {
assert_eq!(
remover_outcome,
Some(PUB0),
"if the remover won the gen-0 CAS, it must have swapped PUB0 (the gen-0 value)"
);
}
});
}