use std::sync::PoisonError;
use std::time::Duration;
use crossbeam_utils::Backoff;
use crate::SyncError;
use crate::primitives::{AtomicUsize, Condvar, Mutex, Ordering};
const HELP_RECHECK: Duration = Duration::from_micros(500);
#[derive(Debug)]
pub struct AtomicCounter {
count: AtomicUsize,
waiters: AtomicUsize,
lock: Mutex<()>,
cvar: Condvar,
}
impl Default for AtomicCounter {
fn default() -> Self {
Self::new(0)
}
}
impl AtomicCounter {
#[must_use]
pub fn new(count: usize) -> Self {
Self {
count: AtomicUsize::new(count),
waiters: AtomicUsize::new(0),
lock: Mutex::new(()),
cvar: Condvar::new(),
}
}
#[must_use]
pub fn get(&self) -> usize {
self.count.load(Ordering::Acquire)
}
#[must_use]
pub fn is_zero(&self) -> bool {
self.get() == 0
}
pub fn increment(&self) -> Result<(), SyncError> {
let mut current = self.count.load(Ordering::Relaxed);
loop {
let next = current.checked_add(1).ok_or(SyncError::CounterOverflow)?;
match self.count.compare_exchange_weak(
current,
next,
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => return Ok(()),
Err(actual) => current = actual,
}
}
}
pub fn decrement(&self) -> bool {
let mut current = self.count.load(Ordering::Acquire);
let reached_zero = loop {
let Some(next) = current.checked_sub(1) else {
return false;
};
match self.count.compare_exchange_weak(
current,
next,
Ordering::SeqCst,
Ordering::Acquire,
) {
Ok(_) => break next == 0,
Err(actual) => current = actual,
}
};
if reached_zero && self.waiters.fetch_add(0, Ordering::SeqCst) > 0 {
let _guard = self.lock.lock().unwrap_or_else(PoisonError::into_inner);
self.cvar.notify_all();
}
reached_zero
}
pub fn wait(&self) {
self.wait_inner(None);
}
pub fn wait_timeout(&self, timeout: Duration) -> bool {
self.wait_inner(Some(timeout))
}
pub fn wait_while_helping(&self, mut help: impl FnMut() -> bool) {
let backoff = Backoff::new();
while !self.is_zero() {
if help() {
backoff.reset();
} else if backoff.is_completed() {
self.wait_timeout(HELP_RECHECK);
} else {
backoff.snooze();
}
}
}
fn wait_inner(&self, timeout: Option<Duration>) -> bool {
if self.is_zero() {
return true;
}
let mut guard = self.lock.lock().unwrap_or_else(PoisonError::into_inner);
self.waiters.fetch_add(1, Ordering::SeqCst);
match timeout {
None => {
while self.count.load(Ordering::SeqCst) != 0 {
guard = self
.cvar
.wait(guard)
.unwrap_or_else(PoisonError::into_inner);
}
}
Some(timeout) => {
if self.count.load(Ordering::SeqCst) != 0 {
guard = self
.cvar
.wait_timeout(guard, timeout)
.unwrap_or_else(PoisonError::into_inner)
.0;
}
}
}
self.waiters.fetch_sub(1, Ordering::SeqCst);
drop(guard);
self.is_zero()
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn increment_decrement_roundtrip() {
let c = AtomicCounter::new(0);
c.increment().unwrap();
c.increment().unwrap();
assert_eq!(c.get(), 2);
assert!(!c.decrement());
assert!(c.decrement());
assert!(!c.decrement(), "decrement on zero is a no-op");
assert!(c.is_zero());
}
#[test]
fn overflow_is_an_error() {
let c = AtomicCounter::new(usize::MAX);
assert_eq!(c.increment(), Err(SyncError::CounterOverflow));
}
#[test]
fn wait_wakes_on_zero() {
const N: usize = 32;
let c = Arc::new(AtomicCounter::new(N));
let threads: Vec<_> = (0..N)
.map(|_| {
let c = Arc::clone(&c);
std::thread::spawn(move || {
c.decrement();
})
})
.collect();
c.wait();
assert!(c.is_zero());
for t in threads {
t.join().unwrap();
}
}
#[test]
fn helper_runs_until_zero() {
let c = AtomicCounter::new(3);
let mut calls = 0;
c.wait_while_helping(|| {
calls += 1;
c.decrement();
true
});
assert_eq!(calls, 3);
}
#[test]
fn wait_timeout_reports_state() {
let c = AtomicCounter::new(1);
assert!(!c.wait_timeout(Duration::from_millis(1)));
c.decrement();
assert!(c.wait_timeout(Duration::from_millis(1)));
}
}