use std::{
hint,
sync::atomic::{fence, AtomicBool, AtomicI64, Ordering},
sync::Arc,
thread,
};
use crate::{barrier::Barrier, Sequence};
use crossbeam_utils::CachePadded;
use parking_lot::{Condvar, Mutex};
macro_rules! return_if_available_or_shutdown {
($requested:expr, $barrier:expr, $shutdown:expr) => {{
let requested = $requested;
let barrier = $barrier;
let shutdown = $shutdown;
let available = barrier.get_after(requested);
if available >= requested {
fence(Ordering::Acquire);
return WaitOutcome::Available { upper: available };
}
if shutdown.load(Ordering::Relaxed) == requested {
return WaitOutcome::Shutdown;
}
}};
}
pub enum WaitOutcome {
Available {
upper: Sequence,
},
Shutdown,
Timeout,
}
pub trait WakeupNotifier: Send + Sync {
fn wake(&self);
}
#[derive(Default, Clone)]
pub struct NoopWakeupNotifier;
impl WakeupNotifier for NoopWakeupNotifier {
#[inline]
fn wake(&self) {}
}
pub trait WaitStrategy: Send + Sync + Clone + 'static {
type Waiter: Waiter;
type Notifier: WakeupNotifier + Clone + Send + Sync + 'static;
fn new_waiter(&self) -> Self::Waiter;
fn notifier(&self) -> Self::Notifier;
}
pub trait Waiter: Send {
fn wait_for(
&mut self,
requested: Sequence,
barrier: &impl Barrier,
shutdown: &CachePadded<AtomicI64>,
) -> WaitOutcome;
}
#[derive(Clone, Copy, Default)]
pub struct BusySpin;
impl WaitStrategy for BusySpin {
type Waiter = BusySpinWaiter;
type Notifier = NoopWakeupNotifier;
#[inline]
fn new_waiter(&self) -> Self::Waiter {
BusySpinWaiter
}
#[inline]
fn notifier(&self) -> Self::Notifier {
NoopWakeupNotifier
}
}
pub struct BusySpinWaiter;
impl Waiter for BusySpinWaiter {
#[inline]
fn wait_for(
&mut self,
requested: Sequence,
barrier: &impl Barrier,
shutdown: &CachePadded<AtomicI64>,
) -> WaitOutcome {
loop {
return_if_available_or_shutdown!(requested, barrier, shutdown);
}
}
}
#[derive(Clone, Copy, Default)]
pub struct BusySpinWithSpinLoopHint;
impl WaitStrategy for BusySpinWithSpinLoopHint {
type Waiter = BusySpinWithSpinLoopHintWaiter;
type Notifier = NoopWakeupNotifier;
fn new_waiter(&self) -> Self::Waiter {
BusySpinWithSpinLoopHintWaiter
}
#[inline]
fn notifier(&self) -> Self::Notifier {
NoopWakeupNotifier
}
}
pub struct BusySpinWithSpinLoopHintWaiter;
impl Waiter for BusySpinWithSpinLoopHintWaiter {
fn wait_for(
&mut self,
requested: Sequence,
barrier: &impl Barrier,
shutdown: &CachePadded<AtomicI64>,
) -> WaitOutcome {
loop {
return_if_available_or_shutdown!(requested, barrier, shutdown);
hint::spin_loop();
}
}
}
const SPIN_TRIES: u32 = 100;
#[derive(Clone, Copy, Default)]
pub struct YieldingWaitStrategy;
impl WaitStrategy for YieldingWaitStrategy {
type Waiter = YieldingWaiter;
type Notifier = NoopWakeupNotifier;
fn new_waiter(&self) -> Self::Waiter {
YieldingWaiter {
spins_remaining: SPIN_TRIES,
}
}
#[inline]
fn notifier(&self) -> Self::Notifier {
NoopWakeupNotifier
}
}
pub struct YieldingWaiter {
spins_remaining: u32,
}
impl Waiter for YieldingWaiter {
fn wait_for(
&mut self,
requested: Sequence,
barrier: &impl Barrier,
shutdown: &CachePadded<AtomicI64>,
) -> WaitOutcome {
self.spins_remaining = SPIN_TRIES;
loop {
return_if_available_or_shutdown!(requested, barrier, shutdown);
if self.spins_remaining > 0 {
self.spins_remaining -= 1;
hint::spin_loop();
} else {
thread::yield_now();
}
}
}
}
#[derive(Clone, Default)]
pub struct BlockingWaitStrategy {
inner: Arc<BlockingInner>,
spin_tries: u32,
}
impl BlockingWaitStrategy {
pub fn with_spin_tries(mut self, spin_tries: u32) -> Self {
self.spin_tries = spin_tries;
self
}
}
#[derive(Default)]
struct BlockingInner {
mutex: Mutex<u64>,
condvar: Condvar,
}
pub struct BlockingWaiter {
inner: Arc<BlockingInner>,
spin_tries: u32,
}
#[derive(Clone)]
pub struct BlockingNotifier {
inner: Arc<BlockingInner>,
}
impl WakeupNotifier for BlockingNotifier {
fn wake(&self) {
let mut epoch = self.inner.mutex.lock();
*epoch = epoch.wrapping_add(1);
self.inner.condvar.notify_all();
}
}
impl WaitStrategy for BlockingWaitStrategy {
type Waiter = BlockingWaiter;
type Notifier = BlockingNotifier;
fn new_waiter(&self) -> Self::Waiter {
BlockingWaiter {
inner: Arc::clone(&self.inner),
spin_tries: self.spin_tries,
}
}
fn notifier(&self) -> Self::Notifier {
BlockingNotifier {
inner: Arc::clone(&self.inner),
}
}
}
impl Waiter for BlockingWaiter {
fn wait_for(
&mut self,
requested: Sequence,
barrier: &impl Barrier,
shutdown: &CachePadded<AtomicI64>,
) -> WaitOutcome {
loop {
return_if_available_or_shutdown!(requested, barrier, shutdown);
for _ in 0..self.spin_tries {
return_if_available_or_shutdown!(requested, barrier, shutdown);
hint::spin_loop();
}
let mut epoch_guard = self.inner.mutex.lock();
let epoch = *epoch_guard;
while *epoch_guard == epoch {
return_if_available_or_shutdown!(requested, barrier, shutdown);
self.inner.condvar.wait(&mut epoch_guard);
}
}
}
}
#[derive(Clone, Default)]
pub struct LiteBlockingWaitStrategy {
inner: Arc<LiteBlockingInner>,
spin_tries: u32,
}
impl LiteBlockingWaitStrategy {
pub fn with_spin_tries(mut self, spin_tries: u32) -> Self {
self.spin_tries = spin_tries;
self
}
}
#[derive(Default)]
struct LiteBlockingInner {
mutex: Mutex<u64>,
condvar: Condvar,
signal_needed: AtomicBool,
}
pub struct LiteBlockingWaiter {
inner: Arc<LiteBlockingInner>,
spin_tries: u32,
}
#[derive(Clone)]
pub struct LiteBlockingNotifier {
inner: Arc<LiteBlockingInner>,
}
impl WakeupNotifier for LiteBlockingNotifier {
fn wake(&self) {
if !self.inner.signal_needed.swap(false, Ordering::AcqRel) {
return;
}
let mut epoch = self.inner.mutex.lock();
*epoch = epoch.wrapping_add(1);
self.inner.condvar.notify_all();
}
}
impl WaitStrategy for LiteBlockingWaitStrategy {
type Waiter = LiteBlockingWaiter;
type Notifier = LiteBlockingNotifier;
fn new_waiter(&self) -> Self::Waiter {
LiteBlockingWaiter {
inner: Arc::clone(&self.inner),
spin_tries: self.spin_tries,
}
}
fn notifier(&self) -> Self::Notifier {
LiteBlockingNotifier {
inner: Arc::clone(&self.inner),
}
}
}
impl Waiter for LiteBlockingWaiter {
fn wait_for(
&mut self,
requested: Sequence,
barrier: &impl Barrier,
shutdown: &CachePadded<AtomicI64>,
) -> WaitOutcome {
loop {
return_if_available_or_shutdown!(requested, barrier, shutdown);
for _ in 0..self.spin_tries {
return_if_available_or_shutdown!(requested, barrier, shutdown);
hint::spin_loop();
}
let mut epoch_guard = self.inner.mutex.lock();
let epoch = *epoch_guard;
while *epoch_guard == epoch {
self.inner.signal_needed.store(true, Ordering::Release);
return_if_available_or_shutdown!(requested, barrier, shutdown);
self.inner.condvar.wait(&mut epoch_guard);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
sync::{
atomic::{AtomicI64, AtomicUsize},
mpsc, Arc,
},
time::Duration,
};
struct TestBarrier {
available: Arc<AtomicI64>,
}
impl TestBarrier {
fn new(v: i64) -> Self {
Self {
available: Arc::new(AtomicI64::new(v)),
}
}
}
impl Barrier for TestBarrier {
fn get_after(&self, _prev: Sequence) -> Sequence {
self.available.load(Ordering::Relaxed)
}
}
#[test]
fn yielding_waiter_resets_spin_budget_each_wait() {
let mut waiter = YieldingWaiter {
spins_remaining: 0, };
let barrier = TestBarrier::new(5);
let shutdown = CachePadded::new(AtomicI64::new(-1));
let available = waiter.wait_for(5, &barrier, &shutdown);
assert!(matches!(available, WaitOutcome::Available { upper: 5 }));
assert_eq!(waiter.spins_remaining, SPIN_TRIES);
let barrier = TestBarrier::new(4); let shutdown = CachePadded::new(AtomicI64::new(-1));
let barrier_ref = barrier.available.clone();
let handle = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_micros(50));
barrier_ref.store(6, Ordering::Relaxed);
});
let _ = waiter.wait_for(6, &barrier, &shutdown);
handle.join().unwrap();
let barrier = TestBarrier::new(7);
let shutdown = CachePadded::new(AtomicI64::new(-1));
let _ = waiter.wait_for(7, &barrier, &shutdown);
assert_eq!(waiter.spins_remaining, SPIN_TRIES);
}
#[test]
fn yielding_waiter_honors_shutdown() {
let mut waiter = YieldingWaiter {
spins_remaining: SPIN_TRIES,
};
let barrier = TestBarrier::new(-1);
let shutdown = CachePadded::new(AtomicI64::new(10));
let res = waiter.wait_for(10, &barrier, &shutdown);
assert!(matches!(res, WaitOutcome::Shutdown));
}
#[test]
fn busy_spin_waiter_observes_release_with_acquire_fence() {
let mut waiter = BusySpinWaiter;
let shutdown = Arc::new(CachePadded::new(AtomicI64::new(-1)));
let data = Arc::new(AtomicI64::new(0));
let barrier = TestBarrier::new(0);
let barrier_for_thread = barrier.available.clone();
let data_for_thread = data.clone();
let handle = std::thread::spawn(move || {
data_for_thread.store(1, Ordering::Release);
barrier_for_thread.store(1, Ordering::Release);
});
let available = waiter.wait_for(1, &barrier, shutdown.as_ref());
assert!(matches!(available, WaitOutcome::Available { upper: 1 }));
assert_eq!(data.load(Ordering::Acquire), 1);
handle.join().unwrap();
}
#[test]
fn busy_spin_waiter_exits_on_shutdown_race() {
let mut waiter = BusySpinWaiter;
let barrier = TestBarrier::new(-1);
let shutdown = Arc::new(CachePadded::new(AtomicI64::new(-1)));
let shutdown_ref = shutdown.clone();
let handle = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_micros(50));
shutdown_ref.store(5, Ordering::Relaxed);
});
let res = waiter.wait_for(5, &barrier, shutdown.as_ref());
assert!(matches!(res, WaitOutcome::Shutdown));
handle.join().unwrap();
}
#[test]
fn blocking_wait_strategy_wakes_waiters() {
let barrier = TestBarrier::new(0);
let shutdown = CachePadded::new(AtomicI64::new(-1));
let strategy = BlockingWaitStrategy::default();
let notifier = strategy.notifier();
let (tx, rx) = mpsc::channel();
let barrier_available = barrier.available.clone();
std::thread::spawn(move || {
let mut waiter = strategy.new_waiter();
let res = waiter.wait_for(1, &barrier, &shutdown);
tx.send(res).unwrap();
});
std::thread::sleep(Duration::from_millis(10));
barrier_available.store(1, Ordering::Relaxed);
notifier.wake();
let outcome = rx.recv_timeout(Duration::from_secs(1)).unwrap();
assert!(matches!(outcome, WaitOutcome::Available { upper: 1 }));
}
#[test]
fn blocking_wait_strategy_spin_window_can_avoid_sleep() {
struct CountingBarrier {
polls_before_ready: AtomicUsize,
}
impl CountingBarrier {
fn new(polls_before_ready: usize) -> Self {
Self {
polls_before_ready: AtomicUsize::new(polls_before_ready),
}
}
}
impl Barrier for CountingBarrier {
fn get_after(&self, prev: Sequence) -> Sequence {
if self.polls_before_ready.load(Ordering::Relaxed) == 0 {
return prev;
}
let _ = self.polls_before_ready.fetch_sub(1, Ordering::Relaxed);
prev - 1
}
}
let barrier = CountingBarrier::new(5);
let shutdown = CachePadded::new(AtomicI64::new(-1));
let strategy = BlockingWaitStrategy::default().with_spin_tries(16);
let mut waiter = strategy.new_waiter();
let outcome = waiter.wait_for(1, &barrier, &shutdown);
assert!(matches!(outcome, WaitOutcome::Available { upper: 1 }));
}
#[test]
fn lite_blocking_wait_strategy_skips_wake_when_no_waiters() {
let strategy = LiteBlockingWaitStrategy::default();
let notifier = strategy.notifier();
assert!(!strategy.inner.signal_needed.load(Ordering::Relaxed));
notifier.wake();
assert!(!strategy.inner.signal_needed.load(Ordering::Relaxed));
}
#[test]
fn lite_blocking_wait_strategy_spin_window_can_avoid_indicating_signal_needed() {
struct CountingBarrier {
polls_before_ready: AtomicUsize,
}
impl CountingBarrier {
fn new(polls_before_ready: usize) -> Self {
Self {
polls_before_ready: AtomicUsize::new(polls_before_ready),
}
}
}
impl Barrier for CountingBarrier {
fn get_after(&self, prev: Sequence) -> Sequence {
if self.polls_before_ready.load(Ordering::Relaxed) == 0 {
return prev;
}
let _ = self.polls_before_ready.fetch_sub(1, Ordering::Relaxed);
prev - 1
}
}
let barrier = CountingBarrier::new(5);
let shutdown = CachePadded::new(AtomicI64::new(-1));
let strategy = LiteBlockingWaitStrategy::default().with_spin_tries(16);
let mut waiter = strategy.new_waiter();
let outcome = waiter.wait_for(1, &barrier, &shutdown);
assert!(matches!(outcome, WaitOutcome::Available { upper: 1 }));
assert!(!strategy.inner.signal_needed.load(Ordering::Relaxed));
}
#[test]
fn lite_blocking_wait_strategy_wakes_waiters() {
let barrier = TestBarrier::new(0);
let shutdown = CachePadded::new(AtomicI64::new(-1));
let strategy = LiteBlockingWaitStrategy::default();
let notifier = strategy.notifier();
let (tx, rx) = mpsc::channel();
let barrier_available = barrier.available.clone();
let strategy_for_thread = strategy.clone();
std::thread::spawn(move || {
let mut waiter = strategy_for_thread.new_waiter();
let res = waiter.wait_for(1, &barrier, &shutdown);
tx.send(res).unwrap();
});
std::thread::sleep(Duration::from_millis(10));
barrier_available.store(1, Ordering::Relaxed);
notifier.wake();
let outcome = rx.recv_timeout(Duration::from_secs(1)).unwrap();
assert!(matches!(outcome, WaitOutcome::Available { upper: 1 }));
}
}