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}