use std::cell::{Cell, RefCell};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
pub const MAX_RELEASED_THREADS: usize = 1000;
static RELEASED: AtomicUsize = AtomicUsize::new(0);
enum Hook {
Owned(Box<dyn FnOnce()>),
Borrowed(*const (dyn Fn() + 'static)),
}
impl Hook {
fn call(self) {
match self {
Hook::Owned(f) => f(),
Hook::Borrowed(f) => unsafe { (*f)() },
}
}
}
struct Restore {
prev_hook: Option<Option<Hook>>,
prev_released: bool,
}
impl Drop for Restore {
fn drop(&mut self) {
let prev = self.prev_hook.take().flatten();
let _ = HOOK.try_with(|h| *h.borrow_mut() = prev);
if let Ok(true) = RELEASED_HERE.try_with(|r| r.replace(self.prev_released)) {
RELEASED.fetch_sub(1, Ordering::SeqCst);
}
}
}
thread_local! {
static HOOK: RefCell<Option<Hook>> = const { RefCell::new(None) };
static RELEASED_HERE: Cell<bool> = const { Cell::new(false) };
}
pub fn run_detachable<R>(on_release: impl FnOnce() + 'static, f: impl FnOnce() -> R) -> (R, bool) {
run_with(Hook::Owned(Box::new(on_release)), f)
}
pub fn run_detachable_ref<R>(on_release: &dyn Fn(), f: impl FnOnce() -> R) -> (R, bool) {
let hook: *const (dyn Fn() + 'static) = unsafe { std::mem::transmute::<*const dyn Fn(), *const (dyn Fn() + 'static)>(on_release) };
run_with(Hook::Borrowed(hook), f)
}
fn run_with<R>(hook: Hook, f: impl FnOnce() -> R) -> (R, bool) {
let mut restore = Restore {
prev_hook: Some(HOOK.with(|h| h.borrow_mut().replace(hook))),
prev_released: RELEASED_HERE.with(|r| r.replace(false)),
};
let result = f();
let released = RELEASED_HERE.with(|r| r.get());
let prev = restore.prev_hook.take().flatten();
HOOK.with(|h| *h.borrow_mut() = prev);
RELEASED_HERE.with(|r| r.set(restore.prev_released));
std::mem::forget(restore);
if released {
RELEASED.fetch_sub(1, Ordering::SeqCst);
}
(result, released)
}
pub fn can_release() -> bool {
RELEASED_HERE.with(|r| r.get()) || HOOK.with(|h| h.borrow().is_some())
}
pub fn release_consumer() -> bool {
if RELEASED_HERE.with(|r| r.get()) {
return true;
}
let Some(hook) = HOOK.with(|h| h.borrow_mut().take()) else {
return false;
};
if RELEASED.fetch_add(1, Ordering::SeqCst) >= MAX_RELEASED_THREADS {
RELEASED.fetch_sub(1, Ordering::SeqCst);
HOOK.with(|h| *h.borrow_mut() = Some(hook));
return false;
}
RELEASED_HERE.with(|r| r.set(true));
hook.call();
true
}
pub fn async_sleep(d: Duration) -> bool {
let released = release_consumer();
if !d.is_zero() {
std::thread::sleep(d);
}
released
}
pub fn released_threads() -> usize {
RELEASED.load(Ordering::SeqCst)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn a_borrowed_hook_releases_once_and_is_removed_afterwards() {
let calls = Cell::new(0);
let hook = || calls.set(calls.get() + 1);
let ((), released) = run_detachable_ref(&hook, || {
assert!(can_release());
assert!(async_sleep(Duration::ZERO));
assert!(async_sleep(Duration::ZERO));
});
assert!(released);
assert_eq!(calls.get(), 1);
assert!(!can_release());
let ((), released) = run_detachable_ref(&hook, || ());
assert!(!released);
assert_eq!(calls.get(), 1);
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_detachable_ref(&hook, || panic!("boom"))));
assert!(r.is_err());
assert!(!can_release());
assert!(!release_consumer());
}
#[test]
fn release_runs_hook_once() {
let calls = Arc::new(AtomicUsize::new(0));
let c = calls.clone();
let (v, released) = run_detachable(
move || {
c.fetch_add(1, Ordering::SeqCst);
},
|| {
assert!(can_release());
assert!(async_sleep(Duration::ZERO));
assert!(async_sleep(Duration::from_millis(1)));
42
},
);
assert_eq!(v, 42);
assert!(released);
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert!(!can_release());
assert!(!async_sleep(Duration::ZERO));
let (_, released) = run_detachable(|| {}, || ());
assert!(!released);
}
}