-
Notifications
You must be signed in to change notification settings - Fork 24
Description
Proposal
Problem statement
Rusts Locks are wonderful, most multithreaded code would be impossible to write safe without them. But handling locks
over more than one scope is just a nightmare. This is because you always require the ownership of the guard if you want
to release the lock. This often leads to call chains where every method requires the owned Guard and the Mutex reference
as parameter and the owned Guard as return type. This produces unreadable code and leads to a lot of useless parameter
passing. The second approach if passing the Mutex is not wanted is to lock and unlock it in every scope needed. With
this style the code is much more readable, but it takes a lot of unnecessary locks and unlocks of the Mutex. This
approach is less performant.
Motivating examples or use cases
Unlocks the mutex temporary and lock it after the calling func again, while only holding a mutable reference.
If the Mutex is poisoned upon trying to lock it again, the heal function will be called to fix the inner data. After
this, the poison will be cleared and the mutex guard is valid again.
Example
use std::thread;
use std::sync::Mutex;
use std::sync::Condvar;
use std::time::Duration;
pub fn request_data(guard: &mut MutexGuard<usize>, condvar: &Condvar) {
MutexGuard::freeze_guard(
guard,
|| {
&condvar.notify_one();
thread::sleep(Duration::from_millis(100));
},
|poison| { panic!() }
);
}
pub fn main() {
let mutex = Mutex::new(0usize);
let condvar = Condvar::new();
thread::scope(|s| {
let outer_guard = mutex.lock().unwrap();
s.spawn(|| {
let inner_guard = mutex.lock().unwrap();
let inner_guard_ref = &mut inner_guard;
assert_eq!(**inner_guard_ref, 0);
request_data(inner_guard_ref, &condvar);
assert_eq!(**inner_guard_ref, 123);
});
let outer_guard = &condvar.wait(outer_guard).unwrap();
**outer_guard = 123;
drop(outer_guard)
})
}Solution sketch
The concept is to add a method with three parameters for the guard. First, obviously the mutable reference to the guard.
The second parameter is a closure containing code that should be run while the Mutex is unlocked. This by itself would
already work, but it might cause problems if another thread panics while holding the data. Therefore, the third
parameter is a "heal" closure. It takes a PoisonError with the guard as parameter to allow the thread to fix the inner
data.
This would allow always just using the mutable reference instead of the owned object. Which leads to better readability
and maintainability while still being safe.
impl<'a, T: ?Sized> MutexGuard<'a, T> {
pub fn freeze_guard<F, H>(orig: &mut Self, func: F, heal: H) -> ()
where
F: FnOnce() -> (),
H: FnOnce(PoisonError<T>) -> (),
{
// Unlocking the mutex
unsafe {
orig.lock.poison.done(&self.poison);
orig.lock.inner.unlock();
}
// FIXME: Upon panicking in func the drop of self will be called which will lead to problems because the mutex
// is not actually locked by this guard. This might need a small change in the drop logic of the guard.
// calling func
func();
// trying to acquire lock again
let lock_result = orig.lock.lock();
let new_guard = match lock_result {
Ok(guard) => guard,
Err(poison_error) => {
heal(poison_error);
orig.lock.clear_poison();
// consider the case that the mutex is getting healed, another thread grabs the cleared mutex and poisons
// it again. The heal function is currently an FnOnce and therefore it cannot be cleared again.
orig.lock.lock().unwrap()
},
};
// replacing the old variable with the new
std::mem::swap(orig, &new_guard);
// forgetting the value to prevent unlocking of the mutex again
std::mem::forget(new_guard)
}
}Safety
During the call of func the mutable reference will be in the scope of freeze_guard and therefore it is not possible
to access it. The heal function will always restore the cleared mutex state and therefore guarantees a valid guard
after freeze_guard. If heal panics the mutex guard will remain poisoned as before.
Panics:
If the thread panics while running the func closure the mutex will remain clear because it is not locked by the
current thread (excluding the case that the thread is acquiring the lock in the func). If the thread panics in the
heal function the thread mutex remains poisoned.
Alternatives
There are currently three ways to achieve the same behavior:
- Passing the owned Guard across the call stack. This results in bad readability and maintainability.
- Locking and unlocking the Mutex at every call. If the mutex is heavily used, this costs a lot of unnecessary performance.
- Juggling with alot of unsafe code to somehow access the inner lock and poison to manually achieve the behavior of this
function.
Links and related work
An prevoius discussion has been made here: rust-lang/rfcs#3870