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        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}