use core::fmt;
use std::sync::{Arc, LockResult, Mutex, MutexGuard, PoisonError};
#[derive(Debug)]
pub struct Shared<T> {
inner: Arc<Mutex<T>>,
}
impl<T> Clone for Shared<T> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SynchronizationError {
Poisoned,
}
impl fmt::Display for SynchronizationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Poisoned => f.write_str("shared state lock is poisoned"),
}
}
}
impl std::error::Error for SynchronizationError {}
impl<T> Shared<T> {
#[must_use]
pub fn new(value: T) -> Self {
Self {
inner: Arc::new(Mutex::new(value)),
}
}
pub fn lock(&self) -> Result<MutexGuard<'_, T>, SynchronizationError> {
self.inner
.lock()
.map_err(|_| SynchronizationError::Poisoned)
}
#[must_use]
pub fn strong_count(&self) -> usize {
Arc::strong_count(&self.inner)
}
}
impl<T> From<LockResult<MutexGuard<'_, T>>> for SynchronizationError {
fn from(_: LockResult<MutexGuard<'_, T>>) -> Self {
Self::Poisoned
}
}
impl<T> From<PoisonError<MutexGuard<'_, T>>> for SynchronizationError {
fn from(_: PoisonError<MutexGuard<'_, T>>) -> Self {
Self::Poisoned
}
}
#[cfg(test)]
mod tests {
use super::{Shared, SynchronizationError};
use std::sync::{mpsc, Barrier};
use std::time::Duration;
#[test]
fn clones_share_the_same_value() {
let shared = Shared::new(10);
let clone = shared.clone();
*clone.lock().expect("lock should be healthy") += 5;
assert_eq!(*shared.lock().expect("lock should be healthy"), 15);
assert_eq!(shared.strong_count(), 2);
}
#[test]
fn serializes_concurrent_updates() {
let shared = Shared::new(0_u64);
let threads: Vec<_> = (0..8)
.map(|_| {
let shared = shared.clone();
std::thread::spawn(move || {
for _ in 0..1_000 {
*shared.lock().expect("lock should be healthy") += 1;
}
})
})
.collect();
for thread in threads {
thread.join().expect("worker should not panic");
}
assert_eq!(*shared.lock().expect("lock should be healthy"), 8_000);
}
#[test]
fn cloned_handles_start_together_and_preserve_every_update() {
const WORKERS: usize = 16;
const UPDATES: usize = 2_000;
let shared = Shared::new(0_usize);
let barrier = std::sync::Arc::new(Barrier::new(WORKERS + 1));
let workers: Vec<_> = (0..WORKERS)
.map(|_| {
let shared = shared.clone();
let barrier = std::sync::Arc::clone(&barrier);
std::thread::spawn(move || {
barrier.wait();
for _ in 0..UPDATES {
*shared.lock().expect("lock should remain healthy") += 1;
}
})
})
.collect();
barrier.wait();
for worker in workers {
worker.join().expect("worker should finish without panic");
}
assert_eq!(
*shared.lock().expect("lock should remain healthy"),
WORKERS * UPDATES
);
}
#[test]
fn lock_is_exclusive_until_guard_is_dropped() {
let shared = Shared::new(42_u32);
let guard = shared.lock().expect("initial lock should be healthy");
let clone = shared.clone();
let (tx, rx) = mpsc::channel();
let worker = std::thread::spawn(move || {
let result = clone.lock().map(|value| *value);
tx.send(result).expect("receiver should still exist");
});
assert!(rx.recv_timeout(Duration::from_millis(25)).is_err());
drop(guard);
assert_eq!(
rx.recv_timeout(Duration::from_secs(2)).unwrap().unwrap(),
42
);
worker.join().expect("worker should finish without panic");
}
#[test]
fn reports_poisoned_lock_instead_of_silently_recovering() {
let shared = Shared::new(0);
let clone = shared.clone();
let _ = std::thread::spawn(move || {
let _guard = clone.lock().expect("initial lock should be healthy");
panic!("poison the test mutex");
})
.join();
assert_eq!(shared.lock().unwrap_err(), SynchronizationError::Poisoned);
assert_eq!(
SynchronizationError::Poisoned.to_string(),
"shared state lock is poisoned"
);
}
}