Skip to content

Commit 77e8151

Browse files
committed
std: use block_current_task for thread parking on Hermit
1 parent 2f320a2 commit 77e8151

File tree

6 files changed

+141
-6
lines changed

6 files changed

+141
-6
lines changed

Cargo.lock

+4-3
Original file line numberDiff line numberDiff line change
@@ -1753,12 +1753,13 @@ dependencies = [
17531753

17541754
[[package]]
17551755
name = "hermit-abi"
1756-
version = "0.2.0"
1756+
version = "0.2.3"
17571757
source = "registry+https://github.com/rust-lang/crates.io-index"
1758-
checksum = "1ab7905ea95c6d9af62940f9d7dd9596d54c334ae2c15300c482051292d5637f"
1758+
checksum = "d37fb7dc756218a0559bfc21e4381f03cbb696cdaf959e7e95e927496f0564cd"
17591759
dependencies = [
17601760
"compiler_builtins",
17611761
"libc",
1762+
"rustc-std-workspace-alloc",
17621763
"rustc-std-workspace-core",
17631764
]
17641765

@@ -5048,7 +5049,7 @@ dependencies = [
50485049
"dlmalloc",
50495050
"fortanix-sgx-abi",
50505051
"hashbrown",
5051-
"hermit-abi 0.2.0",
5052+
"hermit-abi 0.2.3",
50525053
"libc",
50535054
"miniz_oxide 0.4.0",
50545055
"object 0.26.2",

library/std/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ dlmalloc = { version = "0.2.3", features = ['rustc-dep-of-std'] }
4242
fortanix-sgx-abi = { version = "0.3.2", features = ['rustc-dep-of-std'] }
4343

4444
[target.'cfg(target_os = "hermit")'.dependencies]
45-
hermit-abi = { version = "0.2.0", features = ['rustc-dep-of-std'] }
45+
hermit-abi = { version = "0.2.3", features = ['rustc-dep-of-std'] }
4646

4747
[target.wasm32-wasi.dependencies]
4848
wasi = { version = "0.11.0", features = ['rustc-dep-of-std'], default-features = false }

library/std/src/sys/hermit/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub mod thread;
4343
pub mod thread_local_dtor;
4444
#[path = "../unsupported/thread_local_key.rs"]
4545
pub mod thread_local_key;
46+
pub mod thread_parker;
4647
pub mod time;
4748

4849
mod condvar;

library/std/src/sys/hermit/mutex.rs

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use crate::cell::UnsafeCell;
22
use crate::collections::VecDeque;
33
use crate::hint;
44
use crate::ops::{Deref, DerefMut, Drop};
5-
use crate::ptr;
65
use crate::sync::atomic::{AtomicUsize, Ordering};
76
use crate::sys::hermit::abi;
87

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Hermit has efficient thread parking primitives in the form of the `block_current_task`/
2+
// `wakeup_task` syscalls. `block_current_task` marks the current thread as blocked, which
3+
// means the scheduler will not try to reschedule the task once it is switched away from.
4+
// `wakeup_task` undoes this. Since Hermit is not pre-emptive, this means programs get to
5+
// run code in between calling `block_current_task` and actually waiting (with a call to
6+
// `yield_now`) without encountering any race conditions.
7+
//
8+
// The thread parker uses an atomic variable which is set one of three states:
9+
// * EMPTY: the token has not been made available
10+
// * NOTIFIED: the token is available
11+
// * some pid: the thread with the specified PID is waiting or about to be waiting for
12+
// the token
13+
// Since `wakeup_task` requires the task to be actually waiting, the state needs to
14+
// be checked in between preparing to park and actually parking to avoid deadlocks.
15+
// If the state has changed, the thread resets its own state by calling `wakeup_task`.
16+
17+
use super::abi;
18+
use crate::pin::Pin;
19+
use crate::sync::atomic::AtomicU64;
20+
use crate::sync::atomic::Ordering::{Acquire, Relaxed, Release};
21+
use crate::time::{Duration, Instant};
22+
23+
// These values are larger than u32::MAX, so they never conflict with the thread's `pid`.
24+
const EMPTY: u64 = 0x1_0000_0000;
25+
const NOTIFIED: u64 = 0x2_0000_0000;
26+
27+
// Note about atomic memory orderings:
28+
// Acquire ordering is necessary to obtain the token, as otherwise the parked thread
29+
// could perform memory operations visible before it was unparked. Since once set,
30+
// the token cannot be unset by other threads, the state can be reset with a relaxed
31+
// store once it has been read with acquire ordering.
32+
pub struct Parker {
33+
state: AtomicU64,
34+
}
35+
36+
impl Parker {
37+
/// Construct the thread parker. The UNIX parker implementation
38+
/// requires this to happen in-place.
39+
pub unsafe fn new(parker: *mut Parker) {
40+
parker.write(Parker { state: AtomicU64::new(EMPTY) })
41+
}
42+
43+
// This implementation doesn't require `unsafe` and `Pin`, but other implementations do.
44+
pub unsafe fn park(self: Pin<&Self>) {
45+
if self.state.load(Acquire) == NOTIFIED {
46+
self.state.store(EMPTY, Relaxed);
47+
return;
48+
}
49+
50+
let pid = abi::getpid();
51+
abi::block_current_task();
52+
if self.state.compare_exchange(EMPTY, pid as u64, Acquire, Acquire).is_ok() {
53+
// Loop to avoid spurious wakeups.
54+
loop {
55+
abi::yield_now();
56+
57+
if self.state.load(Acquire) == NOTIFIED {
58+
break;
59+
}
60+
61+
abi::block_current_task();
62+
63+
if self.state.load(Acquire) == NOTIFIED {
64+
abi::wakeup_task(pid);
65+
break;
66+
}
67+
}
68+
} else {
69+
abi::wakeup_task(pid);
70+
}
71+
72+
self.state.store(EMPTY, Relaxed);
73+
}
74+
75+
// This implementation doesn't require `unsafe` and `Pin`, but other implementations do.
76+
pub unsafe fn park_timeout(self: Pin<&Self>, dur: Duration) {
77+
if self.state.load(Acquire) == NOTIFIED {
78+
self.state.store(EMPTY, Relaxed);
79+
return;
80+
}
81+
82+
if dur < Duration::from_millis(1) {
83+
// Spin on the token for sub-millisecond parking.
84+
let now = Instant::now();
85+
let Some(deadline) = now.checked_add(dur) else { return; };
86+
loop {
87+
abi::yield_now();
88+
89+
if self.state.load(Acquire) == NOTIFIED {
90+
self.state.store(EMPTY, Relaxed);
91+
return;
92+
} else if Instant::now() >= deadline {
93+
// Swap to provide acquire ordering even if the timeout occurred
94+
// before the token was set.
95+
self.state.swap(EMPTY, Acquire);
96+
return;
97+
}
98+
}
99+
} else {
100+
let timeout = dur.as_millis().try_into().unwrap_or(u64::MAX);
101+
let pid = abi::getpid();
102+
abi::block_current_task_with_timeout(timeout);
103+
if self.state.compare_exchange(EMPTY, pid as u64, Acquire, Acquire).is_ok() {
104+
abi::yield_now();
105+
106+
// Swap to provide acquire ordering even if the timeout occurred
107+
// before the token was set. This situation can result in spurious
108+
// wakeups on the next call to `park_timeout`, but it is better to let
109+
// those be handled by the user rather than to do some perhaps unnecessary,
110+
// but always expensive guarding.
111+
self.state.swap(EMPTY, Acquire);
112+
} else {
113+
abi::wakeup_task(pid);
114+
self.state.store(EMPTY, Relaxed);
115+
}
116+
}
117+
}
118+
119+
// This implementation doesn't require `Pin`, but other implementations do.
120+
pub fn unpark(self: Pin<&Self>) {
121+
// Use release ordering to synchonize with the parked thread.
122+
let state = self.state.swap(NOTIFIED, Release);
123+
124+
if !matches!(state, EMPTY | NOTIFIED) {
125+
// SAFETY: `wakeup_task` is marked unsafe, but is actually safe to use
126+
unsafe {
127+
let pid = state as u32;
128+
// This is a noop if the task is not blocked or has terminated, but
129+
// that is fine.
130+
abi::wakeup_task(pid);
131+
}
132+
}
133+
}
134+
}

library/std/src/sys_common/thread_parker/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ cfg_if::cfg_if! {
1313
} else if #[cfg(target_os = "solid_asp3")] {
1414
mod wait_flag;
1515
pub use wait_flag::Parker;
16-
} else if #[cfg(any(windows, target_family = "unix"))] {
16+
} else if #[cfg(any(windows, target_family = "unix", target_os = "hermit"))] {
1717
pub use crate::sys::thread_parker::Parker;
1818
} else {
1919
mod generic;

0 commit comments

Comments
 (0)