-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Conversation
Can you please add a justification to these methods for why they are sound? |
I'm fairly certain they are safe, but I'm not 100% sure. Originally I was going to open an issue asking if it was safe, given the surrounding API and, if so, if they could be added to tokio. But since the implementation was relatively easy, I just decided to open up a PR and I was hoping we could discuss here the safety. In detail: However, with If we come to the conclusion that it isn't, should I just add the justification to the inside of the functions with a |
Here's one argument for their soundness:
@Amanieu Do you have any feedback for the soundness of the proposed methods in this PR? |
I believe this is sound. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think @Darksonn's argument about soundness is convincing.
If we decide that these methods should be added, I think it also makes sense to add them to the OwnedRwLockWriteGuard
.
tokio/src/sync/rwlock/write_guard.rs
Outdated
let data = f(&*this) as *const U; | ||
let this = this.skip_drop(); | ||
|
||
RwLockReadGuard { | ||
s: this.s, | ||
data, | ||
marker: PhantomData, | ||
#[cfg(all(tokio_unstable, feature = "tracing"))] | ||
resource_span: this.resource_span, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should also release the write access here. Currently it's not possible for another reader to access the guarded data after using one of these functions.
Releasing the write access should be done by releasing all permits but one to the backing semaphore. You can take a look how downgrade
is implemented.
downgrade
also records some tracing events. It would be great to have them in both of these functions as well.
You will need to add tests for this. In particular, you should make sure to test that its possible to acquire another read lock while holding the downgraded read lock. |
`downgrade_map` can already be made using a combination of `RwLockWriteGuard::downgrade` and `RwLockReadGuard::map`, but this version runs `f` *before* downgrading, in case it's useful for anyone and just for completion's sake. The real use case is with `try_downgrade_map`. In this case, we run `f` *before* we downgrade, so we can return the write lock in case it returns `None`. This isn't implementable in safe code, at least not without one of the following tradeoffs: * Call `f` twice: Not always doable, when it's expensive or not idempotent. * Downgrade, call `f`, then re-lock if it returned `None`: If you're doing double-checked locking, this goes against the whole point of it, because you unlock and so state might have changed in the meantime.
…erly downgrading. `RwLockWriteGuard::{downgrade_map, try_downgrade_map}` doctests now test whether a read lock can be obtained afterwards.
These are mirrors of the existing methods on `RwLockLockWriteGuard`.
The doctests check if a read lock can be acquired after the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this change looks good, I left some comments inline.
Would you prefer a proper test on tests/ instead?
I think doctests are fine.
/// # async fn main() { | ||
/// let lock = Arc::new(RwLock::new(Foo(1))); | ||
/// | ||
/// let mapped = OwnedRwLockWriteGuard::downgrade_map(Arc::clone(&lock).write_owned().await, |f| &f.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you split this line? It's hard to see what's going on here from the docs page.
/// let mapped = OwnedRwLockWriteGuard::downgrade_map(Arc::clone(&lock).write_owned().await, |f| &f.0); | |
/// let guard = Arc::clone(&lock).write_owned().await; | |
/// let mapped = OwnedRwLockWriteGuard::downgrade_map(guard, |f| &f.0); |
/// # async fn main() { | ||
/// let lock = Arc::new(RwLock::new(Foo(1))); | ||
/// | ||
/// let guard = OwnedRwLockWriteGuard::try_downgrade_map(Arc::clone(&lock).write_owned().await, |f| Some(&f.0)).expect("should not fail"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same here.
/// 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. |
There was a problem hiding this comment.
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
.
I think the tests are exhaustive enough for all scenarios. Let me know if there any cases I've missed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! I'll let @Darksonn take a look as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [tokio](https://tokio.rs) ([source](https://github.com/tokio-rs/tokio)) | dependencies | minor | `1.26.0` -> `1.27.0` | | [tokio](https://tokio.rs) ([source](https://github.com/tokio-rs/tokio)) | dev-dependencies | minor | `1.26.0` -> `1.27.0` | --- ### Release Notes <details> <summary>tokio-rs/tokio</summary> ### [`v1.27.0`](https://github.com/tokio-rs/tokio/releases/tag/tokio-1.27.0): Tokio v1.27.0 [Compare Source](tokio-rs/tokio@tokio-1.26.0...tokio-1.27.0) ##### 1.27.0 (March 27th, 2023) This release bumps the MSRV of Tokio to 1.56. ([#​5559]) ##### Added - io: add `async_io` helper method to sockets ([#​5512]) - io: add implementations of `AsFd`/`AsHandle`/`AsSocket` ([#​5514], [#​5540]) - net: add `UdpSocket::peek_sender()` ([#​5520]) - sync: add `RwLockWriteGuard::{downgrade_map, try_downgrade_map}` ([#​5527]) - task: add `JoinHandle::abort_handle` ([#​5543]) ##### Changed - io: use `memchr` from `libc` ([#​5558]) - macros: accept path as crate rename in `#[tokio::main]` ([#​5557]) - macros: update to syn 2.0.0 ([#​5572]) - time: don't register for a wakeup when `Interval` returns `Ready` ([#​5553]) ##### Fixed - fs: fuse std iterator in `ReadDir` ([#​5555]) - tracing: fix `spawn_blocking` location fields ([#​5573]) - time: clean up redundant check in `Wheel::poll()` ([#​5574]) ##### Documented - macros: define cancellation safety ([#​5525]) - io: add details to docs of `tokio::io::copy[_buf]` ([#​5575]) - io: refer to `ReaderStream` and `StreamReader` in module docs ([#​5576]) [#​5512]: tokio-rs/tokio#5512 [#​5514]: tokio-rs/tokio#5514 [#​5520]: tokio-rs/tokio#5520 [#​5525]: tokio-rs/tokio#5525 [#​5527]: tokio-rs/tokio#5527 [#​5540]: tokio-rs/tokio#5540 [#​5543]: tokio-rs/tokio#5543 [#​5553]: tokio-rs/tokio#5553 [#​5555]: tokio-rs/tokio#5555 [#​5557]: tokio-rs/tokio#5557 [#​5558]: tokio-rs/tokio#5558 [#​5559]: tokio-rs/tokio#5559 [#​5572]: tokio-rs/tokio#5572 [#​5573]: tokio-rs/tokio#5573 [#​5574]: tokio-rs/tokio#5574 [#​5575]: tokio-rs/tokio#5575 [#​5576]: tokio-rs/tokio#5576 </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNS4yNC41IiwidXBkYXRlZEluVmVyIjoiMzUuMjQuNSJ9--> Co-authored-by: cabr2-bot <cabr2.help@gmail.com> Reviewed-on: https://codeberg.org/Calciumdibromid/CaBr2/pulls/1838 Reviewed-by: crapStone <crapstone@noreply.codeberg.org> Co-authored-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org> Co-committed-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
Adds
RwLockWriteGuard::{downgrade_map, try_downgrade_map}
.Motivation
These functions allow the following code to work:
The code uses a double-checked lock on a
RwLock
to do some action, and then returns a mapped read guard to the inner data.With the current suite of functions, we can't easily perform the second check efficiently. We need to not unlock the rwlock, which means we'd need to first call
try_get
to check if it's available, then perform a downgrade and map it withtry_get().unwrap()
to get the value again. This involves callingtry_get
twice, which isn't desirable.These function are
Fn(&T) -> &U
, so I don't believe there are any soundness issues (I made a stack overflow question to check if it was sound, and someone suggested that if it was, this could be a good API to add, so I made this PR).Solution
We simply add the functions on
RwLockWriteGuard
.RwLockMappedWriteGuard
can't have them, due to unsoundness, but a non-mappedRwLockWriteGuard
is fine.