Condvar RwLock

Hi all,

Is there any way to use a condition variable with an RwLock using only the std lib? If not, are there any proposals, expected future changes already planned?

For reference from cpp std::condition_variable_any - cppreference.com

I assume you are aware of the existence of Condvar in std::sync - Rust. Do you mind translating this fairly uncommented link to cpp-reference into a more concrete proposal as to what you thing is missing? Your title mentions RwLock, are you asking about a Condvar that supports RwLock? You use the phrase “shared mutex”; in my experience, a Mutex is almost always being shared, or are you using “shared mutex” as a technical term for something concrete that isn’t an ordinary std::sync::Mutex?

1 Like

Yea sorry I mixed the terms, a shared_mutex in cpp is an RwLock in Rust. I will adjust the text.

1 Like

I assume there is some API practicalities that would need to be worked out, right? For example, it seems reasonably that in a wait_while-style loop, you wouldn’t want to force full exclusive mutable access to the RwLock just to check whether or not to wake up. But RwLock (in the standard library) doesn’t come with an upgradable_read-style method.

Also, when you use the Condvar you could be interested in either read-access or write-access as the ultimate goal anyways. Thinking about this for a minute, you do pass in a guard though anyway; I suppose it the thing was generic and accepted either of RwLockReadGuard or RwLockWriteGuard, or even a possibly new RwLockUpgradableReadGuard to control what precisely you’ll “get back” once the condvar’s wait_while loop succeeds, than that might be a decent API.


Regarding existing std-only approaches, I can imagine a way to achieve both

  • some form of upgradable_read-like API
  • and a way to use Condvar with this

as follows:

Use a combined (Mutex<()>, RwLock<T>), and follow the convention that all callers to RwLock::write are expected to lock the Mutex first (and keep it locked until the RwLockWriteGuard is released). Possibly enforcing this convention by creating a wrapper struct.

Then locking the Mutex<()> is like requesting upgradable_read access. You can choose to RwLock::read once you have the Mutex, and it will immediately succeed (because there can only be other readers at the moment, no writers), giving you readable access; but you can also move over to calling RwLock::write, and the convention ensures no other thread will come in and get mutable access in the mean-time between releasing the RwLockReadGuard and acquiring the RwLockWriteGuard (since you’re holding onto the Mutex, noone else will call RwLock::write).

Then a Condvar for such effectively-upgradable-read access can work as an ordinary std::sync::Condvar on that Mutex<()>.

I don’t know how much overhead such an approach is compared to the most optimal way of implementing an RwLock with upgradable_read support.


Since your asking about “using only std lib” solutions, are you aware of existing crates that offer a Condvar that supports some form of RwLock? If yes, that might be a good precendent for API design, and also the crate’s popularity might be a good indicator for whether or not something like it is valuable for the standard library.

While this does shim something similar to upgradable_read, it is not upgradable_read; an upgradable_read prevents new reads from being acquired while it's out.

(This is why upgradable_read is very rarely what you want. It's pretty much exclusively an optimization of double checked access to allow the second check to occur while waiting for read loans to be returned.)

1 Like

Why would it do that? Wouldn’t it be enought to start preventing other reads once you start waiting on the actual upgrade operation (if you decide to do it at all)?


Edit: Skimming through RwLock in lock_api - Rust, I can’t find any 100% clear clarification either way; perhaps looking into the implementation of parking_lot to learn more is the only way…


Edit2: Looking into the parking_lot implementation, it seems to be not the case that any .read()s would be prevented by the presence of an upgradable_read lock. Shared lock (fast path) does not care about UPGRADABLE_BIT being set, only about WRITER_BIT; and upgradable lock (fast path) does indeed only set the UPGRADABLE_BIT.

2 Likes

I would like to reply to two things

A) I believe that deciding whether new readers should be allowed or not in an upgradable lock using the simplistic approach that @steffahn described can be easily achieved as follows

use std::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};

struct SimpleUpgradableRwLock {
    mutex: Mutex<()>,
    rwlock: RwLock<()>,
}

impl SimpleUpgradableRwLock {
    fn read(&self) -> RwLockReadGuard<'_, ()> {
        // omitting or adding this line decides whether new
        // readers are allowed or not respectively
        let _lock = self.mutex.lock();
        self.rwlock.read().unwrap()
    }

    fn upgradable(&self) -> (MutexGuard<'_, ()>, RwLockReadGuard<'_, ()>) {
        let lock = self.mutex.lock().unwrap();
        (lock, self.rwlock.read().unwrap())
    }

    fn write(&self) -> RwLockWriteGuard<'_, ()> {
        let _lock = self.mutex.lock().unwrap();
        self.rwlock.write().unwrap()
    }

    fn upgrade<'a>(
        &self,
        lock: MutexGuard<'a, ()>,
        read: RwLockReadGuard<'_, ()>,
    ) -> (MutexGuard<'a, ()>, RwLockWriteGuard<'_, ()>) {
        drop(read);
        (lock, self.rwlock.write().unwrap())
    }
}

B) While I do understand the motivation behind wanting a generic CondVar that works with upgradable rw locks, that covers the common case in which the caller needs a read access only inside the wait_while predicate followed by a write access after successfully waking up (e.g: waiting on a shared queue to be not empty then consuming new items), I see however no issue in starting with a set of APIs in the conventional rust way.

    // not very common, but still maybe covers some rare use cases
    fn wait_while<'a, T, F>(
        &self,
        read: RwLockReadGuard<'a, ()>,
        condition: F,
    ) -> RwLockReadGuard<'a, ()>
    where
        F: FnMut(&T) -> bool,
    {
    }

    // Perhaps not common to have a mutable access inside the predicate
    // yet it is a small price to pay to be able to use CondVar with RwLocks
    // Also similar to what C++ provides already in its std API
    fn wait_while_mut<'a, T, F>(
        &self,
        read: RwLockWriteGuard<'a, ()>,
        condition: F,
    ) -> RwLockWriteGuard<'a, ()>
    where
        F: FnMut(&mut T) -> bool,
    {
    }

    // most likely what you want however requires an RwUpgradableLock so requires a
    // bigger change which can be added later as a future extension
    fn wait_while_upgradable<'a, T, F>(
        &self,
        read: RwLockUpgradableReadGuard<'a, ()>,
        condition: F,
    ) -> RwLockUpgradableReadGuard<'a, ()>
    where
        F: FnMut(&T) -> bool,
    {

Now the nice thing about that approach is

  1. It is easier to implement and extend iteratively, figuring out the details of the most efficient upgradable lock can be done later.
  2. Having even just the first two APIs will allow the users to use a CondVar and make use of read accesses of an RwLock in other parts of the code outside of the wait_while, instead of having to resort to a Mutex now and use exclusive access everywhere. Also this approach is already adopted in the std::condition_variable_any in the C++ stdlib
  3. I can imagine having two different mutex classes; RwLock RwUpgradableLock. I also believe that the latter will almost certainly have some more overhead at least for additional book keeping. Now having 3 APIs will give the user the control to decide whether or not it is worth it to pay for that overhead by using a RwUpgradableLock/wait_while_upgradable() in order to squeeze that read access only in the predicate and write access afterwards or rather perhaps to decide it is just not worth it and then the user can simply use exclusive access directly inside the predicate through RwLock/wait_while_mut().

Let me know what are your thoughts.