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

Conversation

Zenithsiz
Copy link
Contributor

Adds RwLockWriteGuard::{downgrade_map, try_downgrade_map}.

Motivation

These functions allow the following code to work:

impl T {
    fn try_get(&self) -> Option<&U> { ... }
}

pub async fn foo(lock: RwLock<T>) -> RwLockReadGuard<U> {
    // Check with a read lock
    if let Ok(value) = RwLockReadGuard::try_map(lock.read().await, |value| value.try_get()) {
        return value;
    }

    // Else "upgrade" and check if it was changed in the meantime
    let guard = lock.write().await;
    if let Ok(value) = RwLockWriteGuard::try_downgrade_map(guard, |value| value.try_get()) {
        return value;
    }

    // Use `guard` mutably
    // ...
}

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 with try_get().unwrap() to get the value again. This involves calling try_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-mapped RwLockWriteGuard is fine.

@github-actions github-actions bot added the R-loom Run loom tests on this PR label Mar 3, 2023
@Darksonn Darksonn added A-tokio Area: The main tokio crate M-sync Module: tokio/sync labels Mar 3, 2023
@Darksonn
Copy link
Contributor

Darksonn commented Mar 4, 2023

Can you please add a justification to these methods for why they are sound?

@Zenithsiz
Copy link
Contributor Author

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:
The issues in this issue are due to functions that support interior mutability usually containing an fn(&mut Wrapper<T>) -> &mut T to "circumvent" the interior mutability. However, when downgrading, you could then keep a &T, and then proceed to call a function on &Wrapper<T> that changes the T, thus breaking the promise that &T is noalias (assuming T doesn't contain an unsafe cell). The example in the linked issue illustrates this well with RwLock<Cell<u32>>.

However, with {try_}downgrade_map, we only give out a &Wrapper<T>, so you can't "circumvent" the interior mutability and get a &T, I believe. I can't see any way obvious way to break this, but I just wanted to discuss it since there have been issues with downgrading mapped locks.

If we come to the conclusion that it isn't, should I just add the justification to the inside of the functions with a SAFETY: ... comment?

@Darksonn
Copy link
Contributor

Darksonn commented Mar 4, 2023

Here's one argument for their soundness:

  1. If the closure fails, then you've just accessed the contents immutably, which is fine given a write guard.
  2. If the closure succeeds, then your code is equivalent to using downgrade followed by mapping the resulting read guard.

@Amanieu Do you have any feedback for the soundness of the proposed methods in this PR?

@Amanieu
Copy link

Amanieu commented Mar 5, 2023

I believe this is sound.

Copy link
Member

@satakuma satakuma left a 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.

Comment on lines 146 to 155
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,
}
Copy link
Member

@satakuma satakuma Mar 6, 2023

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.

@Darksonn
Copy link
Contributor

Darksonn commented Mar 6, 2023

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`.
@Zenithsiz
Copy link
Contributor Author

The doctests check if a read lock can be acquired after the {try_}downgrade_map, but is this enough? Would you prefer a proper test on tests/ instead?

Copy link
Member

@satakuma satakuma left a 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);
Copy link
Member

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.

Suggested change
/// 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");
Copy link
Member

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.
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.

@Zenithsiz
Copy link
Contributor Author

I think the tests are exhaustive enough for all scenarios. Let me know if there any cases I've missed.

Copy link
Member

@satakuma satakuma left a 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.

Copy link
Contributor

@Darksonn Darksonn left a comment

Choose a reason for hiding this comment

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

LGTM.

@Darksonn Darksonn merged commit 002f4a2 into tokio-rs:master Mar 8, 2023
crapStone pushed a commit to Calciumdibromid/CaBr2 that referenced this pull request Mar 29, 2023
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. ([#&#8203;5559])

##### Added

-   io: add `async_io` helper method to sockets ([#&#8203;5512])
-   io: add implementations of `AsFd`/`AsHandle`/`AsSocket` ([#&#8203;5514], [#&#8203;5540])
-   net: add `UdpSocket::peek_sender()` ([#&#8203;5520])
-   sync: add `RwLockWriteGuard::{downgrade_map, try_downgrade_map}` ([#&#8203;5527])
-   task: add `JoinHandle::abort_handle` ([#&#8203;5543])

##### Changed

-   io: use `memchr` from `libc` ([#&#8203;5558])
-   macros: accept path as crate rename in `#[tokio::main]` ([#&#8203;5557])
-   macros: update to syn 2.0.0 ([#&#8203;5572])
-   time: don't register for a wakeup when `Interval` returns `Ready` ([#&#8203;5553])

##### Fixed

-   fs: fuse std iterator in `ReadDir` ([#&#8203;5555])
-   tracing: fix `spawn_blocking` location fields ([#&#8203;5573])
-   time: clean up redundant check in `Wheel::poll()` ([#&#8203;5574])

##### Documented

-   macros: define cancellation safety ([#&#8203;5525])
-   io: add details to docs of `tokio::io::copy[_buf]` ([#&#8203;5575])
-   io: refer to `ReaderStream` and `StreamReader` in module docs ([#&#8203;5576])

[#&#8203;5512]: tokio-rs/tokio#5512

[#&#8203;5514]: tokio-rs/tokio#5514

[#&#8203;5520]: tokio-rs/tokio#5520

[#&#8203;5525]: tokio-rs/tokio#5525

[#&#8203;5527]: tokio-rs/tokio#5527

[#&#8203;5540]: tokio-rs/tokio#5540

[#&#8203;5543]: tokio-rs/tokio#5543

[#&#8203;5553]: tokio-rs/tokio#5553

[#&#8203;5555]: tokio-rs/tokio#5555

[#&#8203;5557]: tokio-rs/tokio#5557

[#&#8203;5558]: tokio-rs/tokio#5558

[#&#8203;5559]: tokio-rs/tokio#5559

[#&#8203;5572]: tokio-rs/tokio#5572

[#&#8203;5573]: tokio-rs/tokio#5573

[#&#8203;5574]: tokio-rs/tokio#5574

[#&#8203;5575]: tokio-rs/tokio#5575

[#&#8203;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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate M-sync Module: tokio/sync R-loom Run loom tests on this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants