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