#[cfg(not(feature = "std"))]
use core::sync::atomic::{AtomicUsize, Ordering};
#[cfg(all(feature = "threads", not(feature = "std")))]
use crate::error::Error;
use crate::error::Result;
#[cfg(all(feature = "threads", not(feature = "std")))]
pub const MAX_THREADS: usize = 64;
#[cfg(not(feature = "std"))]
const UNSET: usize = usize::MAX;
#[cfg(feature = "std")]
std::thread_local! {
static CURRENT: core::cell::Cell<Option<(usize, usize)>> = const { core::cell::Cell::new(None) };
}
#[cfg(feature = "std")]
pub fn current() -> Option<(usize, usize)> {
CURRENT.with(|slot| slot.get())
}
#[cfg(feature = "std")]
pub fn replace(next: Option<(usize, usize)>) -> Option<(usize, usize)> {
CURRENT.with(|slot| slot.replace(next))
}
#[cfg(not(any(feature = "std", feature = "threads")))]
pub fn current() -> Option<(usize, usize)> {
read_global()
}
#[cfg(not(any(feature = "std", feature = "threads")))]
pub fn replace(next: Option<(usize, usize)>) -> Option<(usize, usize)> {
replace_global(next)
}
#[cfg(all(feature = "threads", not(feature = "std")))]
mod table {
use super::*;
use alloc::boxed::Box;
use once_cell::race::OnceBox;
pub type ThreadIdHook = Box<dyn Fn() -> usize + Send + Sync>;
static HOOK: OnceBox<ThreadIdHook> = OnceBox::new();
pub struct Slot {
pub token: AtomicUsize,
pub worker: AtomicUsize,
pub num_workers: AtomicUsize,
}
impl Slot {
const fn new() -> Slot {
Slot { token: AtomicUsize::new(UNSET), worker: AtomicUsize::new(UNSET), num_workers: AtomicUsize::new(1) }
}
}
#[allow(clippy::declare_interior_mutable_const)]
const EMPTY: Slot = Slot::new();
pub static SLOTS: [Slot; MAX_THREADS] = [EMPTY; MAX_THREADS];
pub fn set_hook(hook: impl Fn() -> usize + Send + Sync + 'static) -> Result<()> {
HOOK.set(Box::new(Box::new(hook))).map_err(|_| Error::value("the thread id hook has already been set"))
}
pub fn token() -> Option<usize> {
HOOK.get().map(|hook| hook())
}
fn find(token: usize) -> Option<&'static Slot> {
SLOTS.iter().find(|slot| slot.token.load(Ordering::Acquire) == token)
}
fn claim(token: usize) -> Option<&'static Slot> {
SLOTS.iter().find(|slot| slot.token.compare_exchange(UNSET, token, Ordering::AcqRel, Ordering::Acquire).is_ok())
}
pub fn current() -> Option<(usize, usize)> {
let Some(token) = token() else {
return read_global();
};
let slot = find(token)?;
match slot.worker.load(Ordering::Acquire) {
UNSET => None,
worker => Some((worker, slot.num_workers.load(Ordering::Acquire))),
}
}
pub fn replace(next: Option<(usize, usize)>) -> Option<(usize, usize)> {
let Some(token) = token() else {
return replace_global(next);
};
let slot = match find(token).or_else(|| claim(token)) {
Some(slot) => slot,
None => {
log::warn!("more than {MAX_THREADS} worker threads; falling back to a shared identity");
return replace_global(next);
}
};
let previous = match slot.worker.load(Ordering::Acquire) {
UNSET => None,
worker => Some((worker, slot.num_workers.load(Ordering::Acquire))),
};
match next {
Some((worker, num_workers)) => {
slot.num_workers.store(num_workers.max(1), Ordering::Release);
slot.worker.store(worker, Ordering::Release);
}
None => {
slot.worker.store(UNSET, Ordering::Release);
slot.token.store(UNSET, Ordering::Release);
}
}
previous
}
}
#[cfg(all(feature = "threads", not(feature = "std")))]
pub use table::{current, replace};
#[cfg(not(feature = "std"))]
static GLOBAL_FALLBACK: (AtomicUsize, AtomicUsize) = (AtomicUsize::new(UNSET), AtomicUsize::new(1));
#[cfg(not(feature = "std"))]
fn read_global() -> Option<(usize, usize)> {
match GLOBAL_FALLBACK.0.load(Ordering::Acquire) {
UNSET => None,
worker => Some((worker, GLOBAL_FALLBACK.1.load(Ordering::Acquire))),
}
}
#[cfg(not(feature = "std"))]
fn replace_global(next: Option<(usize, usize)>) -> Option<(usize, usize)> {
let previous = read_global();
let (worker, num_workers) = next.unwrap_or((UNSET, 1));
GLOBAL_FALLBACK.1.store(num_workers.max(1), Ordering::Release);
GLOBAL_FALLBACK.0.store(worker, Ordering::Release);
previous
}
#[cfg(any(feature = "std", not(feature = "threads")))]
pub fn set_thread_id_hook(_hook: impl Fn() -> usize + Send + Sync + 'static) -> Result<()> {
Ok(())
}
#[cfg(all(feature = "threads", not(feature = "std")))]
pub fn set_thread_id_hook(hook: impl Fn() -> usize + Send + Sync + 'static) -> Result<()> {
table::set_hook(hook)
}
pub fn with_worker<T>(worker: usize, num_workers: usize, body: impl FnOnce() -> T) -> T {
let previous = replace(Some((worker, num_workers)));
let result = body();
replace(previous);
result
}
pub fn set_worker(worker: usize, num_workers: usize) {
replace(Some((worker, num_workers)));
}
pub fn clear_worker() {
replace(None);
}
#[cfg(test)]
extern crate std;
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused_imports)]
use std::vec::Vec;
#[test]
fn binds_and_restores_an_identity() {
assert_eq!(current(), None);
with_worker(3, 8, || {
assert_eq!(current(), Some((3, 8)));
with_worker(1, 2, || assert_eq!(current(), Some((1, 2))));
assert_eq!(current(), Some((3, 8)), "the outer binding is restored");
});
assert_eq!(current(), None);
}
#[test]
fn set_and_clear_persist_beyond_a_scope() {
set_worker(5, 6);
assert_eq!(current(), Some((5, 6)));
clear_worker();
assert_eq!(current(), None);
}
#[cfg(feature = "std")]
#[test]
fn identities_do_not_leak_between_threads() {
set_worker(1, 4);
let seen = std::thread::spawn(current).join().expect("thread");
assert_eq!(seen, None, "another thread has its own identity");
assert_eq!(current(), Some((1, 4)));
clear_worker();
}
#[cfg(feature = "std")]
#[test]
fn many_threads_keep_their_own_identity() {
let handles: Vec<_> =
std::vec::Vec::from_iter((0..16).map(|i| std::thread::spawn(move || with_worker(i, 16, current))));
for (i, handle) in handles.into_iter().enumerate() {
assert_eq!(handle.join().expect("thread"), Some((i, 16)));
}
}
#[cfg(all(feature = "threads", not(feature = "std")))]
#[test]
fn the_slot_table_keeps_threads_apart() {
std::thread_local! {
static TOKEN: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
set_thread_id_hook(|| TOKEN.with(|t| t.get())).expect("the hook is set once");
assert!(set_thread_id_hook(|| 0).is_err(), "the hook cannot be replaced");
let handles: Vec<_> = std::vec::Vec::from_iter((0..8).map(|i| {
std::thread::spawn(move || {
TOKEN.with(|t| t.set(i + 1));
let seen = with_worker(i, 8, current);
(seen, current())
})
}));
for (i, handle) in handles.into_iter().enumerate() {
let (inside, after) = handle.join().expect("thread");
assert_eq!(inside, Some((i, 8)), "thread {i} should see its own identity");
assert_eq!(after, None, "the binding is released when the scope ends");
}
}
#[cfg(all(not(feature = "threads"), not(feature = "std")))]
#[test]
fn without_threads_a_single_global_is_used() {
set_worker(2, 3);
assert_eq!(current(), Some((2, 3)));
assert_eq!(std::thread::spawn(current).join().expect("thread"), Some((2, 3)));
clear_worker();
}
}