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 Rwlock try_upgrade method #514

Closed
devmitch opened this issue Dec 27, 2024 · 8 comments
Closed

Add Rwlock try_upgrade method #514

devmitch opened this issue Dec 27, 2024 · 8 comments
Labels
ACP-accepted API Change Proposal is accepted (seconded with no objections) api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api

Comments

@devmitch
Copy link

devmitch commented Dec 27, 2024

Proposal

This proposal suggests to add a try_upgrade method on RwLockReadGuard that transforms it into a RwLockWriteGuard if it is immediately possible (ie if there are no read guards except for this one at this time)

Problem statement

There is currently no way to atomically change a RwLockReadGuard into a RwLockWriteGuard. A blocking upgrade method is problematic, since if there are two threads blocking on the upgrade at the same time, only one of them can succeed and the other will fail and must drop their read lock. There are a few ways to handle a blocking API.

However, it is much easier and more straightfoward to implement the non-blocking try_upgrade. If the read guard is the only one in existence (write guards cannot exist due to the existence of the read guard), we can immediately and atomically convert the read guard into a write guard. Otherwise, if there are other readers, the method will immediately fail, and the thread can continue with the read guard.

Motivating examples or use cases

Some B-Tree data structures use algorithms that optimistically acquire write locks on nodes (by upgrading read locks) if it is currently possible, and rebalance the tree, such as the Foster B-Tree. Currently this is not possible with the Rust standard library locks.

Solution sketch

// src/sync/rwlock.rs
impl<'a, T: ?Sized> RwLockReadGuard<'a, T> {
    // note: this function takes ownership of the read guard
    pub fn try_upgrade(orig: Self) -> Result<RwLockWriteGuard<'a, T>, RwLockReadGuard<'a, T>> {
        // call non-blocking `try_upgrade` on inner sys::RwLock
        // if successful, construct the resulting RwLockWriteGuard and return wrapped in Ok()
        // otherwise, re-construct the RwLockReadGuard that this function was called on, and return it in Err()      
    }
}
// src/sys/sync/rwlock/futex.rs
impl RwLock {
    pub fn try_upgrade(&self) -> bool {
        // I assume we use strong CAS since this function is not in a loop (though the guard caller could be...?)
        self.state.compare_exchange(READ_LOCKED, WRITE_LOCKED, Acquire, Relaxed).is_ok()
    }

Alternatives

I couldn't find any crates that implement this design.

Dropping the read guard and re-acquiring a write guard can lead to threads sneaking in after the read guard drop and before the write guard acquire, changing the protected data, and possibly invalidating any conclusions made about the protected data before the read guard was dropped.

Links and related work

RwLock downgrade was recently implemented:

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.
@devmitch devmitch added api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api labels Dec 27, 2024
@devmitch devmitch changed the title (My API Change Proposal) Add Rwlock try_upgrade method Dec 27, 2024
@tgross35
Copy link

This would mirror the unstable rwlock_downgrade API

@devmitch
Copy link
Author

devmitch commented Dec 27, 2024

@tgross35 Sorry, clicked submit too quickly, have updated my proposal

@conradludgate
Copy link

I couldn't find any crates that implement this design.

Parking lot does have some upgrade support, but it's somewhat limited in that to use upgrade, the read lock guard still has some higher priority. What I mean here is that you cannot have 2 UpgradeableRead guards active at a time, but you can have 1 upgradeable guard + N non-upgradable guards active at the same time.

https://docs.rs/parking_lot/latest/parking_lot/type.RwLock.html#method.upgradable_read

@devmitch
Copy link
Author

What I mean here is that you cannot have 2 UpgradeableRead guards active at a time

Indeed this is the bottleneck that prevents their use when using them on concurrent B-Trees. Any thread that wishes to potentially upgrade a node to a write lock will need to take an upgrade lock on the node, and the higher up this node is in the tree, the more it limits concurrent access to the nodes below, as only one can exist at a time.

The downside to the parking_lot approach is that having the option to upgrade limits concurrency, whether or not the upgrade is exercised. The downside to the approach in this proposal is that it is fallible with "try", similar to the existing try_write.

Also, if this was to be accepted, it should be made clear in the docs that threads shouldn't loop on try_upgrade forever without releasing the read locks, as 2 or more of these threads will deadlock.

@cuviper
Copy link
Member

cuviper commented Dec 27, 2024

Can we support this on all targets?

Should this fail if another writer is already waiting?

@devmitch
Copy link
Author

devmitch commented Dec 28, 2024

Can we support this on all targets?

Briefly looking through the other target versions, for teeos it should always succeed in upgrading, similar to the recent downgrade method:

// Since there is no difference between read-locked and write-locked on this platform, this
// function is simply a no-op as only 1 reader can read: the original writer.

I can't find anything regarding how SOLID works, maybe someone else who knows more about this platform can help? Otherwise, like other functions in the standard library not supported on SOLID, it can panic.

It should be supported in queue.rs with a CAS.

Should this fail if another writer is already waiting?

For the sake of simplicity I think the answer should be no. Therefore, priority will be implicitly given to upgraded reads over waiting writers.

@joshtriplett joshtriplett added the ACP-accepted API Change Proposal is accepted (seconded with no objections) label Jan 7, 2025
@joshtriplett
Copy link
Member

We discussed this in today's @rust-lang/libs-api meeting.

Approved, ship it.

Please make sure the documentation is very clear that this is opportunistic and code must not loop on it (because that will cause deadlocks).

@the8472
Copy link
Member

the8472 commented Jan 7, 2025

I also suggest adding a cfg(miri) to the impl that spuriously fails the try so that people don't rely on it too much since it may fail on some platforms.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ACP-accepted API Change Proposal is accepted (seconded with no objections) api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api
Projects
None yet
Development

No branches or pull requests

6 participants