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 drop(reap_signal);
115 Arc::into_raw(core)
116 }
117
118 unsafe fn from_raw(raw: *const ThreadCore) -> ThreadWakeHandle {
119 let core = unsafe {
120 // SAFETY: every pointer placed in the batch came from one
121 // `Arc::into_raw`, and `pop` removes it exactly once.
122 Arc::from_raw(raw)
123 };
124 let reap_signal = Arc::clone(&core.reap_signal);
125 ThreadWakeHandle {
126 core: ManuallyDrop::new(core),
127 reap_signal,
128 }
129 }
130
131 fn pop(&mut self) -> Option<ThreadWakeHandle> {
132 let raw = self.head;
133 if raw.is_null() {
134 return None;
135 }
136
137 let next = unsafe {
138 // SAFETY: the raw Arc keeps the core alive and this batch has
139 // exclusive ownership of its embedded link.
140 (*raw).wake_batch_next.load(Ordering::Acquire).cast_const()
141 };
142 self.head = next;
143 if next.is_null() {
144 self.tail = ptr::null();
145 }
146 self.len -= 1;
147 unsafe {
148 // SAFETY: clearing the link completes this batch's exclusive node
149 // ownership before reconstructing the owning wake handle.
150 (*raw)
151 .wake_batch_next
152 .store(ptr::null_mut(), Ordering::Relaxed);
153 (*raw).wake_batch_linked.store(false, Ordering::Release);
154 Some(Self::from_raw(raw))
155 }
156 }
157}
158
159impl Default for ThreadWakeBatch {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165impl Drop for ThreadWakeBatch {
166 fn drop(&mut self) {
167 // Dropping an undrained batch is a caller bug, but ownership must still
168 // be released without invoking scheduler callbacks from an unknown
169 // lock context.
170 let was_empty = self.is_empty();
171 while let Some(wake) = self.pop() {
172 drop(wake);
173 }
174 debug_assert!(was_empty, "thread wake batch was not drained");
175 }
176}