Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I replace the value inside a Mutex?

I have a Git repository hidden behind a Mutex:

pub struct GitRepo {
    contents: Mutex<GitContents>,
    workdir: PathBuf,
}

I want to query it, but only a maximum of once: after it's been queried, I want to just use the results we got the first time. A repository has either a git2::Repository, or a vector of results. A Repository is Send but not Sync.

enum GitContents {
    Before { repo: git2::Repository },
    After { statuses: Git },
}

struct Git {
    statuses: Vec<(PathBuf, git2::Status)>,
}

The GitContents enum reflects the fact that we either have the repository to query, or the results of querying it, but never both.

I'm trying to get Rust to enforce this property by having the function that turns a repository into statuses consume the repository as it produces the status vector:

fn repo_to_statuses(repo: git2::Repository, workdir: &Path) -> Git {
    // Assume this does something useful...
    Git { statuses: Vec::new() }
}

However, I can't get the Mutex to play nice with this. Here is my attempt so far to write a function that queries a GitRepo with a predicate P, replacing the value inside the Mutex if it hasn't been queried yet:

impl GitRepo {
    fn search<P: Fn(&Git) -> bool>(&self, p: P) -> bool {
        use std::mem::replace;
        // Make this thread wait until the mutex becomes available.
        // If it's locked, it's because another thread is running repo_to_statuses
        let mut contents = self.contents.lock().unwrap();
        match *contents {
            // If the repository has been queried then just use the existing results
            GitContents::After { ref statuses } => p(statuses),
            // If it hasn't, then replace it with some results, then use them.
            GitContents::Before { ref repo } => {
                let statuses = repo_to_statuses(*repo, &self.workdir);
                let result = p(&statuses);
                replace(&mut *contents, GitContents::After { statuses });
                result
            },
        }
    }
}

Although there is mutation involved, this method only takes &self rather than &mut self because it returns the same result regardless of whether the repository is being queried for the first or second time, even though there's more work being done on the first. But Rust complains:

  • It refuses to move the repo out of the contents I've borrowed in repo_to_statuses(*repo, &self.workdir), even though I know the value should get replaced immediately afterwards. ("cannot move out of borrowed content")
  • It doesn't like me replace-ing &mut *contents either, because I'm borrowing the contents immutably as the value being match-ed. ("cannot borrow 'contents' as mutable because it is also borrowed as immutable")

Is there any way to convince the borrow checker of my intentions?

like image 775
Ben S Avatar asked Aug 31 '17 16:08

Ben S


1 Answers

The question you are asking and the real inner problem have nothing intrinsically to do with a Mutex, once you have locked it and have a mutable reference or a type that implements DerefMut.

You can assign a new value to the reference using the dereference operator *. If you need the previous value, you can use std::mem::replace.

use std::sync::Mutex;
use std::mem;

fn example_not_using_old_value(state: &Mutex<String>) {
    let mut state = state.lock().expect("Could not lock mutex");
    *state = String::from("dereferenced");
}

fn example_using_old_value(state: &Mutex<String>) -> String {
    let mut state = state.lock().expect("Could not lock mutex");
    mem::replace(&mut *state, String::from("replaced"))
}

fn main() {
    let state = Mutex::new("original".into());
    example_not_using_old_value(&state);
    let was = example_using_old_value(&state);

    println!("Is now {:?}", state);
    println!("Was {:?}", was);
}    

We dereference the MutexGuard<T> to get a T, and take a mutable reference to that, yielding a &mut T that we can call mem::replace with.


Your broader problem is because you can't move out of borrowed content (see the numerous Q&A for that). See these directly relevant Q&A:

  • Change enum variant while moving the field to the new variant
  • Is it possible to switch variants in the value of a mutable reference to an enum?
  • Temporarily move out of borrowed content

You may wish to add a new enum variant that represents the state when everything's been moved out but nothing has been moved back in yet. Then you can replace your value with that dummy and take ownership of the old value, performing your operations, then putting the new value back.

like image 87
Shepmaster Avatar answered Nov 09 '22 02:11

Shepmaster