sync: add RwLockWriteGuard::{downgrade_map, try_downgrade_map} (#5527)

This commit is contained in:
Filipe Rodrigues
2023-03-08 17:06:08 +01:00
committed by GitHub
parent ff2f286c12
commit 002f4a28c8
4 changed files with 369 additions and 3 deletions
+153 -1
View File
@@ -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`.
///
@@ -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.
+162 -1
View File
@@ -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`.
///
@@ -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.
@@ -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> {
+51 -1
View File
@@ -13,7 +13,7 @@ use std::task::Poll;
use futures::future::FutureExt;
use tokio::sync::RwLock;
use tokio::sync::{RwLock, RwLockWriteGuard};
use tokio_test::task::spawn;
use tokio_test::{assert_pending, assert_ready};
@@ -279,3 +279,53 @@ fn try_read_try_write() {
assert_eq!(*lock.try_read().unwrap(), 1515);
}
#[maybe_tokio_test]
async fn downgrade_map() {
let lock = RwLock::new(0);
let write_guard = lock.write().await;
let mut read_t = spawn(lock.read());
// We can't create a read when a write exists
assert_pending!(read_t.poll());
// During the call to `f`, `read_t` doesn't have access yet.
let read_guard1 = RwLockWriteGuard::downgrade_map(write_guard, |v| {
assert_pending!(read_t.poll());
v
});
// After the downgrade, `read_t` got the lock
let read_guard2 = assert_ready!(read_t.poll());
// Ensure they're equal, as we return the original value
assert_eq!(&*read_guard1 as *const _, &*read_guard2 as *const _);
}
#[maybe_tokio_test]
async fn try_downgrade_map() {
let lock = RwLock::new(0);
let write_guard = lock.write().await;
let mut read_t = spawn(lock.read());
// We can't create a read when a write exists
assert_pending!(read_t.poll());
// During the call to `f`, `read_t` doesn't have access yet.
let write_guard = RwLockWriteGuard::try_downgrade_map(write_guard, |_| {
assert_pending!(read_t.poll());
None::<&()>
})
.expect_err("downgrade didn't fail");
// After `f` returns `None`, `read_t` doesn't have access
assert_pending!(read_t.poll());
// After `f` returns `Some`, `read_t` does have access
let read_guard1 = RwLockWriteGuard::try_downgrade_map(write_guard, |v| Some(v))
.expect("downgrade didn't succeed");
let read_guard2 = assert_ready!(read_t.poll());
// Ensure they're equal, as we return the original value
assert_eq!(&*read_guard1 as *const _, &*read_guard2 as *const _);
}