2

I have a scenario like this : Rust Playground

  • I have a struct Container with a lock that is protecting multiple members. I don't want RwLock per member.
  • I want to acquire this lock and then call another function complex_mutation that does some complex logic and mutates this struct.
  • The problem is that RAII guard _guard for lock acquires a reference and then calling another function complex_mutation where &mut self is passed results in multiple immutable + mutable references.
  • I cannot drop the guard before creating another mutable reference as that is what is providing the synchronization here.
  • If I could just inline everything from complex_mutation this will work but that would be too ugly for a real world scenario.

This is a common pattern in C++. How do I solve this in RUST?

Nitish Upreti
  • 6,312
  • 9
  • 50
  • 92
  • Pass the guard to `complex_mutation`? – cdhowie Aug 21 '23 at 21:09
  • How does passing the guard help? This function ```complex_mutation``` is an existing helper I would rather not modify. – Nitish Upreti Aug 21 '23 at 21:10
  • Hmm, looking at the code, the lock is entirely redundant because you take `&mut self` in both functions. Recall that `&mut` references are exclusive, so it's impossible for anything else in the program to have access to `*self`. Just remove the lock. – cdhowie Aug 21 '23 at 21:12
  • your `RwLock<()>` protects `()` and nothing more. You should use `RwLock<(u32, u32)>` instead so it contains the data it's protecting. This is different than how it is done in C++. – kmdreko Aug 21 '23 at 21:12
  • Is there any other workaround than RwLock<(u32, u32)>, I would rather not move everything under this lock. The ```Container``` struct has some APIs that are guaranteed to be run single threaded and would not be acquiring this lock. – Nitish Upreti Aug 21 '23 at 21:15
  • @NitishUpreti this is not a work-around, it is the way locks are designed in Rust. – prog-fh Aug 21 '23 at 21:20
  • I see. Is RwLock<(u32, u32)> also redundant here as @cdhowie pointed out? When would I need this RwLock? – Nitish Upreti Aug 21 '23 at 21:23
  • 1
    @NitishUpreti If you have a `&mut` then it's guaranteed nothing else in the program can read from or write to the referent. Types like `RwLock` offer what's called "interior mutability" which is a concept that allows you to go from a shared reference (`&`) to an exclusive reference (`&mut`) under a specific set of circumstances. If you have an exclusive reference, you don't need interior mutability. So an `RwLock` would be useful if you take `&self` but not if you take `&mut self`. – cdhowie Aug 21 '23 at 21:24

1 Answers1

5

You seem to be confused about a few things. Let me try to clarify.

  • Locks in Rust protect the data contained within the lock. In this case, you are protecting a (), which is pointless.
  • If you are taking &mut self then locks don't help you. Locks are one of many mechanisms that allow you to get an exclusive reference (&mut) from a shared reference (&). The ability to write to a value when you start with a shared reference is called "interior mutability" and it is implemented by RwLock, and also by Mutex, RefCell, etc. If you are starting with an exclusive reference already, interior mutability is redundant.

With that out of the way, what you want to do is put the data in your type into the lock. You can do this with a tuple, but a better idea would be a private "inner" type, like this:

struct Container {
    lock: RwLock<ContainerInner>,
}

struct ContainerInner {
    pub data1: u32,
    pub data2: u32,
}

Now, you want to take &self instead.

If you have methods that need to operate on the data in ContainerInner because you call them from multiple locations, you can simply put them on ContainerInner instead, like this:

impl Container {
    pub fn update_date(&self, val1: u32, val2: u32) {
        self.lock.write().unwrap().complex_mutation(val1, val2);
    }
}

impl ContainerInner {
    fn complex_mutation(&mut self, val1: u32, val2: u32) {
        self.data1 = val1;
        self.data2 = val2;
    }
}

Note that if you have a &mut self then you don't actually need to lock; RwLock::get_mut() allows you to get a &mut to the inner value if you have a &mut to the lock without locking because having an &mut to the lock is a static guarantee that the value isn't shared.

If it helps you conceptually, & and &mut work kind of like RwLock's read() and write() respectively -- you can have many & or a single &mut, and if you have a &mut you can't have a &. (This is somewhat simplified, but you get the idea.) Unlike with runtime locks, the Rust compiler checks that these are used correctly at compile time, which means no runtime checks or locks are needed.

So you could add a method like this:

impl Container {
    pub fn update_date_mut(&mut self, val1: u32, val2: u32) {
        self.lock.get_mut().unwrap().complex_mutation(val1, val2);
    }
}

This is exactly the same as the other method except we take &mut self which allows us to use get_mut() in place of write(), bypassing the lock. The compiler will not let you call this method unless it can prove the value isn't shared, so it is safe to use if the compiler lets you use it. Otherwise, you need to use the other method, which will lock.

cdhowie
  • 158,093
  • 24
  • 286
  • 300
  • Thanks for such a great explanation! This really drives the point home: "Unlike with runtime locks, the Rust compiler checks that these are used correctly at compile time, which means no runtime checks or locks are needed." – Nitish Upreti Aug 21 '23 at 21:41
  • 1
    @NitishUpreti Yep! I think the key takeaway is this quote: "If you are starting with an exclusive reference already, interior mutability is redundant." – cdhowie Aug 21 '23 at 21:42