use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use crate::objects::{self, FieldSlot};
use crate::sched::{ProcessId, ProcessRequest};
const COUNT_SLOT: FieldSlot = FieldSlot {
offset: 0,
words: 1,
};
pub const SEMAPHORE_FIELD_WORDS: usize = 1;
#[derive(Default)]
struct Table {
queues: HashMap<u64, VecDeque<(ProcessId, i64)>>,
waiting_on: HashMap<ProcessId, u64>,
}
thread_local! {
static TABLE: RefCell<Table> = RefCell::new(Table::default());
}
fn with_table<R>(f: impl FnOnce(&mut Table) -> R) -> R {
TABLE.with(|t| f(&mut t.borrow_mut()))
}
fn count_of(h: u64) -> Option<i64> {
let mut w = [0u64];
objects::field_get(h, COUNT_SLOT, &mut w).then_some(w[0] as i64)
}
fn set_count(h: u64, count: i64) {
objects::field_set(h, COUNT_SLOT, &[count as u64]);
}
pub fn init(h: u64, count: i64) -> bool {
objects::field_set(h, COUNT_SLOT, &[count as u64])
}
fn grant(t: &mut Table, h: u64, emit: &mut dyn FnMut(ProcessRequest)) -> usize {
let Some(mut count) = count_of(h) else {
return 0;
};
let mut woken = 0;
if let Some(queue) = t.queues.get_mut(&h) {
while let Some(&(proc, need)) = queue.front() {
if count < need {
break;
}
count -= need;
queue.pop_front();
t.waiting_on.remove(&proc);
emit(ProcessRequest::SyncWake(proc));
woken += 1;
}
if queue.is_empty() {
t.queues.remove(&h);
}
}
set_count(h, count);
woken
}
pub fn put(h: u64, n: i64, emit: &mut dyn FnMut(ProcessRequest)) -> bool {
if n < 0 {
return count_of(h).is_some();
}
let woken = with_table(|t| {
let count = count_of(h)?;
set_count(h, count.saturating_add(n));
Some(grant(t, h, emit))
});
let Some(woken) = woken else {
return false;
};
for _ in 0..woken {
objects::release(h);
}
true
}
pub fn get(h: u64, proc: ProcessId, n: i64, emit: &mut dyn FnMut(ProcessRequest)) -> bool {
with_table(|t| {
let Some(count) = count_of(h) else {
return false;
};
if n < 0 {
return false;
}
let queued = t.queues.get(&h).is_some_and(|q| !q.is_empty());
if !queued && count >= n {
set_count(h, count - n);
return false;
}
assert!(
!t.waiting_on.contains_key(&proc),
"semaphore: process {proc} called get() while already blocked in one"
);
objects::retain(h);
t.queues.entry(h).or_default().push_back((proc, n));
t.waiting_on.insert(proc, h);
emit(ProcessRequest::SyncBlock);
true
})
}
pub fn try_get(h: u64, n: i64) -> bool {
let Some(count) = count_of(h) else {
return false;
};
if n < 0 || count < n {
return false;
}
set_count(h, count - n);
true
}
pub fn keys(h: u64) -> Option<i64> {
count_of(h)
}
pub fn waiters(h: u64) -> usize {
with_table(|t| t.queues.get(&h).map_or(0, VecDeque::len))
}
pub fn cancel(proc: ProcessId, emit: &mut dyn FnMut(ProcessRequest)) {
let cancelled = with_table(|t| {
let h = t.waiting_on.remove(&proc)?;
if let Some(queue) = t.queues.get_mut(&h) {
queue.retain(|&(p, _)| p != proc);
if queue.is_empty() {
t.queues.remove(&h);
}
}
Some((h, grant(t, h, emit)))
});
let Some((h, woken)) = cancelled else {
return;
};
for _ in 0..=woken {
objects::release(h);
}
}
pub fn reset_for_new_run() {
let queues = with_table(|t| {
t.waiting_on.clear();
std::mem::take(&mut t.queues)
});
for (h, queue) in queues {
for _ in queue {
objects::release(h);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::netlist::ClassTemplateId;
fn sem(count: i64, template: u32) -> u64 {
let h = objects::alloc(ClassTemplateId(template), SEMAPHORE_FIELD_WORDS);
assert!(init(h, count));
h
}
#[derive(Default)]
struct Log(Vec<ProcessRequest>);
impl Log {
fn sink(&mut self) -> impl FnMut(ProcessRequest) + '_ {
|r| self.0.push(r)
}
}
#[test]
fn get_takes_keys_without_blocking_when_enough_are_free() {
let h = sem(3, 17_000);
let mut log = Log::default();
assert!(!get(h, 1_000, 2, &mut log.sink()));
assert_eq!(keys(h), Some(1));
assert!(log.0.is_empty(), "an immediate get queues no request");
assert!(get(h, 1_000, 2, &mut log.sink()), "one key is not two");
assert_eq!(log.0, [ProcessRequest::SyncBlock]);
assert_eq!(keys(h), Some(1), "a blocked get takes nothing yet");
}
#[test]
fn put_serves_waiters_in_arrival_order_and_hands_over_the_keys() {
let h = sem(0, 17_001);
let mut log = Log::default();
for p in [1_010, 1_011, 1_012] {
assert!(get(h, p, 1, &mut log.sink()));
}
log.0.clear();
assert!(put(h, 2, &mut log.sink()));
assert_eq!(
log.0,
[
ProcessRequest::SyncWake(1_010),
ProcessRequest::SyncWake(1_011)
]
);
assert_eq!(
keys(h),
Some(0),
"the woken processes already hold the keys"
);
assert_eq!(waiters(h), 1);
}
#[test]
fn a_large_request_is_not_overtaken_by_a_later_small_one() {
let h = sem(0, 17_002);
let mut log = Log::default();
assert!(get(h, 1_020, 3, &mut log.sink()));
assert!(
get(h, 1_021, 1, &mut log.sink()),
"queues behind the get(3)"
);
log.0.clear();
put(h, 2, &mut log.sink());
assert!(
log.0.is_empty(),
"two keys serve neither the head (3) nor, by FIFO, the next"
);
assert_eq!(keys(h), Some(2));
put(h, 2, &mut log.sink());
assert_eq!(
log.0,
[
ProcessRequest::SyncWake(1_020),
ProcessRequest::SyncWake(1_021)
]
);
assert_eq!(keys(h), Some(0));
}
#[test]
fn a_get_arriving_behind_a_queue_waits_even_when_keys_are_free() {
let h = sem(1, 17_003);
let mut log = Log::default();
assert!(get(h, 1_030, 2, &mut log.sink()));
assert!(
get(h, 1_031, 1, &mut log.sink()),
"FIFO: the one free key belongs to the queue's order, not to the newcomer"
);
assert_eq!(waiters(h), 2);
}
#[test]
fn try_get_never_blocks_and_takes_keys_only_when_available() {
let h = sem(2, 17_004);
assert!(try_get(h, 1));
assert!(!try_get(h, 2));
assert_eq!(keys(h), Some(1));
assert!(try_get(h, 1));
assert!(!try_get(h, 1));
assert!(try_get(h, 0), "taking zero keys succeeds");
assert_eq!(waiters(h), 0);
}
#[test]
fn a_negative_count_must_climb_above_zero_first() {
let h = sem(-2, 17_005);
let mut log = Log::default();
assert!(!try_get(h, 1));
put(h, 2, &mut log.sink());
assert!(!try_get(h, 1), "two puts from -2 reach zero, not one");
put(h, 1, &mut log.sink());
assert!(try_get(h, 1));
}
#[test]
fn cancelling_a_waiter_removes_it_and_lets_the_next_one_through() {
let h = sem(1, 17_006);
let mut log = Log::default();
assert!(get(h, 1_040, 2, &mut log.sink()), "blocks the queue head");
assert!(get(h, 1_041, 1, &mut log.sink()));
log.0.clear();
cancel(1_040, &mut log.sink());
assert_eq!(log.0, [ProcessRequest::SyncWake(1_041)]);
assert_eq!(keys(h), Some(0));
assert_eq!(waiters(h), 0);
cancel(1_040, &mut log.sink());
assert_eq!(log.0.len(), 1);
}
#[test]
fn a_waiter_keeps_its_semaphore_alive_until_it_leaves_the_queue() {
let h = sem(0, 17_007);
let mut log = Log::default();
assert!(get(h, 1_050, 1, &mut log.sink()));
assert_eq!(
objects::refcount(h),
Some(2),
"the allocation's and the waiter's"
);
put(h, 1, &mut log.sink());
assert_eq!(objects::refcount(h), Some(1), "the woken waiter let go");
assert!(get(h, 1_051, 1, &mut log.sink()));
cancel(1_051, &mut log.sink());
assert_eq!(
objects::refcount(h),
Some(1),
"a cancelled waiter let go too"
);
}
#[test]
fn a_negative_key_count_changes_nothing() {
let h = sem(2, 17_008);
let mut log = Log::default();
assert!(!try_get(h, -1));
assert_eq!(keys(h), Some(2));
assert!(put(h, -5, &mut log.sink()));
assert_eq!(keys(h), Some(2));
assert!(!get(h, 1_070, -3, &mut log.sink()));
assert_eq!(keys(h), Some(2));
assert!(log.0.is_empty());
assert_eq!(waiters(h), 0);
}
#[test]
fn a_non_semaphore_handle_blocks_and_grants_nothing() {
let mut log = Log::default();
assert!(!get(0, 1_060, 1, &mut log.sink()));
assert!(!put(0, 1, &mut log.sink()));
assert!(!try_get(0, 1));
assert!(log.0.is_empty());
}
}