Skip to content

Commit

Permalink
sync: add RwLockWriteGuard::{downgrade_map, try_downgrade_map} (#5527)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zenithsiz committed Mar 8, 2023
1 parent ff2f286 commit 002f4a2
Show file tree
Hide file tree
Showing 4 changed files with 369 additions and 3 deletions.
154 changes: 153 additions & 1 deletion tokio/src/sync/rwlock/owned_write_guard.rs
Expand Up @@ -100,7 +100,78 @@ impl<T: ?Sized> OwnedRwLockWriteGuard<T> {
}
}

/// Attempts to make a new [`OwnedRwLockMappedWriteGuard`] for a component
/// Makes a new [`OwnedRwLockReadGuard`] for a component of the locked data.
///
/// This operation cannot fail as the `OwnedRwLockWriteGuard` passed in already
/// locked the data.
///
/// This is an associated function that needs to be used as
/// `OwnedRwLockWriteGuard::downgrade_map(..)`. A method would interfere with methods of
/// the same name on the contents of the locked data.
///
/// Inside of `f`, you retain exclusive access to the data, despite only being given a `&T`. Handing out a
/// `&mut T` would result in unsoundness, as you could use interior mutability.
///
/// # Examples
///
/// ```
/// use std::sync::Arc;
/// use tokio::sync::{RwLock, OwnedRwLockWriteGuard};
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// struct Foo(u32);
///
/// # #[tokio::main]
/// # async fn main() {
/// let lock = Arc::new(RwLock::new(Foo(1)));
///
/// let guard = Arc::clone(&lock).write_owned().await;
/// let mapped = OwnedRwLockWriteGuard::downgrade_map(guard, |f| &f.0);
/// let foo = lock.read_owned().await;
/// assert_eq!(foo.0, *mapped);
/// # }
/// ```
#[inline]
pub fn downgrade_map<F, U: ?Sized>(this: Self, f: F) -> OwnedRwLockReadGuard<T, U>
where
F: FnOnce(&T) -> &U,
{
let data = f(&*this) as *const U;
let this = this.skip_drop();
let guard = OwnedRwLockReadGuard {
lock: this.lock,
data,
_p: PhantomData,
#[cfg(all(tokio_unstable, feature = "tracing"))]
resource_span: this.resource_span,
};

// Release all but one of the permits held by the write guard
let to_release = (this.permits_acquired - 1) as usize;
guard.lock.s.release(to_release);

#[cfg(all(tokio_unstable, feature = "tracing"))]
guard.resource_span.in_scope(|| {
tracing::trace!(
target: "runtime::resource::state_update",
write_locked = false,
write_locked.op = "override",
)
});

#[cfg(all(tokio_unstable, feature = "tracing"))]
guard.resource_span.in_scope(|| {
tracing::trace!(
target: "runtime::resource::state_update",
current_readers = 1,
current_readers.op = "add",
)
});

guard
}

/// Attempts to make a new [`OwnedRwLockMappedWriteGuard`] for a component
/// of the locked data. The original guard is returned if the closure
/// returns `None`.
///
Expand Down Expand Up @@ -159,6 +230,87 @@ impl<T: ?Sized> OwnedRwLockWriteGuard<T> {
})
}

