From 528b37a738abf6cb0166fb60701dc2841ad54ebf Mon Sep 17 00:00:00 2001 From: joboet Date: Mon, 28 Oct 2024 16:56:22 +0100 Subject: std: refactor `pthread`-based synchronization The non-trivial code for `pthread_condvar` is duplicated across the thread parking and the `Mutex`/`Condvar` implementations. This PR moves that code into `sys::pal`, which now exposes a non-movable wrapper type for `pthread_mutex_t` and `pthread_condvar_t`. --- library/std/src/sys/sync/mutex/pthread.rs | 157 ++++++------------------------ library/std/src/sys/sync/mutex/sgx.rs | 4 +- 2 files changed, 32 insertions(+), 129 deletions(-) (limited to 'library/std/src/sys/sync/mutex') diff --git a/library/std/src/sys/sync/mutex/pthread.rs b/library/std/src/sys/sync/mutex/pthread.rs index abd58122523..5719bb10f7f 100644 --- a/library/std/src/sys/sync/mutex/pthread.rs +++ b/library/std/src/sys/sync/mutex/pthread.rs @@ -1,163 +1,66 @@ -use crate::cell::UnsafeCell; -use crate::io::Error; -use crate::mem::{MaybeUninit, forget}; -use crate::sys::cvt_nz; -use crate::sys::sync::OnceBox; +#![forbid(unsafe_op_in_unsafe_fn)] -struct AllocatedMutex(UnsafeCell); +use crate::mem::forget; +use crate::pin::Pin; +use crate::sys::pal::sync as pal; +use crate::sys::sync::OnceBox; pub struct Mutex { - inner: OnceBox, -} - -unsafe impl Send for AllocatedMutex {} -unsafe impl Sync for AllocatedMutex {} - -impl AllocatedMutex { - fn new() -> Box { - let mutex = Box::new(AllocatedMutex(UnsafeCell::new(libc::PTHREAD_MUTEX_INITIALIZER))); - - // Issue #33770 - // - // A pthread mutex initialized with PTHREAD_MUTEX_INITIALIZER will have - // a type of PTHREAD_MUTEX_DEFAULT, which has undefined behavior if you - // try to re-lock it from the same thread when you already hold a lock - // (https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_mutex_init.html). - // This is the case even if PTHREAD_MUTEX_DEFAULT == PTHREAD_MUTEX_NORMAL - // (https://github.com/rust-lang/rust/issues/33770#issuecomment-220847521) -- in that - // case, `pthread_mutexattr_settype(PTHREAD_MUTEX_DEFAULT)` will of course be the same - // as setting it to `PTHREAD_MUTEX_NORMAL`, but not setting any mode will result in - // a Mutex where re-locking is UB. - // - // In practice, glibc takes advantage of this undefined behavior to - // implement hardware lock elision, which uses hardware transactional - // memory to avoid acquiring the lock. While a transaction is in - // progress, the lock appears to be unlocked. This isn't a problem for - // other threads since the transactional memory will abort if a conflict - // is detected, however no abort is generated when re-locking from the - // same thread. - // - // Since locking the same mutex twice will result in two aliasing &mut - // references, we instead create the mutex with type - // PTHREAD_MUTEX_NORMAL which is guaranteed to deadlock if we try to - // re-lock it from the same thread, thus avoiding undefined behavior. - unsafe { - let mut attr = MaybeUninit::::uninit(); - cvt_nz(libc::pthread_mutexattr_init(attr.as_mut_ptr())).unwrap(); - let attr = PthreadMutexAttr(&mut attr); - cvt_nz(libc::pthread_mutexattr_settype( - attr.0.as_mut_ptr(), - libc::PTHREAD_MUTEX_NORMAL, - )) - .unwrap(); - cvt_nz(libc::pthread_mutex_init(mutex.0.get(), attr.0.as_ptr())).unwrap(); - } - - mutex - } -} - -impl Drop for AllocatedMutex { - #[inline] - fn drop(&mut self) { - let r = unsafe { libc::pthread_mutex_destroy(self.0.get()) }; - if cfg!(target_os = "dragonfly") { - // On DragonFly pthread_mutex_destroy() returns EINVAL if called on a - // mutex that was just initialized with libc::PTHREAD_MUTEX_INITIALIZER. - // Once it is used (locked/unlocked) or pthread_mutex_init() is called, - // this behavior no longer occurs. - debug_assert!(r == 0 || r == libc::EINVAL); - } else { - debug_assert_eq!(r, 0); - } - } + pub pal: OnceBox, } impl Mutex { #[inline] pub const fn new() -> Mutex { - Mutex { inner: OnceBox::new() } + Mutex { pal: OnceBox::new() } } - /// Gets access to the pthread mutex under the assumption that the mutex is - /// locked. - /// - /// This allows skipping the initialization check, as the mutex can only be - /// locked if it is already initialized, and allows relaxing the ordering - /// on the pointer load, since the allocation cannot have been modified - /// since the `lock` and the lock must have occurred on the current thread. - /// - /// # Safety - /// Causes undefined behavior if the mutex is not locked. #[inline] - pub(crate) unsafe fn get_assert_locked(&self) -> *mut libc::pthread_mutex_t { - unsafe { self.inner.get_unchecked().0.get() } - } - - #[inline] - fn get(&self) -> *mut libc::pthread_mutex_t { - // If initialization fails, the mutex is destroyed. This is always sound, - // however, as the mutex cannot have been locked yet. - self.inner.get_or_init(AllocatedMutex::new).0.get() + fn get(&self) -> Pin<&pal::Mutex> { + // If the initialization race is lost, the new mutex is destroyed. + // This is sound however, as it cannot have been locked. + self.pal.get_or_init(|| { + let mut pal = Box::pin(pal::Mutex::new()); + // SAFETY: we only call `init` once, namely here. + unsafe { pal.as_mut().init() }; + pal + }) } #[inline] pub fn lock(&self) { - #[cold] - #[inline(never)] - fn fail(r: i32) -> ! { - let error = Error::from_raw_os_error(r); - panic!("failed to lock mutex: {error}"); - } - - let r = unsafe { libc::pthread_mutex_lock(self.get()) }; - // As we set the mutex type to `PTHREAD_MUTEX_NORMAL` above, we expect - // the lock call to never fail. Unfortunately however, some platforms - // (Solaris) do not conform to the standard, and instead always provide - // deadlock detection. How kind of them! Unfortunately that means that - // we need to check the error code here. To save us from UB on other - // less well-behaved platforms in the future, we do it even on "good" - // platforms like macOS. See #120147 for more context. - if r != 0 { - fail(r) - } + // SAFETY: we call `init` above, therefore reentrant locking is safe. + // In `drop` we ensure that the mutex is not destroyed while locked. + unsafe { self.get().lock() } } #[inline] pub unsafe fn unlock(&self) { - let r = libc::pthread_mutex_unlock(self.get_assert_locked()); - debug_assert_eq!(r, 0); + // SAFETY: the mutex can only be locked if it is already initialized + // and we observed this initialization since we observed the locking. + unsafe { self.pal.get_unchecked().unlock() } } #[inline] pub fn try_lock(&self) -> bool { - unsafe { libc::pthread_mutex_trylock(self.get()) == 0 } + // SAFETY: we call `init` above, therefore reentrant locking is safe. + // In `drop` we ensure that the mutex is not destroyed while locked. + unsafe { self.get().try_lock() } } } impl Drop for Mutex { fn drop(&mut self) { - let Some(mutex) = self.inner.take() else { return }; + let Some(pal) = self.pal.take() else { return }; // We're not allowed to pthread_mutex_destroy a locked mutex, // so check first if it's unlocked. - if unsafe { libc::pthread_mutex_trylock(mutex.0.get()) == 0 } { - unsafe { libc::pthread_mutex_unlock(mutex.0.get()) }; - drop(mutex); + if unsafe { pal.as_ref().try_lock() } { + unsafe { pal.as_ref().unlock() }; + drop(pal) } else { // The mutex is locked. This happens if a MutexGuard is leaked. // In this case, we just leak the Mutex too. - forget(mutex); - } - } -} - -pub(super) struct PthreadMutexAttr<'a>(pub &'a mut MaybeUninit); - -impl Drop for PthreadMutexAttr<'_> { - fn drop(&mut self) { - unsafe { - let result = libc::pthread_mutexattr_destroy(self.0.as_mut_ptr()); - debug_assert_eq!(result, 0); + forget(pal) } } } diff --git a/library/std/src/sys/sync/mutex/sgx.rs b/library/std/src/sys/sync/mutex/sgx.rs index 8529e857970..3eb981bc65a 100644 --- a/library/std/src/sys/sync/mutex/sgx.rs +++ b/library/std/src/sys/sync/mutex/sgx.rs @@ -13,7 +13,7 @@ impl Mutex { } fn get(&self) -> &SpinMutex> { - self.inner.get_or_init(|| Box::new(SpinMutex::new(WaitVariable::new(false)))) + self.inner.get_or_init(|| Box::pin(SpinMutex::new(WaitVariable::new(false)))).get_ref() } #[inline] @@ -33,7 +33,7 @@ impl Mutex { pub unsafe fn unlock(&self) { // SAFETY: the mutex was locked by the current thread, so it has been // initialized already. - let guard = unsafe { self.inner.get_unchecked().lock() }; + let guard = unsafe { self.inner.get_unchecked().get_ref().lock() }; if let Err(mut guard) = WaitQueue::notify_one(guard) { // No other waiters, unlock *guard.lock_var_mut() = false; -- cgit 1.4.1-3-g733a5 From 8b2ff49ff9dd20ee417907c2e96daa9f0cd8e7c4 Mon Sep 17 00:00:00 2001 From: joboet Date: Sat, 30 Nov 2024 16:22:56 +0100 Subject: std: clarify comments about initialization --- library/std/src/sys/pal/unix/sync/condvar.rs | 12 ++++++------ library/std/src/sys/pal/unix/sync/mutex.rs | 8 +++++--- library/std/src/sys/sync/condvar/pthread.rs | 2 +- library/std/src/sys/sync/mutex/pthread.rs | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) (limited to 'library/std/src/sys/sync/mutex') diff --git a/library/std/src/sys/pal/unix/sync/condvar.rs b/library/std/src/sys/pal/unix/sync/condvar.rs index 13eeba9c880..73631053e9f 100644 --- a/library/std/src/sys/pal/unix/sync/condvar.rs +++ b/library/std/src/sys/pal/unix/sync/condvar.rs @@ -23,7 +23,7 @@ impl Condvar { } /// # Safety - /// `init` must have been called. + /// `init` must have been called on this instance. #[inline] pub unsafe fn notify_one(self: Pin<&Self>) { let r = unsafe { libc::pthread_cond_signal(self.raw()) }; @@ -31,7 +31,7 @@ impl Condvar { } /// # Safety - /// `init` must have been called. + /// `init` must have been called on this instance. #[inline] pub unsafe fn notify_all(self: Pin<&Self>) { let r = unsafe { libc::pthread_cond_broadcast(self.raw()) }; @@ -39,7 +39,7 @@ impl Condvar { } /// # Safety - /// * `init` must have been called. + /// * `init` must have been called on this instance. /// * `mutex` must be locked by the current thread. /// * This condition variable may only be used with the same mutex. #[inline] @@ -49,7 +49,7 @@ impl Condvar { } /// # Safety - /// * `init` must have been called. + /// * `init` must have been called on this instance. /// * `mutex` must be locked by the current thread. /// * This condition variable may only be used with the same mutex. pub unsafe fn wait_timeout(&self, mutex: Pin<&Mutex>, dur: Duration) -> bool { @@ -95,7 +95,7 @@ impl Condvar { const CLOCK: libc::clockid_t = libc::CLOCK_MONOTONIC; /// # Safety - /// May only be called once. + /// May only be called once per instance of `Self`. pub unsafe fn init(self: Pin<&mut Self>) { use crate::mem::MaybeUninit; @@ -137,7 +137,7 @@ impl Condvar { const CLOCK: libc::clockid_t = libc::CLOCK_REALTIME; /// # Safety - /// May only be called once. + /// May only be called once per instance of `Self`. pub unsafe fn init(self: Pin<&mut Self>) { if cfg!(any(target_os = "espidf", target_os = "horizon", target_os = "teeos")) { // NOTE: ESP-IDF's PTHREAD_COND_INITIALIZER support is not released yet diff --git a/library/std/src/sys/pal/unix/sync/mutex.rs b/library/std/src/sys/pal/unix/sync/mutex.rs index 8ffd375bf91..8ff6c3d3d15 100644 --- a/library/std/src/sys/pal/unix/sync/mutex.rs +++ b/library/std/src/sys/pal/unix/sync/mutex.rs @@ -18,7 +18,7 @@ impl Mutex { } /// # Safety - /// Must only be called once. + /// May only be called once per instance of `Self`. pub unsafe fn init(self: Pin<&mut Self>) { // Issue #33770 // @@ -58,7 +58,8 @@ impl Mutex { } /// # Safety - /// * If `init` was not called, reentrant locking causes undefined behaviour. + /// * If `init` was not called on this instance, reentrant locking causes + /// undefined behaviour. /// * Destroying a locked mutex causes undefined behaviour. pub unsafe fn lock(self: Pin<&Self>) { #[cold] @@ -82,7 +83,8 @@ impl Mutex { } /// # Safety - /// * If `init` was not called, reentrant locking causes undefined behaviour. + /// * If `init` was not called on this instance, reentrant locking causes + /// undefined behaviour. /// * Destroying a locked mutex causes undefined behaviour. pub unsafe fn try_lock(self: Pin<&Self>) -> bool { unsafe { libc::pthread_mutex_trylock(self.raw()) == 0 } diff --git a/library/std/src/sys/sync/condvar/pthread.rs b/library/std/src/sys/sync/condvar/pthread.rs index 4d2f9c0aaba..5bb7431eecf 100644 --- a/library/std/src/sys/sync/condvar/pthread.rs +++ b/library/std/src/sys/sync/condvar/pthread.rs @@ -22,7 +22,7 @@ impl Condvar { fn get(&self) -> Pin<&pal::Condvar> { self.cvar.get_or_init(|| { let mut cvar = Box::pin(pal::Condvar::new()); - // SAFETY: we only call `init` once, namely here. + // SAFETY: we only call `init` once per `pal::Condvar`, namely here. unsafe { cvar.as_mut().init() }; cvar }) diff --git a/library/std/src/sys/sync/mutex/pthread.rs b/library/std/src/sys/sync/mutex/pthread.rs index 5719bb10f7f..75b4b9c6dad 100644 --- a/library/std/src/sys/sync/mutex/pthread.rs +++ b/library/std/src/sys/sync/mutex/pthread.rs @@ -21,7 +21,7 @@ impl Mutex { // This is sound however, as it cannot have been locked. self.pal.get_or_init(|| { let mut pal = Box::pin(pal::Mutex::new()); - // SAFETY: we only call `init` once, namely here. + // SAFETY: we only call `init` once per `pal::Mutex`, namely here. unsafe { pal.as_mut().init() }; pal }) -- cgit 1.4.1-3-g733a5