Search code examples
rustmemory-management

When is having a mutable and an immutable reference at the same time really a problem?


I've been playing around with Rust for some time now. It's hard, it's new, it's exciting, and the compiler is very helpful. So I usually end up with working code. But there is one problem I quite fundamentally do not understand. Which is why having multiple mutable references, or some immutable ones and a mutable one, is a problem?

Firstly, in a sequentially executing program, I don't see how this could ever be a problem. Say I have two references, no matter whether mutable or immutable. At any point in time, only one thing will use the reference. Sure, the value may change, but I don't see how memory unsafety could arise here.

The only way this could ever be relevant in any way (that I can imagine) is when we introduce multiple threads. I'd be more comfortable knowing for sure that all of them only read the data, because there is no way some other thread can screw with the value while I'm reading it. Is this assumption correct? Is the whole mutability deal only relevant in multi-threaded contexts (other than code predictability that is also found in other languages like let and const in JS)? If not, how could a sequential program run into trouble (as seen by Rust) because data was mutable when it shouldn't have been?

The second issue seems even more striking: Sure, I can only have one mutable reference at a time, or none while also having immutable references. But there still is the owner of the data! He can mutate it at any time, particularly also while another spawned thread reads the data via an immutable reference (if that is even possible, which I doubt, due to lifetime checks). What kind of safety is added by disallowing mutable references to otherwise referenced data when the owner still keeps the right to mutate the data?

As you can see, these questions are quite fundamental to my understanding of what Rust adds in terms of memory safety in this regard. Unfortunately, I haven't found any explicit explanations to why this is done, only that.


Solution

  • For your first issue, this trivial Python example can help

    def remove_unexpected_element(sequence, unexpected):
        for (idx, elem) in enumerate(sequence):
            if elem == unexpected:
                sequence.pop(idx)
    
    seq = [10, 10, 20, 20, 30, 30]
    remove_unexpected_element(seq, 20)
    print(seq) # [10, 10, 20, 30, 30]
    

    A programmer aware of the details of how iterating on a sequence works in Python could detect at first sight that this is incorrect. But if we don't know these details, we don't know if this code does what it seems to do. Maybe another language could perform this loop as expected... (Note that Java performs a runtime check, checkForComodification(), in order to detect such a situation and throw an exception) With Rust, we could simply not express such an ambiguous situation because we try to mutate a data (the sequence) while we are still consulting it (during all the iterations). If we really want to express such an algorithm, we would have to count explicitly with an integer and access the sequence using indexing. Instead of a long consultation of the sequence (during the iterations), we would have to perform multiple distinct accesses (check length, remove an element...) and it's our responsibility to keep all of this consistent. If this does not work as expected, it is because of a bug which is explicitly readable in the source code of our algorithm.

    For your second issue, the mutable reference exists simultaneously with the mutable value it refers to, but during the section of code this reference is used, the value cannot directly be accessed. This is exactly what the borrow-checker takes care of. When it comes to multi-threading, you won't be able to pass an exclusive (mutable) reference to another thread while still being able to use the original value in its original location without a synchronisation mechanism.