/// Attempts to make a new [`OwnedRwLockReadGuard`] for a component of
/// the locked data. The original guard is returned if the closure returns
/// `None`.
///
/// This operation cannot fail as the `OwnedRwLockWriteGuard` passed in already
/// locked the data.
///
/// This is an associated function that needs to be
/// used as `OwnedRwLockWriteGuard::try_downgrade_map(...)`. A method would interfere with
/// methods of the same name on the contents of the locked data.
///
/// Inside of `f`, you retain exclusive access to the data, despite only being given a `&T`. Handing out a
/// `&mut T` would result in unsoundness, as you could use interior mutability.
///
/// If this function returns `Err(...)`, the lock is never unlocked nor downgraded.
///
/// # Examples
///
/// ```
/// use std::sync::Arc;
/// use tokio::sync::{RwLock, OwnedRwLockWriteGuard};
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// struct Foo(u32);
///
/// # #[tokio::main]
/// # async fn main() {
/// let lock = Arc::new(RwLock::new(Foo(1)));
///
/// let guard = Arc::clone(&lock).write_owned().await;
/// let guard = OwnedRwLockWriteGuard::try_downgrade_map(guard, |f| Some(&f.0)).expect("should not fail");
/// let foo = lock.read_owned().await;
/// assert_eq!(foo.0, *guard);
/// # }
/// ```
#[inline]
pub fn try_downgrade_map<F, U: ?Sized>(
this: Self,
f: F,
) -> Result<OwnedRwLockReadGuard<T, U>, Self>
where
F: FnOnce(&T) -> Option<&U>,
{
let data = match f(&*this) {
Some(data) => data as *const U,
None => return Err(this),
};
let this = this.skip_drop();
let guard = OwnedRwLockReadGuard {
lock: this.lock,
data,
_p: PhantomData,
#[cfg(all(tokio_unstable, feature = "tracing"))]
resource_span: this.resource_span,
};

// Release all but one of the permits held by the write guard
let to_release = (this.permits_acquired - 1) as usize;
guard.lock.s.release(to_release);

#[cfg(all(tokio_unstable, feature = "tracing"))]
guard.resource_span.in_scope(|| {
tracing::trace!(
target: "runtime::resource::state_update",
write_locked = false,
write_locked.op = "override",
)
});

#[cfg(all(tokio_unstable, feature = "tracing"))]
guard.resource_span.in_scope(|| {
tracing::trace!(
target: "runtime::resource::state_update",
current_readers = 1,
current_readers.op = "add",
)
});

Ok(guard)
}

/// Converts this `OwnedRwLockWriteGuard` into an
/// `OwnedRwLockMappedWriteGuard`. This method can be used to store a
/// non-mapped guard in a struct field that expects a mapped guard.
Expand Down
163 changes: 162 additions & 1 deletion tokio/src/sync/rwlock/write_guard.rs
Expand Up @@ -102,7 +102,84 @@ impl<'a, T: ?Sized> RwLockWriteGuard<'a, T> {
}
}

/// Attempts to make a new [`RwLockMappedWriteGuard`] for a component of
/// Makes a new [`RwLockReadGuard`] for a component of the locked data.
///
/// This operation cannot fail as the `RwLockWriteGuard` passed in already
/// locked the data.
///
/// This is an associated function that needs to be used as
/// `RwLockWriteGuard::downgrade_map(..)`. A method would interfere with methods of
/// the same name on the contents of the locked data.
///
/// This is equivalent to a combination of asynchronous [`RwLockWriteGuard::map`] and [`RwLockWriteGuard::downgrade`]
/// from the [`parking_lot` crate].
///
/// Inside of `f`, you retain exclusive access to the data, despite only being given a `&T`. Handing out a
/// `&mut T` would result in unsoundness, as you could use interior mutability.
///
/// [`RwLockMappedWriteGuard`]: struct@crate::sync::RwLockMappedWriteGuard
/// [`RwLockWriteGuard::map`]: https://docs.rs/lock_api/latest/lock_api/struct.RwLockWriteGuard.html#method.map
/// [`RwLockWriteGuard::downgrade`]: https://docs.rs/lock_api/latest/lock_api/struct.RwLockWriteGuard.html#method.downgrade
/// [`parking_lot` crate]: https://crates.io/crates/parking_lot
///
/// # Examples
///
/// ```
/// use tokio::sync::{RwLock, RwLockWriteGuard};
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// struct Foo(u32);
///
/// # #[tokio::main]
/// # async fn main() {
/// let lock = RwLock::new(Foo(1));
///
/// let mapped = RwLockWriteGuard::downgrade_map(lock.write().await, |f| &f.0);
/// let foo = lock.read().await;
/// assert_eq!(foo.0, *mapped);
/// # }
/// ```
#[inline]
pub fn downgrade_map<F, U: ?Sized>(this: Self, f: F) -> RwLockReadGuard<'a, U>
where
F: FnOnce(&T) -> &U,
{
let data = f(&*this) as *const U;
let this = this.skip_drop();
let guard = RwLockReadGuard {
s: this.s,
data,
marker: PhantomData,
#[cfg(all(tokio_unstable, feature = "tracing"))]
resource_span: this.resource_span,
};

// Release all but one of the permits held by the write guard
let to_release = (this.permits_acquired - 1) as usize;
this.s.release(to_release);

#[cfg(all(tokio_unstable, feature = "tracing"))]
guard.resource_span.in_scope(|| {
tracing::trace!(
target: "runtime::resource::state_update",
write_locked = false,
write_locked.op = "override",
)
});

#[cfg(all(tokio_unstable, feature = "tracing"))]
guard.resource_span.in_scope(|| {
tracing::trace!(
target: "runtime::resource::state_update",
current_readers = 1,
current_readers.op = "add",
)
});

guard
}

