Skip to content

Commit 20c7b2c

Browse files
committed
Adds priority-inheritance futexes for mutexex
This uses FUTEX_LOCK_PI and FUTEX_UNLOCK_PI on Linux.
1 parent 8d94e06 commit 20c7b2c

File tree

4 files changed

+150
-1
lines changed

4 files changed

+150
-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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#![cfg(any(target_os = "linux", target_os = "android"))]
2+
3+
use crate::sync::atomic::AtomicU32;
4+
use crate::sys::cvt;
5+
use crate::sys::os::errno;
6+
use crate::{io, ptr};
7+
8+
pub enum FutexLockError {
9+
TryAgain,
10+
DeadLock,
11+
Other(io::Error),
12+
}
13+
14+
pub const fn unlocked() -> u32 {
15+
0
16+
}
17+
18+
pub fn locked() -> u32 {
19+
(unsafe { libc::gettid() }) as _
20+
}
21+
22+
pub fn is_contended(futex_val: u32) -> bool {
23+
(futex_val & libc::FUTEX_WAITERS) != 0
24+
}
25+
26+
pub fn futex_lock(futex: &AtomicU32) -> Result<(), FutexLockError> {
27+
if (unsafe {
28+
libc::syscall(
29+
libc::SYS_futex,
30+
futex as *const AtomicU32,
31+
libc::FUTEX_LOCK_PI | libc::FUTEX_PRIVATE_FLAG,
32+
0,
33+
ptr::null::<u32>(),
34+
// remaining args are unused
35+
)
36+
} == -1)
37+
{
38+
Err(match errno() {
39+
libc::EAGAIN | libc::EINTR => FutexLockError::TryAgain,
40+
libc::EDEADLK => FutexLockError::DeadLock,
41+
errno => FutexLockError::Other(io::Error::from_raw_os_error(errno)),
42+
})
43+
} else {
44+
Ok(())
45+
}
46+
}
47+
48+
pub fn futex_unlock(futex: &AtomicU32) -> io::Result<()> {
49+
cvt(unsafe {
50+
libc::syscall(
51+
libc::SYS_futex,
52+
futex as *const AtomicU32,
53+
libc::FUTEX_UNLOCK_PI | libc::FUTEX_PRIVATE_FLAG,
54+
// remaining args are unused
55+
)
56+
})
57+
.map(|_| ())
58+
}

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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use crate::sync::atomic::AtomicU32;
2+
use crate::sync::atomic::Ordering::{Acquire, Relaxed, Release};
3+
use crate::sys::pi_futex as pi;
4+
pub struct Mutex {
5+
futex: AtomicU32,
6+
}
7+
8+
impl Mutex {
9+
#[inline]
10+
pub const fn new() -> Self {
11+
Self { futex: AtomicU32::new(pi::unlocked()) }
12+
}
13+
14+
#[inline]
15+
pub fn try_lock(&self) -> bool {
16+
self.futex.compare_exchange(pi::unlocked(), pi::locked(), Acquire, Relaxed).is_ok()
17+
}
18+
19+
#[inline]
20+
pub fn lock(&self) {
21+
if self.futex.compare_exchange(pi::unlocked(), pi::locked(), Acquire, Relaxed).is_err() {
22+
self.lock_contended();
23+
}
24+
}
25+
26+
#[cold]
27+
fn lock_contended(&self) {
28+
loop {
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+
if let Err(e) = pi::futex_lock(&self.futex) {
40+
const MSG_PREFIX: &str = "failed to lock mutex";
41+
match e {
42+
pi::FutexLockError::TryAgain => (), // Try again in this case.
43+
pi::FutexLockError::DeadLock => panic!("{MSG_PREFIX}: deadlock detected"),
44+
pi::FutexLockError::Other(e) => panic!("{MSG_PREFIX}: {e}"),
45+
}
46+
} else {
47+
return;
48+
}
49+
}
50+
}
51+
52+
fn spin(&self) -> u32 {
53+
let mut spin = 100;
54+
loop {
55+
// We only use `load` (and not `swap` or `compare_exchange`)
56+
// while spinning, to be easier on the caches.
57+
let state = self.futex.load(Relaxed);
58+
59+
// We stop spinning when the mutex is unlocked,
60+
// but also when it's contended.
61+
if state == pi::unlocked() || pi::is_contended(state) || spin == 0 {
62+
return state;
63+
}
64+
65+
crate::hint::spin_loop();
66+
spin -= 1;
67+
}
68+
}
69+
70+
#[inline]
71+
pub unsafe fn unlock(&self) {
72+
if self.futex.compare_exchange(pi::locked(), pi::unlocked(), Release, Relaxed).is_err() {
73+
// We only wake up one thread. When that thread locks the mutex,
74+
// the kernel will mark the mutex as contended automatically
75+
// (futex != pi::locked() in this case),
76+
// which makes sure that any other waiting threads will also be
77+
// woken up eventually.
78+
self.wake();
79+
}
80+
}
81+
82+
#[cold]
83+
fn wake(&self) {
84+
pi::futex_unlock(&self.futex).unwrap();
85+
}
86+
}

0 commit comments

Comments
 (0)