use core::marker::PhantomData;
use core::ptr::{self, NonNull};
use super::task::Runnable;
use crate::sync::internal::{AtomicBool, AtomicPtr, Ordering, UnsafeCell};
pub(crate) unsafe trait Linked {
fn into_link(self) -> NonNull<Link>;
unsafe fn from_link(link: NonNull<Link>) -> Self;
}
unsafe impl Linked for Runnable {
fn into_link(self) -> NonNull<Link> {
Runnable::into_link(self)
}
unsafe fn from_link(link: NonNull<Link>) -> Self {
unsafe { Runnable::from_link(link) }
}
}
pub(crate) struct Link {
next: AtomicPtr<Link>,
}
impl Link {
pub(crate) fn new() -> Self {
Self {
next: AtomicPtr::new(ptr::null_mut()),
}
}
}
struct Stub(NonNull<Link>);
impl Stub {
fn new() -> Self {
Self(NonNull::from(Box::leak(Box::new(Link::new()))))
}
fn as_ptr(&self) -> *mut Link {
self.0.as_ptr()
}
}
impl Drop for Stub {
fn drop(&mut self) {
drop(unsafe { Box::from_raw(self.0.as_ptr()) });
}
}
pub(crate) enum Pop<T = Runnable> {
Item(T),
Empty,
Busy,
}
pub(crate) struct Inbox<T: Linked = Runnable> {
head: AtomicPtr<Link>,
tail: UnsafeCell<*mut Link>,
stub: Stub,
taken: AtomicBool,
items: PhantomData<T>,
}
unsafe impl<T: Linked> Send for Inbox<T> {}
unsafe impl<T: Linked> Sync for Inbox<T> {}
impl<T: Linked> Inbox<T> {
pub(crate) fn new() -> Self {
let stub = Stub::new();
let at = stub.as_ptr();
Self {
head: AtomicPtr::new(at),
tail: UnsafeCell::new(at),
stub,
taken: AtomicBool::new(true),
items: PhantomData,
}
}
fn stub(&self) -> *mut Link {
self.stub.as_ptr()
}
pub(crate) fn push(&self, item: T) {
self.push_link(item.into_link());
}
fn push_link(&self, link: NonNull<Link>) {
unsafe { link.as_ref() }
.next
.store(ptr::null_mut(), Ordering::Relaxed);
let prev = self.head.swap(link.as_ptr(), Ordering::SeqCst);
unsafe { (*prev).next.store(link.as_ptr(), Ordering::Release) };
}
pub(crate) fn is_empty(&self) -> bool {
self.head.load(Ordering::SeqCst) == self.stub()
}
pub(crate) unsafe fn is_drained(&self) -> bool {
let tail = self.tail.with(|tail| unsafe { *tail });
tail == self.stub() && self.is_empty()
}
pub(crate) unsafe fn pop(&self) -> Pop<T> {
let stub = self.stub();
self.tail.with_mut(|tail| unsafe {
let mut cur = *tail;
let mut next = (*cur).next.load(Ordering::Acquire);
if cur == stub {
if next.is_null() {
return if self.is_empty() {
Pop::Empty
} else {
Pop::Busy
};
}
*tail = next;
cur = next;
next = (*cur).next.load(Ordering::Acquire);
}
if !next.is_null() {
*tail = next;
return Pop::Item(T::from_link(NonNull::new_unchecked(cur)));
}
if cur != self.head.load(Ordering::Acquire) {
return Pop::Busy;
}
self.push_link(NonNull::new_unchecked(stub));
next = (*cur).next.load(Ordering::Acquire);
if next.is_null() {
return Pop::Busy;
}
*tail = next;
Pop::Item(T::from_link(NonNull::new_unchecked(cur)))
})
}
pub(crate) fn is_idle(&self) -> bool {
!self.taken.load(Ordering::SeqCst) && self.is_empty()
}
pub(crate) fn try_take(&self) -> bool {
self.taken
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
}
pub(crate) unsafe fn release(&self) {
self.taken.store(false, Ordering::SeqCst);
}
pub(crate) unsafe fn drain(&self, mut each: impl FnMut(T)) {
let mut spins = 0u32;
loop {
match unsafe { self.pop() } {
Pop::Item(item) => each(item),
Pop::Empty => return,
Pop::Busy => {
spins += 1;
if spins.is_multiple_of(64) {
std::thread::yield_now();
} else {
core::hint::spin_loop();
}
}
}
}
}
}
impl<T: Linked> Drop for Inbox<T> {
fn drop(&mut self) {
unsafe { self.drain(drop) };
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use std::sync::{Arc, Mutex};
use super::*;
use crate::rt::task::{Schedule, ScheduleInfo};
struct Ignore;
unsafe impl Schedule for Ignore {
fn schedule(&self, runnable: Runnable, _info: ScheduleInfo) {
drop(runnable);
}
}
type Log = Arc<Mutex<Vec<u32>>>;
fn task(log: &Log, n: u32) -> Runnable {
let log = Arc::clone(log);
let (runnable, task) =
crate::rt::task::spawn(async move { log.lock().unwrap().push(n) }, Ignore);
task.detach();
runnable
}
fn take(inbox: &Inbox) -> Option<Runnable> {
match unsafe { inbox.pop() } {
Pop::Item(r) => Some(r),
Pop::Empty => None,
Pop::Busy => panic!("no push is under way"),
}
}
#[test]
fn tasks_come_out_in_the_order_they_went_in() {
let log = Log::default();
let inbox: Inbox = Inbox::new();
assert!(inbox.is_empty());
inbox.push(task(&log, 1));
assert!(!inbox.is_empty());
inbox.push(task(&log, 2));
inbox.push(task(&log, 3));
while let Some(r) = take(&inbox) {
r.run();
}
assert_eq!(*log.lock().unwrap(), [1, 2, 3]);
assert!(inbox.is_empty());
let mut again = task(&log, 4);
for _ in 0..3 {
inbox.push(again);
again = take(&inbox).expect("the task came back");
assert!(take(&inbox).is_none());
}
inbox.push(task(&log, 5));
inbox.push(again);
while let Some(r) = take(&inbox) {
r.run();
}
assert_eq!(*log.lock().unwrap(), [1, 2, 3, 5, 4]);
}
#[test]
fn a_push_racing_the_stub_put_back_is_seen_by_the_taker() {
let log = Log::default();
let inbox: Inbox = Inbox::new();
let stub = inbox.stub();
inbox.push(task(&log, 1));
unsafe {
let last = inbox.head.load(Ordering::SeqCst);
inbox.tail.with_mut(|tail| *tail = last);
let pushed = task(&log, 2).into_link();
pushed
.as_ref()
.next
.store(ptr::null_mut(), Ordering::Relaxed);
let prev = inbox.head.swap(pushed.as_ptr(), Ordering::SeqCst);
assert_eq!(prev, last);
inbox.push_link(NonNull::new_unchecked(stub));
assert!((*last).next.load(Ordering::Acquire).is_null());
let looks = (inbox.is_empty(), inbox.is_drained());
(*prev).next.store(pushed.as_ptr(), Ordering::Release);
assert!(looks.0, "the head is the stub");
assert!(!looks.1, "the taker sees the two tasks behind its end");
}
let mut taken = 0;
while let Some(r) = take(&inbox) {
r.run();
taken += 1;
}
assert_eq!(taken, 2);
assert_eq!(*log.lock().unwrap(), [1, 2]);
assert!(unsafe { inbox.is_drained() });
}
#[test]
fn the_role_goes_to_one_taker_at_a_time() {
let inbox: Inbox = Inbox::new();
assert!(!inbox.try_take(), "its maker holds it");
unsafe { inbox.release() };
assert!(inbox.try_take());
assert!(!inbox.try_take());
unsafe { inbox.release() };
}
#[test]
fn a_dropped_inbox_drops_what_it_holds() {
let log = Log::default();
let inbox: Inbox = Inbox::new();
inbox.push(task(&log, 1));
inbox.push(task(&log, 2));
assert_eq!(Arc::strong_count(&log), 3);
drop(inbox);
assert_eq!(
Arc::strong_count(&log),
1,
"both tasks dropped their futures"
);
}
#[repr(C)]
struct Panics {
link: Link,
}
impl Drop for Panics {
fn drop(&mut self) {
panic!("the item's drop panics");
}
}
unsafe impl Linked for Box<Panics> {
fn into_link(self) -> NonNull<Link> {
NonNull::from(Box::leak(self)).cast()
}
unsafe fn from_link(link: NonNull<Link>) -> Self {
unsafe { Box::from_raw(link.cast::<Panics>().as_ptr()) }
}
}
#[test]
fn a_panic_dropping_an_item_still_frees_the_stub() {
let inbox: Inbox<Box<Panics>> = Inbox::new();
inbox.push(Box::new(Panics { link: Link::new() }));
let dropped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(inbox)));
assert!(
dropped.is_err(),
"the item's panic went out of the inbox's drop"
);
}
#[test]
fn pushes_from_threads_all_come_out_in_each_threads_order() {
const THREADS: u32 = 4;
const EACH: u32 = if cfg!(miri) { 40 } else { 2_000 };
let log = Log::default();
let inbox: Arc<Inbox> = Arc::new(Inbox::new());
let pushers: Vec<_> = (0..THREADS)
.map(|t| {
let (inbox, log) = (Arc::clone(&inbox), Arc::clone(&log));
std::thread::spawn(move || {
for i in 0..EACH {
inbox.push(task(&log, t * EACH + i));
}
})
})
.collect();
let mut got = 0;
while got < THREADS * EACH {
match unsafe { inbox.pop() } {
Pop::Item(r) => {
r.run();
got += 1;
}
Pop::Empty | Pop::Busy => std::thread::yield_now(),
}
}
for p in pushers {
p.join().unwrap();
}
assert!(inbox.is_empty());
let log = log.lock().unwrap();
for t in 0..THREADS {
let mine: Vec<u32> = log.iter().copied().filter(|n| n / EACH == t).collect();
assert!(
mine.windows(2).all(|w| w[0] < w[1]),
"thread {t}'s tasks came out of order"
);
assert_eq!(mine.len(), EACH as usize);
}
}
}