Skip to content

Commit a1731ad

Browse files
committed
Adds priority-inheritance futexes for mutexex
This uses FUTEX_LOCK_PI and FUTEX_UNLOCK_PI on Linux.
1 parent 75703c1 commit a1731ad

File tree

4 files changed

+141
-1
lines changed

4 files changed

+141
-1
lines changed

library/std/src/sys/pal/unix/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub mod net;
2323
#[cfg(target_os = "l4re")]
2424
pub use self::l4re::net;
2525
pub mod os;
26+
pub mod pi_futex;
2627
pub mod pipe;
2728
pub mod process;
2829
pub mod stack_overflow;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#![cfg(any(target_os = "linux", target_os = "android"))]
2+
3+
use crate::sync::atomic::AtomicU32;
4+
use crate::sys::cvt;
5+
use crate::{io, ptr};
6+
7+
pub const fn unlocked() -> u32 {
8+
0
9+
}
10+
11+
pub fn locked() -> u32 {
12+
(unsafe { libc::gettid() }) as _
13+
}
14+
15+
pub fn is_contended(futex_val: u32) -> bool {
16+
(futex_val & libc::FUTEX_WAITERS) != 0
17+
}
18+
19+
pub fn is_owned_died(futex_val: u32) -> bool {
20+
(futex_val & libc::FUTEX_OWNER_DIED) != 0
21+
}
22+
23+
pub fn futex_lock(futex: &AtomicU32) -> io::Result<()> {
24+
loop {
25+
match cvt(unsafe {
26+
libc::syscall(
27+
libc::SYS_futex,
28+
ptr::from_ref(futex),
29+
libc::FUTEX_LOCK_PI | libc::FUTEX_PRIVATE_FLAG,
30+
0,
31+
ptr::null::<u32>(),
32+
// remaining args are unused
33+
)
34+
}) {
35+
Ok(_) => return Ok(()),
36+
Err(e) if e.raw_os_error() == Some(libc::EINTR) => continue,
37+
Err(e) => return Err(e),
38+
}
39+
}
40+
}
41+
42+
pub fn futex_unlock(futex: &AtomicU32) -> io::Result<()> {
43+
cvt(unsafe {
44+
libc::syscall(
45+
libc::SYS_futex,
46+
ptr::from_ref(futex),
47+
libc::FUTEX_UNLOCK_PI | libc::FUTEX_PRIVATE_FLAG,
48+
// remaining args are unused
49+
)
50+
})
51+
.map(|_| ())
52+
}

library/std/src/sys/sync/mutex/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
cfg_if::cfg_if! {
22
if #[cfg(any(
3-
all(target_os = "windows", not(target_vendor = "win7")),
43
target_os = "linux",
54
target_os = "android",
5+
))] {
6+
mod pi_futex;
7+
pub use pi_futex::Mutex;
8+
} else if #[cfg(any(
9+
all(target_os = "windows", not(target_vendor = "win7")),
610
target_os = "freebsd",
711
target_os = "openbsd",
812
target_os = "dragonfly",
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use crate::sync::atomic::AtomicU32;
2+
use crate::sync::atomic::Ordering::{Acquire, Relaxed, Release};
3+
use crate::sys::pi_futex as pi;
4+
5+
pub struct Mutex {
6+
futex: AtomicU32,
7+
}
8+
9+
impl Mutex {
10+
#[inline]
11+
pub const fn new() -> Self {
12+
Self { futex: AtomicU32::new(pi::unlocked()) }
13+
}
14+
15+
#[inline]
16+
pub fn try_lock(&self) -> bool {
17+
self.futex.compare_exchange(pi::unlocked(), pi::locked(), Acquire, Relaxed).is_ok()
18+
}
19+
20+
#[inline]
21+
pub fn lock(&self) {
22+
if self.futex.compare_exchange(pi::unlocked(), pi::locked(), Acquire, Relaxed).is_err() {
23+
self.lock_contended();
24+
}
25+
}
26+
27+
#[cold]
28+
fn lock_contended(&self) {
29+
// Spin first to speed things up if the lock is released quickly.
30+
let state = self.spin();
31+
32+
// If it's unlocked now, attempt to take the lock.
33+
if state == pi::unlocked() {
34+
if self.try_lock() {
35+
return;
36+
}
37+
};
38+
39+
pi::futex_lock(&self.futex).expect("failed to lock mutex");
40+
41+
let state = self.futex.load(Relaxed);
42+
if pi::is_owned_died(state) {
43+
panic!(
44+
"failed to lock mutex because the thread owning it finished without unlocking it"
45+
);
46+
}
47+
}
48+
49+
fn spin(&self) -> u32 {
50+
let mut spin = 100;
51+
loop {
52+
// We only use `load` (and not `swap` or `compare_exchange`)
53+
// while spinning, to be easier on the caches.
54+
let state = self.futex.load(Relaxed);
55+
56+
// We stop spinning when the mutex is unlocked,
57+
// but also when it's contended.
58+
if state == pi::unlocked() || pi::is_contended(state) || spin == 0 {
59+
return state;
60+
}
61+
62+
crate::hint::spin_loop();
63+
spin -= 1;
64+
}
65+
}
66+
67+
#[inline]
68+
pub unsafe fn unlock(&self) {
69+
if self.futex.compare_exchange(pi::locked(), pi::unlocked(), Release, Relaxed).is_err() {
70+
// We only wake up one thread. When that thread locks the mutex,
71+
// the kernel will mark the mutex as contended automatically
72+
// (futex != pi::locked() in this case),
73+
// which makes sure that any other waiting threads will also be
74+
// woken up eventually.
75+
self.wake();
76+
}
77+
}
78+
79+
#[cold]
80+
fn wake(&self) {
81+
pi::futex_unlock(&self.futex).unwrap();
82+
}
83+
}

0 commit comments

Comments
 (0)