Skip to content

ACP: Freezable Mutex Guards #679

@SpaceBroetchen

Description

@SpaceBroetchen

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:

  1. Passing the owned Guard across the call stack. This results in bad readability and maintainability.
  2. Locking and unlocking the Mutex at every call. If the mutex is heavily used, this costs a lot of unnecessary performance.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions