ARC and the single mutable reference rule

Rust enforces that only a single mutable reference can exist for an object even for single-threaded applications. Reading the various posts on this forum and online, I understand that this is due to various reasons such as the iterator invalidator problem, and so on.

However, Rust also does allow multiple mutable references in multithreaded environments using ARC and a Mutex. In theory, if I used the same approach of using Arc and Mutexes for single-threaded applications, outside of facing a ridiculously high performance penalty, would the other problems associated with multiple mutable references go away (iterator invalidation, etc.)?

Because it seems like Rust has all of these rules and then Arc basically allows you to bypass them anyway...

Arc and Mutex don’t really exempt you from those rules, they just delay enforcement of them until runtime. This is more flexible because you can use patterns that the compiler can’t verify in advance, but it comes at a cost of both performance (to perform the necessary checks) and reliability (faulty code will panic instead of being caught by the compiler).

You absolutely can do the same thing in single-threaded code, and Rust even provides single-threaded counterparts (Rc and RefCell) that don’t come with the synchronization overhead necessary for multithreaded programming.

8 Likes

No it does not.The mutex ensures that only one thread can access the variable at the time, so there is never more than one mutable reference.

11 Likes

This is a misconception. Arc just allows to share ownership of the same value. All the handles to the same Arc shared value only have shared access to it (i.e. the equivalent of a & reference, not &mut reference). With an Arc<Mutex<T>> this means sharing ownership of a Mutex<T>, with all handles having shared access to the Mutex<T>. Note that for the purpose of this discussion you can achieve the same result by using a &Mutex<T>, since that can also be shared more than once. Having shared access to a Mutex<T> however does not imply having shared access to the T inside it! In fact by default you have no access at all. To gain access you have to call the .lock() method, which will ensure to give (mutable) access to only one place at a time. You can achieve similar results with RwLock (which also allows to get shared access in multiple places) and RefCell (which does something similar to RwLock, but panics if you attempt to access something in a conflicting way and does not support multithreading). In all of these cases you never get multiple mutable references at the same time, you can only get them in different places but at different times.

10 Likes

Having shared access to a Mutex<T> however does not imply having shared access to the T inside it! In fact by default you have no access at all .

Thank you! This is the part that was tripping me up. This makes sense now.

1 Like

To elaborate on these points, here's some code that uses Arc and Mutex:

use std::sync::{Arc, Mutex};

fn main() {
    let values = Arc::new(Mutex::new(vec![1_u32, 2, 3]));
    
    for value in values.lock().unwrap().iter() {
        if *value % 2 == 0 {
            values.lock().unwrap().push(value*2);
        }
    }
}

(playground)

At first glance, it looks like I've accessed values immutably (via the iterator) and mutably (when we push() inside the loop). Yay, we've just beaten the borrow checker!

However, if you actually run the code you'll find it never exits.

This is because the Mutex makes sure only one thing can access its contents at a time, so when we do the values.lock().unwrap().push(value*2) our mutex is already "borrowed" by the iterator, and it goes to sleep until that borrow is over. That borrow can't finish until the push() is executed, so we end up with a deadlock.

The same code without Arc and Mutex would be rejected by the compiler:

fn main() {
    let mut values = vec![1_u32, 2, 3];

    for value in values.iter() {
        if *value % 2 == 0 {
            values.push(value * 2);
        }
    }
}

(playground)

   Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `values` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:13
  |
4 |     for value in values.iter() {
  |                  -------------
  |                  |
  |                  immutable borrow occurs here
  |                  immutable borrow later used here
5 |         if *value % 2 == 0 {
6 |             values.push(value * 2);
  |             ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` (bin "playground") due to 1 previous error

So we didn't actually "beat the borrow checker", all we did was defer detecting the error from compile time to runtime.

6 Likes