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
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
?
Yea sorry I mixed the terms, a shared_mutex in cpp is an RwLock in Rust. I will adjust the text.
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
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 read
s 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.)
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
.
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
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
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.
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.