Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion crates/common/src/borrow.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::lock::{
MapImmutable, PyImmutableMappedMutexGuard, PyMappedMutexGuard, PyMappedRwLockReadGuard,
MapImmutable, PyImmutableMappedMutexGuard, PyMappedDetachingRwLockReadGuard,
PyMappedDetachingRwLockWriteGuard, PyMappedMutexGuard, PyMappedRwLockReadGuard,
PyMappedRwLockWriteGuard, PyMutexGuard, PyRwLockReadGuard, PyRwLockWriteGuard,
};
use alloc::fmt;
Expand All @@ -24,13 +25,15 @@ pub enum BorrowedValue<'a, T: ?Sized> {
MappedMuLock(PyImmutableMappedMutexGuard<'a, T>),
ReadLock(PyRwLockReadGuard<'a, T>),
MappedReadLock(PyMappedRwLockReadGuard<'a, T>),
MappedDetachingReadLock(PyMappedDetachingRwLockReadGuard<'a, T>),
}
impl_from!('a, T, BorrowedValue<'a, T>,
Ref(&'a T),
MuLock(PyMutexGuard<'a, T>),
MappedMuLock(PyImmutableMappedMutexGuard<'a, T>),
ReadLock(PyRwLockReadGuard<'a, T>),
MappedReadLock(PyMappedRwLockReadGuard<'a, T>),
MappedDetachingReadLock(PyMappedDetachingRwLockReadGuard<'a, T>),
);

impl<'a, T: ?Sized> BorrowedValue<'a, T> {
Expand Down Expand Up @@ -59,6 +62,9 @@ impl<'a, T: ?Sized> BorrowedValue<'a, T> {
Self::MappedReadLock(m) => {
BorrowedValue::MappedReadLock(PyMappedRwLockReadGuard::map(m, f))
}
Self::MappedDetachingReadLock(m) => {
BorrowedValue::MappedDetachingReadLock(PyMappedDetachingRwLockReadGuard::map(m, f))
}
}
}
}
Expand All @@ -73,6 +79,7 @@ impl<T: ?Sized> Deref for BorrowedValue<'_, T> {
Self::MappedMuLock(m) => m,
Self::ReadLock(r) => r,
Self::MappedReadLock(m) => m,
Self::MappedDetachingReadLock(m) => m,
}
}
}
Expand All @@ -90,6 +97,7 @@ pub enum BorrowedValueMut<'a, T: ?Sized> {
MappedMuLock(PyMappedMutexGuard<'a, T>),
WriteLock(PyRwLockWriteGuard<'a, T>),
MappedWriteLock(PyMappedRwLockWriteGuard<'a, T>),
MappedDetachingWriteLock(PyMappedDetachingRwLockWriteGuard<'a, T>),
}

impl_from!('a, T, BorrowedValueMut<'a, T>,
Expand All @@ -98,6 +106,7 @@ impl_from!('a, T, BorrowedValueMut<'a, T>,
MappedMuLock(PyMappedMutexGuard<'a, T>),
WriteLock(PyRwLockWriteGuard<'a, T>),
MappedWriteLock(PyMappedRwLockWriteGuard<'a, T>),
MappedDetachingWriteLock(PyMappedDetachingRwLockWriteGuard<'a, T>),
);

impl<'a, T: ?Sized> BorrowedValueMut<'a, T> {
Expand All @@ -113,6 +122,9 @@ impl<'a, T: ?Sized> BorrowedValueMut<'a, T> {
Self::MappedWriteLock(m) => {
BorrowedValueMut::MappedWriteLock(PyMappedRwLockWriteGuard::map(m, f))
}
Self::MappedDetachingWriteLock(m) => BorrowedValueMut::MappedDetachingWriteLock(
PyMappedDetachingRwLockWriteGuard::map(m, f),
),
}
}
}
Expand All @@ -127,6 +139,7 @@ impl<T: ?Sized> Deref for BorrowedValueMut<'_, T> {
Self::MappedMuLock(m) => m,
Self::WriteLock(w) => w,
Self::MappedWriteLock(w) => w,
Self::MappedDetachingWriteLock(w) => w,
}
}
}
Expand All @@ -139,6 +152,7 @@ impl<T: ?Sized> DerefMut for BorrowedValueMut<'_, T> {
Self::MappedMuLock(m) => &mut *m,
Self::WriteLock(w) => &mut *w,
Self::MappedWriteLock(w) => &mut *w,
Self::MappedDetachingWriteLock(w) => &mut *w,
}
}
}
16 changes: 16 additions & 0 deletions crates/common/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use lock_api::{

cfg_select! {
feature = "threading" => {
pub use detaching::{BlockingWaitHook, set_blocking_wait_hook};
pub use parking_lot::{RawMutex, RawRwLock, RawThreadId};
pub use std::sync::OnceLock as OnceCell;
pub use core::cell::LazyCell;
Expand Down Expand Up @@ -47,6 +48,8 @@ cfg_select! {
}
}

mod detaching;
pub use detaching::RawDetachingRwLock;
mod immutable_mutex;
pub use immutable_mutex::*;
mod thread_mutex;
Expand All @@ -60,6 +63,19 @@ pub type PyThreadMutex<T> = ThreadMutex<RawMutex, RawThreadId, T>;
pub type PyThreadMutexGuard<'a, T> = ThreadMutexGuard<'a, RawMutex, RawThreadId, T>;
pub type PyMappedThreadMutexGuard<'a, T> = MappedThreadMutexGuard<'a, RawMutex, RawThreadId, T>;

/// A `PyRwLock` for data a thread may hold locked across a blocking call.
///
/// Waiting for one of these leaves the interpreter first, so a thread blocked
/// on it is a thread stop-the-world can park. That is only safe where a
/// collection never takes the same lock — see [`RawDetachingRwLock`] — so this
/// is opt-in per lock rather than what every `PyRwLock` does.
pub type PyDetachingRwLock<T> = RwLock<RawDetachingRwLock, T>;
pub type PyDetachingRwLockReadGuard<'a, T> = RwLockReadGuard<'a, RawDetachingRwLock, T>;
pub type PyDetachingRwLockWriteGuard<'a, T> = RwLockWriteGuard<'a, RawDetachingRwLock, T>;
pub type PyMappedDetachingRwLockReadGuard<'a, T> = MappedRwLockReadGuard<'a, RawDetachingRwLock, T>;
pub type PyMappedDetachingRwLockWriteGuard<'a, T> =
MappedRwLockWriteGuard<'a, RawDetachingRwLock, T>;

pub type PyRwLock<T> = RwLock<RawRwLock, T>;
pub type PyRwLockUpgradableReadGuard<'a, T> = RwLockUpgradableReadGuard<'a, RawRwLock, T>;
pub type PyRwLockReadGuard<'a, T> = RwLockReadGuard<'a, RawRwLock, T>;
Expand Down
258 changes: 258 additions & 0 deletions crates/common/src/lock/detaching.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
//! A reader-writer lock that lets a thread leave its interpreter before it
//! blocks.
//!
//! Stopping the world means waiting for every running thread to reach a
//! safepoint. A thread blocked on a lock reaches none, so if the thread holding
//! that lock has already been stopped, the two wait on each other forever. The
//! holder is not the one who can avoid this — a lock is held across a blocking
//! call precisely because that is what the call needs — so the waiter gives up
//! its interpreter for the duration of the wait instead, which is what a
//! blocking call does anyway.
//!
//! Doing so is safe only for locks nothing reachable from a stop-the-world
//! section takes, so it is opt-in per lock — see [`RawDetachingRwLock`] for the
//! rule and why it is needed.
//!
//! Only the contended path pays for any of this: an acquire that takes the lock
//! on the first try is the same atomic exchange it was, and never reaches the
//! hook. The hook is installed by whoever knows how to detach a thread
//! ([`set_blocking_wait_hook`]); until then, and on any thread that is not
//! running an interpreter, a blocked acquire just blocks.

use super::RawRwLock;
#[cfg(feature = "threading")]
use core::cell::Cell;
use lock_api::{
RawRwLock as RawRwLockTrait, RawRwLockDowngrade, RawRwLockRecursive as RawRwLockRecursiveTrait,
RawRwLockUpgrade as RawRwLockUpgradeTrait, RawRwLockUpgradeDowngrade,
};
#[cfg(feature = "threading")]
use std::sync::OnceLock;

/// Runs `wait` with the calling thread detached from its interpreter.
#[cfg(feature = "threading")]
pub type BlockingWaitHook = fn(wait: &dyn Fn());

#[cfg(feature = "threading")]
static BLOCKING_WAIT: OnceLock<BlockingWaitHook> = OnceLock::new();

/// Install the hook that detaches a thread around a blocked lock acquire.
///
/// Later calls are ignored, so every interpreter in a process can call this
/// during its own initialization.
#[cfg(feature = "threading")]
pub fn set_blocking_wait_hook(hook: BlockingWaitHook) {
let _ = BLOCKING_WAIT.set(hook);
}

#[cfg(feature = "threading")]
std::thread_local! {
/// Set while this thread is inside the hook, so that a lock taken by the
/// hook itself — or by anything detaching and re-attaching runs — waits
/// plainly instead of recursing back into it.
static IN_HOOK: Cell<bool> = const { Cell::new(false) };
}

/// Clears [`IN_HOOK`] even if the hook unwinds.
#[cfg(feature = "threading")]
struct HookGuard;

#[cfg(feature = "threading")]
impl Drop for HookGuard {
fn drop(&mut self) {
let _ = IN_HOOK.try_with(|in_hook| in_hook.set(false));
}
}

/// Block on `wait`, detached from this thread's interpreter if there is one.
///
/// Nothing spins on the way here. The lock underneath already spins before it
/// parks, and skips that spin once a waiter has parked — the same condition
/// `_PyMutex_LockTimed` spins under. A spin layered on top cannot read that
/// condition, and would go on retrying a `try_lock` that reports failure for as
/// long as a writer holds the writer bit, which it takes before it waits for
/// readers to drain: a yield per retry for the whole of exactly the wait this
/// exists to survive.
#[cfg(feature = "threading")]
#[cold]
#[inline(never)]
fn wait_detached(wait: impl Fn()) {
let Some(hook) = BLOCKING_WAIT.get() else {
wait();
return;
};
// `try_with` fails once the thread's locals are being destroyed, which is
// also a point at which there is no interpreter left to detach from.
let entered = IN_HOOK
.try_with(|in_hook| !in_hook.replace(true))
.unwrap_or(false);
if !entered {
wait();
return;
}
let _guard = HookGuard;
hook(&wait);
}

/// Without threads there is no interpreter to leave and nothing to stop.
#[cfg(not(feature = "threading"))]
#[inline]
fn wait_detached(wait: impl Fn()) {
wait();
}

/// A reader-writer lock whose blocking acquires detach first, and which is the
/// raw lock it wraps in every other respect.
///
/// Use through [`PyDetachingRwLock`](super::PyDetachingRwLock).
///
/// # Only for locks a collection never takes
///
/// The wait acquires the lock while detached, so the thread comes back holding
/// it, and re-attaching is a point at which a stop-the-world in flight will
/// park the thread. It is therefore parked *holding the lock*. Everything that
/// stops the world must be able to finish without that lock: if a collection
/// were to take it, the collection would block on a thread only the collection
/// can release, and neither would move again.
///
/// So this is opt-in per lock, and the rule for opting in is that nothing
/// reachable from a stop-the-world section takes the same lock. An object whose
/// payload holds no references — nothing for the collector to traverse into —
/// satisfies that; most do not.
///
/// Not implementing the vm's `Traverse` for this lock enforces part of that: a
/// payload holding one cannot derive `Traverse`, so it cannot become something
/// a collection walks into. Only that part. A collection is not the only thing
/// that stops the world — dumping tracebacks, enumerating thread frames and
/// forking all do — and nothing checks what those reach. For them the rule is
/// still a convention.
#[repr(transparent)]
pub struct RawDetachingRwLock(RawRwLock);

// SAFETY: every method forwards to the wrapped raw lock, which upholds the
// contract; the blocking acquires only add a wait that ends with the same lock
// acquired.
unsafe impl RawRwLockTrait for RawDetachingRwLock {
#[allow(
clippy::declare_interior_mutable_const,
reason = "raw lock initializer, as in the type it wraps"
)]
const INIT: Self = Self(<RawRwLock as RawRwLockTrait>::INIT);

type GuardMarker = <RawRwLock as RawRwLockTrait>::GuardMarker;

#[inline]
fn lock_shared(&self) {
if !self.0.try_lock_shared() {
wait_detached(|| self.0.lock_shared());
}
}

#[inline]
fn try_lock_shared(&self) -> bool {
self.0.try_lock_shared()
}

#[inline]
unsafe fn unlock_shared(&self) {
unsafe { self.0.unlock_shared() }
}

#[inline]
fn lock_exclusive(&self) {
if !self.0.try_lock_exclusive() {
wait_detached(|| self.0.lock_exclusive());
}
}

#[inline]
fn try_lock_exclusive(&self) -> bool {
self.0.try_lock_exclusive()
}

#[inline]
unsafe fn unlock_exclusive(&self) {
unsafe { self.0.unlock_exclusive() }
}

#[inline]
fn is_locked(&self) -> bool {
self.0.is_locked()
}

#[inline]
fn is_locked_exclusive(&self) -> bool {
self.0.is_locked_exclusive()
}
}

// SAFETY: forwards to the wrapped raw lock.
unsafe impl RawRwLockDowngrade for RawDetachingRwLock {
#[inline]
unsafe fn downgrade(&self) {
unsafe { self.0.downgrade() }
}
}

// SAFETY: forwards to the wrapped raw lock.
//
// None of these detach. `upgrade` runs with the upgradable lock already held,
// and `lock_shared_recursive` may be the re-entrant take of a lock this thread
// holds; detaching there would park a thread *holding* the lock, the one thing
// this type must not do. `lock_upgradable` starts from holding nothing and
// could detach as safely as `lock_shared` does, but nothing takes an upgradable
// read of one of these, so it does not.
unsafe impl RawRwLockUpgradeTrait for RawDetachingRwLock {
#[inline]
fn lock_upgradable(&self) {
self.0.lock_upgradable()
}

#[inline]
fn try_lock_upgradable(&self) -> bool {
self.0.try_lock_upgradable()
}

#[inline]
unsafe fn unlock_upgradable(&self) {
unsafe { self.0.unlock_upgradable() }
}

#[inline]
unsafe fn upgrade(&self) {
// SAFETY: the caller holds the upgradable lock, as `upgrade` requires.
unsafe { self.0.upgrade() }
}

#[inline]
unsafe fn try_upgrade(&self) -> bool {
unsafe { self.0.try_upgrade() }
}
}

// SAFETY: forwards to the wrapped raw lock.
unsafe impl RawRwLockUpgradeDowngrade for RawDetachingRwLock {
#[inline]
unsafe fn downgrade_upgradable(&self) {
unsafe { self.0.downgrade_upgradable() }
}

#[inline]
unsafe fn downgrade_to_upgradable(&self) {
unsafe { self.0.downgrade_to_upgradable() }
}
}

// SAFETY: forwards to the wrapped raw lock. Does not detach; see the upgrade
// impl above.
unsafe impl RawRwLockRecursiveTrait for RawDetachingRwLock {
#[inline]
fn lock_shared_recursive(&self) {
self.0.lock_shared_recursive()
}

#[inline]
fn try_lock_shared_recursive(&self) -> bool {
self.0.try_lock_shared_recursive()
}
}
Loading
Loading