Skip to main content

ax_task/thread/
state.rs

1//! Checked thread lifecycle transitions.
2
3use core::sync::atomic::{AtomicU16, Ordering};
4
5use crate::thread::{TaskError, ThreadState};
6
7#[path = "state/rt_lock.rs"]
8mod rt_lock;
9
10const STATE_MASK: u16 = 0b111;
11const WAKE_PENDING: u16 = 1 << 3;
12const PARK_NOTIFIED: u16 = 1 << 4;
13const WAKE_STATE_PUBLISHED: u16 = WAKE_PENDING | PARK_NOTIFIED;
14const RTLOCK_ACTIVE: u16 = 1 << 5;
15const SAVED_SHIFT: u32 = 8;
16
17const fn publish_ordinary_notification(observed: u16) -> u16 {
18    let bits = if observed & RTLOCK_ACTIVE != 0 {
19        WAKE_STATE_PUBLISHED << SAVED_SHIFT
20    } else {
21        WAKE_STATE_PUBLISHED
22    };
23    observed | bits
24}
25
26const fn overlay_rt_lock_state(observed: u16) -> u16 {
27    RTLOCK_ACTIVE | (observed << SAVED_SHIFT) | ThreadState::Running as u16
28}
29
30/// Single atomic publication for task lifecycle and wake/schedule races.
31#[derive(Debug)]
32pub(crate) struct ThreadLifecycle {
33    state: AtomicU16,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub(crate) struct WakePublication {
38    state: ThreadState,
39    already_pending: bool,
40    saved_state_only: bool,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub(crate) enum ParkPublication {
45    Notified,
46    Blocked,
47}
48
49impl WakePublication {
50    pub(crate) const fn saved_state_only(self) -> bool {
51        self.saved_state_only
52    }
53
54    pub(crate) const fn state(self) -> ThreadState {
55        self.state
56    }
57
58    pub(crate) const fn already_pending(self) -> bool {
59        self.already_pending
60    }
61}
62
63impl ThreadLifecycle {
64    pub(crate) const fn new() -> Self {
65        Self {
66            state: AtomicU16::new(ThreadState::New as u16),
67        }
68    }
69
70    #[track_caller]
71    pub(crate) fn state(&self) -> ThreadState {
72        decode_state(self.state.load(Ordering::Acquire))
73    }
74
75    #[track_caller]
76    pub(crate) fn transition(&self, next: ThreadState) -> Result<(), TaskError> {
77        let mut observed = self.state.load(Ordering::Acquire);
78        loop {
79            let current = decode_state(observed);
80            if !transition_is_valid(current, next) {
81                return Err(TaskError::InvalidTransition {
82                    from: current,
83                    to: next,
84                });
85            }
86            // Admission is the boundary between pre-publication notifications
87            // and runnable-thread wakes. Clear the former in this same CAS for
88            // every New transition, including bootstrap. A racing wake that
89            // observes Running publishes after this CAS and remains pending.
90            let retained = if current == ThreadState::New {
91                0
92            } else {
93                observed & !STATE_MASK
94            };
95            let updated = retained | next as u16;
96            match self.state.compare_exchange_weak(
97                observed,
98                updated,
99                Ordering::AcqRel,
100                Ordering::Acquire,
101            ) {
102                Ok(_) => return Ok(()),
103                Err(updated) => observed = updated,
104            }
105        }
106    }
107
108    #[track_caller]
109    pub(crate) fn publish_wake(&self) -> WakePublication {
110        // One CAS owns the choice of active versus saved notification bits.
111        // A separate flag load followed by fetch_or would race RT-lock entry.
112        let previous = self
113            .state
114            .try_update(Ordering::AcqRel, Ordering::Acquire, |observed| {
115                Some(publish_ordinary_notification(observed))
116            })
117            .expect("wake publication cannot be rejected");
118        let saved_state_only = previous & RTLOCK_ACTIVE != 0;
119        let notification = if saved_state_only {
120            previous >> SAVED_SHIFT
121        } else {
122            previous
123        };
124        WakePublication {
125            state: decode_state(previous),
126            already_pending: notification & WAKE_PENDING != 0,
127            saved_state_only,
128        }
129    }
130
131    #[cfg(test)]
132    pub(crate) fn consume_wake(&self, preserve_park_notification: bool) -> bool {
133        let consumed = if preserve_park_notification {
134            WAKE_PENDING
135        } else {
136            WAKE_STATE_PUBLISHED
137        };
138        self.state.fetch_and(!consumed, Ordering::AcqRel) & WAKE_PENDING != 0
139    }
140
141    pub(crate) fn discard_failed_wake(&self) {
142        self.state
143            .fetch_and(!WAKE_STATE_PUBLISHED, Ordering::AcqRel);
144    }
145
146    pub(crate) fn take_park_notification(&self) -> bool {
147        self.state
148            .fetch_and(!WAKE_STATE_PUBLISHED, Ordering::AcqRel)
149            & PARK_NOTIFIED
150            != 0
151    }
152
153    /// Consumes one wake publication and optionally advances a blocked task in
154    /// the same lifecycle CAS. The task lock serializes scheduler ownership,
155    /// while this single atomic update closes the remaining park/wake race
156    /// without publishing an intermediate `Blocked` observation.
157    pub(crate) fn consume_wake_and_transition(
158        &self,
159        preserve_park_notification: bool,
160        next: Option<ThreadState>,
161    ) -> (ThreadState, bool) {
162        let mut observed = self.state.load(Ordering::Acquire);
163        loop {
164            let current = decode_state(observed);
165            let pending = observed & WAKE_PENDING != 0;
166            let consumed = if preserve_park_notification && current == ThreadState::Parking {
167                WAKE_PENDING
168            } else {
169                WAKE_STATE_PUBLISHED
170            };
171            let mut updated = observed & !consumed;
172            if pending && next == Some(ThreadState::Waking) && current == ThreadState::Blocked {
173                updated = (updated & !STATE_MASK) | ThreadState::Waking as u16;
174            }
175            if updated == observed {
176                return (current, pending);
177            }
178            match self.state.compare_exchange_weak(
179                observed,
180                updated,
181                Ordering::AcqRel,
182                Ordering::Acquire,
183            ) {
184                Ok(_) => return (current, pending),
185                Err(next_observed) => observed = next_observed,
186            }
187        }
188    }
189
190    /// Atomically chooses between a racing wake and blocked publication.
191    #[track_caller]
192    pub(crate) fn publish_blocked_from_parking(&self) -> Result<ParkPublication, TaskError> {
193        let mut observed = self.state.load(Ordering::Acquire);
194        loop {
195            let current = decode_state(observed);
196            if current != ThreadState::Parking {
197                return Err(TaskError::InvalidTransition {
198                    from: current,
199                    to: ThreadState::Blocked,
200                });
201            }
202            let (updated, publication) = if observed & PARK_NOTIFIED != 0 {
203                (
204                    (observed & !(STATE_MASK | WAKE_STATE_PUBLISHED)) | ThreadState::Running as u16,
205                    ParkPublication::Notified,
206                )
207            } else {
208                (
209                    (observed & !(STATE_MASK | WAKE_STATE_PUBLISHED)) | ThreadState::Blocked as u16,
210                    ParkPublication::Blocked,
211                )
212            };
213            match self.state.compare_exchange_weak(
214                observed,
215                updated,
216                Ordering::AcqRel,
217                Ordering::Acquire,
218            ) {
219                Ok(_) => return Ok(publication),
220                Err(updated) => observed = updated,
221            }
222        }
223    }
224}
225
226#[track_caller]
227pub(crate) fn decode_state(packed: u16) -> ThreadState {
228    match packed & STATE_MASK {
229        0 => ThreadState::New,
230        2 => ThreadState::Running,
231        3 => ThreadState::Parking,
232        4 => ThreadState::Blocked,
233        5 => ThreadState::Waking,
234        6 => ThreadState::Exited,
235        _ => panic!("invalid thread lifecycle publication: raw={packed:#04x}"),
236    }
237}
238
239pub(crate) const fn transition_is_valid(from: ThreadState, to: ThreadState) -> bool {
240    matches!(
241        (from, to),
242        (ThreadState::New, ThreadState::Running | ThreadState::Exited)
243            | (
244                ThreadState::Running,
245                ThreadState::Parking | ThreadState::Exited
246            )
247            | (
248                ThreadState::Parking,
249                ThreadState::Running | ThreadState::Blocked | ThreadState::Waking
250            )
251            | (
252                ThreadState::Blocked,
253                // Linux `ttwu_runnable()` changes an on-rq sleeper directly
254                // to TASK_RUNNING. Only the off-rq enqueue path uses Waking.
255                ThreadState::Running | ThreadState::Waking | ThreadState::Exited
256            )
257            | (
258                ThreadState::Waking,
259                ThreadState::Running | ThreadState::Exited
260            )
261    )
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn accepts_the_documented_wake_transition() {
270        let lifecycle = ThreadLifecycle::new();
271        lifecycle.transition(ThreadState::Running).unwrap();
272        lifecycle.transition(ThreadState::Parking).unwrap();
273        lifecycle.transition(ThreadState::Waking).unwrap();
274        lifecycle.transition(ThreadState::Running).unwrap();
275        assert_eq!(lifecycle.state(), ThreadState::Running);
276    }
277
278    #[test]
279    fn rejects_runnable_to_blocked_shortcut() {
280        assert!(!transition_is_valid(
281            ThreadState::Running,
282            ThreadState::Blocked
283        ));
284    }
285
286    #[test]
287    fn wake_publication_atomically_defeats_blocked_publication() {
288        let lifecycle = ThreadLifecycle::new();
289        lifecycle.transition(ThreadState::Running).unwrap();
290        lifecycle.transition(ThreadState::Parking).unwrap();
291        assert_eq!(lifecycle.publish_wake().state(), ThreadState::Parking);
292        assert_eq!(
293            lifecycle.publish_blocked_from_parking().unwrap(),
294            ParkPublication::Notified
295        );
296        assert_eq!(lifecycle.state(), ThreadState::Running);
297        assert!(!lifecycle.consume_wake(false));
298    }
299
300    #[test]
301    fn blocked_publication_wins_before_late_wake() {
302        let lifecycle = ThreadLifecycle::new();
303        lifecycle.transition(ThreadState::Running).unwrap();
304        lifecycle.transition(ThreadState::Parking).unwrap();
305        assert_eq!(
306            lifecycle.publish_blocked_from_parking().unwrap(),
307            ParkPublication::Blocked
308        );
309        assert_eq!(lifecycle.publish_wake().state(), ThreadState::Blocked);
310    }
311
312    #[test]
313    fn on_rq_wake_transitions_directly_from_blocked_to_running() {
314        let lifecycle = ThreadLifecycle::new();
315        lifecycle.transition(ThreadState::Running).unwrap();
316        lifecycle.transition(ThreadState::Parking).unwrap();
317        assert_eq!(
318            lifecycle.publish_blocked_from_parking().unwrap(),
319            ParkPublication::Blocked
320        );
321
322        lifecycle.transition(ThreadState::Running).unwrap();
323
324        assert_eq!(lifecycle.state(), ThreadState::Running);
325    }
326
327    #[test]
328    #[should_panic(expected = "invalid thread lifecycle publication: raw=0x0f")]
329    fn invalid_lifecycle_publication_reports_the_packed_byte() {
330        let lifecycle = ThreadLifecycle::new();
331        lifecycle
332            .state
333            .store(WAKE_PENDING | STATE_MASK, Ordering::Relaxed);
334
335        let _ = lifecycle.state();
336    }
337
338    #[test]
339    #[should_panic(expected = "invalid thread lifecycle publication: raw=0x01")]
340    fn reserved_state_encoding_is_rejected() {
341        let lifecycle = ThreadLifecycle::new();
342        lifecycle.state.store(1, Ordering::Relaxed);
343
344        let _ = lifecycle.state();
345    }
346}
347
348#[cfg(all(test, not(miri)))]
349mod loom_tests {
350    use loom::{
351        sync::{
352            Arc,
353            atomic::{AtomicUsize, Ordering},
354        },
355        thread,
356    };
357
358    const STATE_MASK: usize = 0b111;
359    const RUNNING: usize = 2;
360    const PARKING: usize = 3;
361    const BLOCKED: usize = 4;
362    const WAKE_PENDING: usize = 1 << 3;
363    const PARK_NOTIFIED: usize = 1 << 4;
364    const WAKE_STATE_PUBLISHED: usize = WAKE_PENDING | PARK_NOTIFIED;
365
366    #[test]
367    fn wake_publication_cannot_strand_a_parking_thread() {
368        loom::model(|| {
369            let lifecycle = Arc::new(AtomicUsize::new(PARKING));
370
371            let parker = {
372                let lifecycle = Arc::clone(&lifecycle);
373                thread::spawn(move || {
374                    let mut observed = lifecycle.load(Ordering::Acquire);
375                    loop {
376                        assert_eq!(observed & STATE_MASK, PARKING);
377                        let updated = if observed & PARK_NOTIFIED != 0 {
378                            RUNNING
379                        } else {
380                            BLOCKED
381                        };
382                        match lifecycle.compare_exchange_weak(
383                            observed,
384                            updated,
385                            Ordering::AcqRel,
386                            Ordering::Acquire,
387                        ) {
388                            Ok(_) => break,
389                            Err(updated) => observed = updated,
390                        }
391                    }
392                })
393            };
394            let waker = {
395                let lifecycle = Arc::clone(&lifecycle);
396                thread::spawn(move || {
397                    let previous = lifecycle.fetch_or(WAKE_STATE_PUBLISHED, Ordering::AcqRel);
398                    if previous & STATE_MASK == BLOCKED {
399                        let observed = lifecycle.fetch_and(!WAKE_STATE_PUBLISHED, Ordering::AcqRel);
400                        assert_ne!(observed & WAKE_PENDING, 0);
401                        lifecycle
402                            .compare_exchange(BLOCKED, RUNNING, Ordering::AcqRel, Ordering::Acquire)
403                            .unwrap();
404                    }
405                })
406            };
407
408            parker.join().unwrap();
409            waker.join().unwrap();
410            assert_ne!(
411                lifecycle.load(Ordering::Acquire) & STATE_MASK,
412                BLOCKED,
413                "a wake racing Parking-to-Blocked must resume or activate the thread"
414            );
415        });
416    }
417}