Extract scheduler responsibilities into a dedicated Runtime component: - src/runtime.rs: New centralized control flow (669 lines) - src/scheduler.rs: Simplified to task queue & preemption management - tests/runtime.rs: Comprehensive runtime test suite - benches/multi_scheduler.rs: Multi-runtime scheduling benchmarks - Improves modularity and enables per-runtime configuration
315 lines
11 KiB
Rust
315 lines
11 KiB
Rust
//! `loom::Mutex<T>` tests. All run under the scheduler because `lock()`
|
|
//! needs to be able to park.
|
|
|
|
use smarm::{run, spawn, yield_now, LockTimeout, Mutex};
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex as StdMutex;
|
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
use std::time::{Duration, Instant};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Uncontended fast path
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn lock_free_mutex_succeeds() {
|
|
let captured = Arc::new(AtomicU32::new(0));
|
|
let c = captured.clone();
|
|
run(move || {
|
|
let m = Mutex::new(42u32);
|
|
{
|
|
let g = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
c.store(*g, Ordering::SeqCst);
|
|
}
|
|
// After drop we can lock again.
|
|
let g2 = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
assert_eq!(*g2, 42);
|
|
});
|
|
assert_eq!(captured.load(Ordering::SeqCst), 42);
|
|
}
|
|
|
|
#[test]
|
|
fn try_lock_returns_some_when_free_none_when_held() {
|
|
let success_flag = Arc::new(AtomicU32::new(0));
|
|
let s = success_flag.clone();
|
|
run(move || {
|
|
let m = Mutex::new(0u32);
|
|
let g = m.try_lock().expect("free");
|
|
// Holding the guard; a second try_lock on the same actor should fail.
|
|
assert!(m.try_lock().is_none());
|
|
drop(g);
|
|
// Now free again.
|
|
let g2 = m.try_lock().expect("free again");
|
|
drop(g2);
|
|
s.store(1, Ordering::SeqCst);
|
|
});
|
|
assert_eq!(success_flag.load(Ordering::SeqCst), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn guard_mutates_value_visible_through_next_lock() {
|
|
let final_value = Arc::new(AtomicU32::new(0));
|
|
let f = final_value.clone();
|
|
run(move || {
|
|
let m = Mutex::new(0u32);
|
|
{
|
|
let mut g = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
*g = 7;
|
|
}
|
|
let g2 = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
f.store(*g2, Ordering::SeqCst);
|
|
});
|
|
assert_eq!(final_value.load(Ordering::SeqCst), 7);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Contention: a second actor parks until the first releases.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn contended_lock_parks_until_holder_releases() {
|
|
// Actor A locks, yields (still holding), then releases. Actor B tries
|
|
// to lock in between — B should park, then succeed after A drops.
|
|
let log: Arc<StdMutex<Vec<&'static str>>> = Arc::new(StdMutex::new(Vec::new()));
|
|
let la = log.clone();
|
|
let lb = log.clone();
|
|
|
|
run(move || {
|
|
let m = Mutex::new(0u32);
|
|
let m_a = m.clone();
|
|
let m_b = m.clone();
|
|
|
|
let a = spawn(move || {
|
|
let g = m_a.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
la.lock().unwrap().push("A_locked");
|
|
// First yield: lets B run past its first yield_now.
|
|
yield_now();
|
|
// Second yield: lets B reach B_try and attempt lock() while we
|
|
// still hold it, so B parks on the mutex.
|
|
yield_now();
|
|
la.lock().unwrap().push("A_dropping");
|
|
drop(g);
|
|
la.lock().unwrap().push("A_dropped");
|
|
});
|
|
let b = spawn(move || {
|
|
// One yield: lets A run and acquire the lock first.
|
|
yield_now();
|
|
lb.lock().unwrap().push("B_try");
|
|
let _g = m_b.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
lb.lock().unwrap().push("B_locked");
|
|
});
|
|
a.join().unwrap();
|
|
b.join().unwrap();
|
|
});
|
|
|
|
let v = log.lock().unwrap();
|
|
// A locks, B tries (parks), A drops, B gets the lock.
|
|
let pos_a_locked = v.iter().position(|s| *s == "A_locked").unwrap();
|
|
let pos_b_try = v.iter().position(|s| *s == "B_try").unwrap();
|
|
let pos_a_dropped = v.iter().position(|s| *s == "A_dropped").unwrap();
|
|
let pos_b_locked = v.iter().position(|s| *s == "B_locked").unwrap();
|
|
|
|
assert!(pos_a_locked < pos_b_try, "log: {:?}", *v);
|
|
assert!(pos_b_try < pos_a_dropped, "B should attempt before A drops: {:?}", *v);
|
|
assert!(pos_a_dropped < pos_b_locked, "B should lock only after A drops: {:?}", *v);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Timeout: B times out while A holds forever.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn lock_timeout_returns_err_when_holder_never_releases() {
|
|
let saw_err = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
let s = saw_err.clone();
|
|
|
|
run(move || {
|
|
let m: Mutex<u32> = Mutex::new(0);
|
|
let m_a = m.clone();
|
|
let m_b = m.clone();
|
|
|
|
let a = spawn(move || {
|
|
// Hold the lock for 100ms, blocking B's attempt with a 20ms timeout.
|
|
let _g = m_a.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
smarm::sleep(Duration::from_millis(100));
|
|
// _g drops here.
|
|
});
|
|
let b = spawn(move || {
|
|
// Let A acquire first.
|
|
yield_now();
|
|
let t0 = Instant::now();
|
|
let res = m_b.lock_timeout(Duration::from_millis(20));
|
|
let elapsed = t0.elapsed();
|
|
assert!(matches!(res, Err(LockTimeout)), "got {:?}", res);
|
|
// Sanity: actually waited approximately the timeout.
|
|
assert!(
|
|
elapsed >= Duration::from_millis(15),
|
|
"timed out too fast: {:?}",
|
|
elapsed
|
|
);
|
|
assert!(
|
|
elapsed < Duration::from_millis(80),
|
|
"timed out far too slow: {:?}",
|
|
elapsed
|
|
);
|
|
s.store(true, Ordering::SeqCst);
|
|
});
|
|
a.join().unwrap();
|
|
b.join().unwrap();
|
|
});
|
|
|
|
assert!(saw_err.load(Ordering::SeqCst));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FIFO fairness: when many actors queue, they get the lock in arrival order.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn waiters_are_granted_the_lock_in_fifo_order() {
|
|
let order: Arc<StdMutex<Vec<u32>>> = Arc::new(StdMutex::new(Vec::new()));
|
|
|
|
run({
|
|
let order = order.clone();
|
|
move || {
|
|
let m: Mutex<()> = Mutex::new(());
|
|
|
|
// Holder: takes the lock, yields to let others queue up, then
|
|
// releases. Each waiter records its arrival order on acquisition.
|
|
let m_holder = m.clone();
|
|
let holder = spawn(move || {
|
|
let g = m_holder.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
// Let waiters pile up.
|
|
for _ in 0..5 {
|
|
yield_now();
|
|
}
|
|
drop(g);
|
|
});
|
|
|
|
// Spawn 4 waiters in order 1, 2, 3, 4. Each yields once before
|
|
// calling lock(), so we know the holder ran first.
|
|
let mut handles = vec![holder];
|
|
for id in 1u32..=4 {
|
|
let m_w = m.clone();
|
|
let o = order.clone();
|
|
handles.push(spawn(move || {
|
|
// Stagger the lock attempts so they arrive in order.
|
|
for _ in 0..id {
|
|
yield_now();
|
|
}
|
|
let _g = m_w.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
o.lock().unwrap().push(id);
|
|
}));
|
|
}
|
|
for h in handles {
|
|
h.join().unwrap();
|
|
}
|
|
}
|
|
});
|
|
|
|
let v = order.lock().unwrap().clone();
|
|
assert_eq!(v, vec![1, 2, 3, 4], "waiters should acquire in arrival order");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Grant-vs-timeout race: holder drops just before timer would fire — waiter
|
|
// should get the lock, not LockTimeout.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn grant_wins_when_holder_releases_before_timeout() {
|
|
let got_lock = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
let g = got_lock.clone();
|
|
|
|
run(move || {
|
|
let m: Mutex<u32> = Mutex::new(0);
|
|
let m_a = m.clone();
|
|
let m_b = m.clone();
|
|
|
|
let a = spawn(move || {
|
|
let _g = m_a.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
// Hold for 10ms, well under B's 100ms timeout.
|
|
smarm::sleep(Duration::from_millis(10));
|
|
});
|
|
let b = spawn(move || {
|
|
yield_now();
|
|
let res = m_b.lock_timeout(Duration::from_millis(100));
|
|
if res.is_ok() {
|
|
g.store(true, Ordering::SeqCst);
|
|
}
|
|
});
|
|
a.join().unwrap();
|
|
b.join().unwrap();
|
|
});
|
|
|
|
assert!(got_lock.load(Ordering::SeqCst));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Panic in critical section: next waiter still gets the lock (no poisoning).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn next_waiter_gets_lock_after_holder_panics() {
|
|
let next_got_it = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
let n = next_got_it.clone();
|
|
|
|
run(move || {
|
|
let m: Mutex<u32> = Mutex::new(7);
|
|
let m_a = m.clone();
|
|
let m_b = m.clone();
|
|
|
|
let a = spawn(move || {
|
|
let _g = m_a.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
yield_now();
|
|
panic!("holder dies mid-critical-section");
|
|
});
|
|
let b = spawn(move || {
|
|
yield_now();
|
|
// A is dead but its guard's Drop ran during unwind. We get the lock.
|
|
let g = m_b.lock_timeout(Duration::from_millis(100)).unwrap();
|
|
assert_eq!(*g, 7);
|
|
n.store(true, Ordering::SeqCst);
|
|
});
|
|
let _ = a.join(); // panic — expected
|
|
b.join().unwrap();
|
|
});
|
|
|
|
assert!(next_got_it.load(Ordering::SeqCst));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Multiple short critical sections under contention all complete (no lost
|
|
// wakeups, no deadlock). Counts up to N from M actors.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn many_actors_increment_shared_counter_via_mutex() {
|
|
const ACTORS: u32 = 8;
|
|
const PER_ACTOR: u32 = 50;
|
|
|
|
let final_value = Arc::new(AtomicU32::new(0));
|
|
let fv = final_value.clone();
|
|
|
|
run(move || {
|
|
let m: Mutex<u32> = Mutex::new(0);
|
|
let mut handles = Vec::new();
|
|
for _ in 0..ACTORS {
|
|
let m_i = m.clone();
|
|
handles.push(spawn(move || {
|
|
for _ in 0..PER_ACTOR {
|
|
let mut g = m_i.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
*g += 1;
|
|
}
|
|
}));
|
|
}
|
|
for h in handles {
|
|
h.join().unwrap();
|
|
}
|
|
let g = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
|
fv.store(*g, Ordering::SeqCst);
|
|
});
|
|
|
|
assert_eq!(final_value.load(Ordering::SeqCst), ACTORS * PER_ACTOR);
|
|
}
|