Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RwLockWriteGuard::{downgrade_map, try_downgrade_map}. #5527

Merged
merged 5 commits into from Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to have a test which checks this guarantee. For example, a test which checks if a reader which was already waiting for the access before calling this function is still waiting after this function returns Err.

///
/// [`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