28

Like in the topic, why Rust prevents from multiple mutable references? I have read chapter in rust-book, and I understand that when we have multi-threaded code we are secured from data races but let's look at this code:

fn main() {
    let mut x1 = String::from("hello");
    let r1 = &mut x1;
    let r2 = &mut x1;

    r1.insert(0, 'w');

}

This code is not running simultaneously so there is no possibility for data races. What is more when I am creating new thread and I want to use variable from parent thread in a new thread I have to move it so only new thread is an owner of the parent variable.

The only reason I can see is that programmer can lose himself in his code when it is growing up. We have multiple places in which one piece of data can be modified and even the code is not running parallel we can get some bugs.

4
  • 2
    You should only ask one question per post. I suggest you edit your post to remove the 2nd question and ask it in its own post. Commented Oct 13, 2019 at 15:29
  • 1
    Ok, I did it. Here is link: stackoverflow.com/q/58367174/9620900 if u want to answer. Commented Oct 13, 2019 at 19:42
  • 1
    What would be the advantage of having multiple mutable references to x1 in your code? Where would you want to use r2 where you couldn't also just use r1? Commented Oct 13, 2019 at 19:51
  • Whole Java works in the way I described above. You can have million of objects containing reference to the same piece of data in the same scope or am I wrong? Commented Oct 13, 2019 at 20:04

2 Answers 2

38

The fact that Rust prevent two mutable references at the same time to prevent data races is a common misconception. This is only one of the reasons. Preventing two mutable references makes it possible to keep invariants on types easily and let the compiler enforce that the invariant are not violated.

Take this piece of C++ code for an example:

#include <vector>

int main() {
    std::vector<int> foo = { 1, 2, 3 };
    
    for (auto& e: foo) {
        if (e % 2 == 0) {
            foo.push_back(e+1);
        }
    }

    return 0;
}

This is unsafe because you cannot mutate a vector while you are iterating it. Mutating the vector might reallocate its internal buffer, which invalidates all references. In C++, this is a UB. In Java or C# you would get a runtime exception.

Rust however, prevents this kind of issues at compile time:

fn main() {
    let mut foo = vec![1, 2, 3];
    
    for e in foo {
        if e % 2 == 0 {
            foo.push(e+1);
        }
    }
}

gives an error:

error[E0382]: borrow of moved value: `foo`
 --> src/main.rs:6:13
  |
2 |     let mut foo = vec![1, 2, 3];
  |         ------- move occurs because `foo` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
3 |     
4 |     for e in foo {
  |              ---
  |              |
  |              value moved here
  |              help: consider borrowing to avoid moving into the for loop: `&foo`
5 |         if e % 2 == 0 {
6 |             foo.push(e+1);
  |             ^^^ value borrowed here after move

Other languages, such as Python, manage internal buffers for you. While this avoid runtime errors, it this might still lead to unexpected result in some cases. For example in Python:

lst = ['a', 'b', 'c', 'd', 'e']
for x in lst:
    lst.remove(x)
print(lst)

prints

['b', 'd']

The list was edited with no issues, but because the iterator had no knowledge of this, this lead to skipping some elements. Iterating with for x in list(lst) instead (making an explicit copy of the list so that the modified list and the iterator don't share any memory) yields [] as expected.

Sign up to request clarification or add additional context in comments.

8 Comments

do you have any reference from official doc to support "Preventing two mutable references makes it possible to keep invariants on types easily and let the compiler enforce that the invariant are not violated." ?
@Izana The answer gives an example where C++ allows multiple references, including one mutable, which is broken, and shows that the Rust direct equivalent fails to compile.
Sure. I mean, "invariants" is likely related to the algorithm that compiler. Do you have reference that this invariant is dedicatedly chosen during compiler design? When you mention "prevent data races is a common misconception", I suspect making it easy for the compiler algorithm is considered more important.
Why would java fail with that though. New internal buffer would mean the references itself are written to a new location. But the references still point to the right heap location. Nothing would be invalidated.
This is probably a dumb question as I’m new to this - if you have one mutable reference, and the ownership is also mutable, how does that prevent changes from two different contexts? Can’t one be via the single mutable reference, and the other via the owning mutable variable?
|
1

The big benefit of this restriction is that rust can prevent data races at compile time. A data race occurs when multiple pointers or references access the same memory location simultaneously, with at least one attempting to write data. Without proper synchronization mechanisms, this concurrent access can lead to corrupted or unpredictable data.

Rust enforces a “single writer or multiple readers” rule: either you can read and write the value, or it can be shared by any number of readers, but never both at the same time.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.