Skip to main content

ax_task/thread/handle/
wake_batch.rs

1//! Allocation-free task-context wake batching.
2
3use alloc::{rc::Rc, sync::Arc};
4use core::{marker::PhantomData, mem::ManuallyDrop, ptr, sync::atomic::Ordering};
5
6use super::{ThreadCore, ThreadWakeHandle};
7
8/// A task-context wake list backed by nodes embedded in each thread core.
9///
10/// Queue owners add handles while holding their own metadata lock, release
11/// that lock, and then call [`Self::wake_all`]. This is the same ownership
12/// split as Linux `wake_q`: unlink and select atomically under the domain lock,
13/// perform scheduler wakes afterwards, and never allocate in between.
14///
15/// A thread can occur at most once in all live batches. Duplicate insertion is
16/// coalesced and returns `false`. The batch is deliberately neither `Send` nor
17/// `Sync`; the task context that selected the waiters must drain it.
18#[must_use = "selected threads must be woken after releasing the domain lock"]
19pub struct ThreadWakeBatch {
20    head: *const ThreadCore,
21    tail: *const ThreadCore,
22    len: usize,
23    _task_context: PhantomData<Rc<()>>,
24}
25
26impl ThreadWakeBatch {
27    /// Creates an empty batch without allocating.
28    pub const fn new() -> Self {
29        Self {
30            head: ptr::null(),
31            tail: ptr::null(),
32            len: 0,
33            _task_context: PhantomData,
34        }
35    }
36
37    /// Adds a wake handle, returning `false` when this thread is already in a
38    /// live batch.
39    pub fn push(&mut self, wake: ThreadWakeHandle) -> bool {
40        let core = &wake.core;
41        if core
42            .wake_batch_linked
43            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
44            .is_err()
45        {
46            return false;
47        }
48
49        let raw = Self::into_raw(wake);
50        unsafe {
51            // SAFETY: `raw` owns one strong reference transferred from `wake`.
52            // The successful linked transition gives this batch exclusive
53            // access to the embedded link until `pop` clears it.
54            (*raw)
55                .wake_batch_next
56                .store(ptr::null_mut(), Ordering::Relaxed);
57            if self.tail.is_null() {
58                self.head = raw;
59            } else {
60                (*self.tail)
61                    .wake_batch_next
62                    .store(raw.cast_mut(), Ordering::Release);
63            }
64        }
65        self.tail = raw;
66        self.len += 1;
67        true
68    }
69
70    /// Returns the number of unique threads selected by this batch.
71    pub const fn len(&self) -> usize {
72        self.len
73    }
74
75    /// Returns whether no thread has been selected.
76    pub const fn is_empty(&self) -> bool {
77        self.len == 0
78    }
79
80    /// Wakes all selected threads in FIFO order.
81    pub fn wake_all(mut self) -> usize {
82        let count = self.len;
83        while let Some(wake) = self.pop() {
84            let _result = wake.wake();
85        }
86        count
87    }
88
89    fn into_raw(wake: ThreadWakeHandle) -> *const ThreadCore {
90        let mut wake = ManuallyDrop::new(wake);
91        let core = unsafe {
92            // SAFETY: `wake` will not run Drop. Its core ownership is moved to
93            // the returned raw Arc and reconstructed exactly once by `pop`.
94            ManuallyDrop::take(&mut wake.core)
95        };
96        let reap_signal = unsafe {
97            // SAFETY: identical ownership transfer for the auxiliary Arc. The
98            // external lease remains owned by the batch and is released when
99            // the reconstructed handle is dropped.
100            ptr::read(&wake.reap_signal)
101        };
102        drop(reap_signal);
103        Arc::into_raw(core)
104    }
105
106    unsafe fn from_raw(raw: *const ThreadCore) -> ThreadWakeHandle {
107        let core = unsafe {
108            // SAFETY: every pointer placed in the batch came from one
109            // `Arc::into_raw`, and `pop` removes it exactly once.
110            Arc::from_raw(raw)
111        };
112        let reap_signal = Arc::clone(&core.reap_signal);
113        ThreadWakeHandle {
114            core: ManuallyDrop::new(core),
115            reap_signal,
116        }
117    }
118
119    fn pop(&mut self) -> Option<ThreadWakeHandle> {
120        let raw = self.head;
121        if raw.is_null() {
122            return None;
123        }
124
125        let next = unsafe {
126            // SAFETY: the raw Arc keeps the core alive and this batch has
127            // exclusive ownership of its embedded link.
128            (*raw).wake_batch_next.load(Ordering::Acquire).cast_const()
129        };
130        self.head = next;
131        if next.is_null() {
132            self.tail = ptr::null();
133        }
134        self.len -= 1;
135        unsafe {
136            // SAFETY: clearing the link completes this batch's exclusive node
137            // ownership before reconstructing the owning wake handle.
138            (*raw)
139                .wake_batch_next
140                .store(ptr::null_mut(), Ordering::Relaxed);
141            (*raw).wake_batch_linked.store(false, Ordering::Release);
142            Some(Self::from_raw(raw))
143        }
144    }
145}
146
147impl Default for ThreadWakeBatch {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl Drop for ThreadWakeBatch {
154    fn drop(&mut self) {
155        // Dropping an undrained batch is a caller bug, but ownership must still
156        // be released without invoking scheduler callbacks from an unknown
157        // lock context.
158        let was_empty = self.is_empty();
159        while let Some(wake) = self.pop() {
160            drop(wake);
161        }
162        debug_assert!(was_empty, "thread wake batch was not drained");
163    }
164}