An item's mutability is essentially part of the name of the variable in rust. Take for example this code:
let mut foo = String::new();
let foo = foo;
let mut foo = foo;
foo
suddenly becomes immutable, but it does not mean that the first two foo
s don't exist.
On the other hand, a mutable reference is attached to the lifetime of the object and is therefore type-bound, and will exist for its own lifetime, disallowing any kind of access to the original object if it is not through the reference.
let mut my_string = String::new();
my_string.push_str("This is ok! ");
let foo: &mut String = &mut my_string;
foo.push_str("This goes through the mutable reference, and is therefore ok! ");
my_string.push_str("This is not ok, and will not compile because `foo` still exists");
println!("We use foo here because of non lexical lifetimes: {:?}", foo);
The second call to my_string.push_str
will not compile because foo
can (in this case it is guaranteed to) be used afterwards.
Your specific question asks something similar to the following, but you don't even need multithreading to test this:
fn immutably_use_value(x: &str) {
println!("{:?}", x);
}
let mut foo = String::new();
let bar = &foo; //This now has immutable access to the mutable object.
let baz = &foo; //Two points are allowed to observe a value at the same time. (Ignoring `Sync`)
immutably_use_value(bar); //Ok, we can observe it immutably
foo.push_str("Hello world!"); //This would be ok... but we use the immutable references later!
immutably_use_value(baz);
This does not compile. If you could annotate the lifetimes, they'd look something similar to this:
let mut foo = String::new(); //Has lifetime 'foo
let bar: &'foo String = &foo; //Has lifetime 'bar: 'foo
let baz: &'foo String = &foo; //Has lifetime 'baz: 'foo
//On the other hand:
let mut foo = String::new(); //Has lifetime 'foo
let bar: &'foo mut String = &mut foo; //Has lifetime 'bar: mut 'foo
let baz: &'foo mut String = &mut foo; //Error, we cannot have overlapping mutable borrows for the same object!
A few extra notes:
Due to NLL (Non Lexical Lifetimes), the following code will compile:
let mut foo = String::new();
let bar = &foo;
foo.push_str("Abc");
Because bar
is not used after the mutable use of foo
.
You mention threading, which has its own constraints and traits involved:
The Send
trait will allow you to give ownership of a variable across a thread.
The Sync
trait will allow you to share a reference to a variable across a thread. This includes mutable references, as long as the original thread does not use the object for the duration of the borrow.
A few examples:
- Type
T
is Send + Sync
, it can be sent across threads and be shared between them
- Type
T
is !Send + Sync
, it can be shared across threads, but not sent between them. An example is a window handle that can only be destroyed on the original thread.
- Type
T
is Send + !Sync
, it can be sent across threads, but not shared between them. An example is RefCell
, which will can only use its runtime borrow-checking on a single thread due to it not using atomics (Multithreading safe components).
- Type
T
is !Send + !Sync
, it can only live on the thread it was created on. An example is Rc
, which cannot send a copy of itself across threads because it cannot count references atomically (Look at Arc
to do that) and since it carries no lifetimes to force a single copy of itself to exist when sending across a thread boundary, it therefore cannot be sent across threads.
- I use
&str
instead of &String
in my third example, this is because String: Deref<str>
(You may need to scroll down to see it), and therefore anywhere I need a &str
I can chuck a &String
in because the compiler will autoderef.