Skip to main content

ax_task/thread/
state.rs

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