/// Attempts to make a new [`RwLockMappedWriteGuard`] for a component of
/// the locked data. The original guard is returned if the closure returns
/// `None`.
///
Expand Down Expand Up @@ -165,6 +242,90 @@ impl<'a, T: ?Sized> RwLockWriteGuard<'a, T> {
})
}

/// Attempts to make a new [`RwLockReadGuard`] for a component of
/// the locked data. The original guard is returned if the closure returns
/// `None`.
///
/// This operation cannot fail as the `RwLockWriteGuard` passed in already
/// locked the data.
///
/// This is an associated function that needs to be
/// used as `RwLockWriteGuard::try_downgrade_map(...)`. A method would interfere with
/// methods of the same name on the contents of the locked data.
///
/// This is equivalent to a combination of asynchronous [`RwLockWriteGuard::try_map`] and [`RwLockWriteGuard::downgrade`]
/// from the [`parking_lot` crate].
///
/// Inside of `f`, you retain exclusive access to the data, despite only being given a `&T`. Handing out a
/// `&mut T` would result in unsoundness, as you could use interior mutability.
///
/// If this function returns `Err(...)`, the lock is never unlocked nor downgraded.
///
/// [`RwLockMappedWriteGuard`]: struct@crate::sync::RwLockMappedWriteGuard
/// [`RwLockWriteGuard::map`]: https://docs.rs/lock_api/latest/lock_api/struct.RwLockWriteGuard.html#method.map
/// [`RwLockWriteGuard::downgrade`]: https://docs.rs/lock_api/latest/lock_api/struct.RwLockWriteGuard.html#method.downgrade
/// [`parking_lot` crate]: https://crates.io/crates/parking_lot
///
/// # Examples
///
/// ```
/// use tokio::sync::{RwLock, RwLockWriteGuard};
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// struct Foo(u32);
///
/// # #[tokio::main]
/// # async fn main() {
/// let lock = RwLock::new(Foo(1));
///
/// let guard = RwLockWriteGuard::try_downgrade_map(lock.write().await, |f| Some(&f.0)).expect("should not fail");
/// let foo = lock.read().await;
/// assert_eq!(foo.0, *guard);
/// # }
/// ```
#[inline]
pub fn try_downgrade_map<F, U: ?Sized>(this: Self, f: F) -> Result<RwLockReadGuard<'a, U>, Self>
where
F: FnOnce(&T) -> Option<&U>,
{
let data = match f(&*this) {
Some(data) => data as *const U,
None => return Err(this),
};
let this = this.skip_drop();
let guard = RwLockReadGuard {
s: this.s,
data,
marker: PhantomData,
#[cfg(all(tokio_unstable, feature = "tracing"))]
resource_span: this.resource_span,
};

// Release all but one of the permits held by the write guard
let to_release = (this.permits_acquired - 1) as usize;
this.s.release(to_release);

#[cfg(all(tokio_unstable, feature = "tracing"))]
guard.resource_span.in_scope(|| {
tracing::trace!(
target: "runtime::resource::state_update",
write_locked = false,
write_locked.op = "override",
)
});

#[cfg(all(tokio_unstable, feature = "tracing"))]
guard.resource_span.in_scope(|| {
tracing::trace!(
target: "runtime::resource::state_update",
current_readers = 1,
current_readers.op = "add",
)
});

Ok(guard)
}

/// Converts this `RwLockWriteGuard` into an `RwLockMappedWriteGuard`. This
/// method can be used to store a non-mapped guard in a struct field that
/// expects a mapped guard.
Expand Down
3 changes: 3 additions & 0 deletions tokio/src/sync/rwlock/write_guard_mapped.rs
Expand Up @@ -160,6 +160,9 @@ impl<'a, T: ?Sized> RwLockMappedWriteGuard<'a, T> {
resource_span: this.resource_span,
})
}

// Note: No `downgrade`, `downgrade_map` nor `try_downgrade_map` because they would be unsound, as we're already
// potentially been mapped with internal mutability.
}

impl<T: ?Sized> ops::Deref for RwLockMappedWriteGuard<'_, T> {
Expand Down

0 comments on commit 002f4a2

Please sign in to comment.