Why is that code fragment memory safe?

let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);

What if println! was threaded and r1 and r2 are still used after/during the next println! statement?

The println macro expands to code which doesn't require 'static bounds in any functions it calls.

Rust has special traits (Sync and Send) as a way to guarantee memory safety across threads. Read about concurrency in Rust and you'll find out about them.

1 Like

Threading APIs ensure memory safety using lifetimes and trait bounds.

But then I don't understand the multi threaded by default paradigma.

Why isn't it possible then to read and write to references when they are synced (locked) anyway?

What do you mean by "multi-threaded by default paradigm"? Again, println is not multi-threaded.

I don't get what you mean by this. References are not implicitly synchronized, if that's what you are asking.

Yes, imagine println! could be any function.
Rust was mentioned multi threaded per default or fearless concurrency, so I thought it would be safe to automatically make any function call threaded.

Hmm okay, that's possibly the reason then.
I will experiment a little bit further with it.

In the hypothetical where println! was threaded, then it would have to require that its inputs are 'static (i.e. not references to local data) and the code wouldn't compile — similarly to how std::thread::spawn() does require that its inputs are 'static.

2 Likes

Here's a case where a mutable reference is used by a thread:

use std::thread;

fn main() {
    let mut a = vec![1, 2, 3];
    let r = &mut a;
    println!("r is {:?}", r);

    thread::scope(|s| {
        s.spawn(|| {
            println!("in another thread");
            r.push(4);
            println!("r is {:?}", r);
        });
    });

    println!("back in main thread");
    r.push(5);
    println!("r is {:?}", r);
    println!("a is {:?}", a);
}

Vec<i32> is both Sync and Send. The thread's lambda reborrows r. Since r is a &mut, the borrow checker prevents another thread's lambda from also reborrowing it. Try to duplicate the s.spawn(...) block; the borrow checker will complain. The borrow checker also prevents the main thread from accessing it (that would be another reborrow) while the thread::scope is still active. After the thread::scope ends, it's safe for the main thread to access it again.

1 Like

There's nothing multi-threaded "by default". Rust programs are thread-safe. This doesn't necessarily mean that they are multi-threaded. It only means that if you are going to create threads, then the language still ensures safety.

As already mentioned, the way it ensures thread safety is lifetime and trait bounds. Unbounded threads require 'static values, scoped threads require that the spawned function borrows for 'scope at most, and threading in general requires Send and/or Sync types.

But since printing isn't threaded, there's absolutely no need for any such bound, it would only be overly restrictive.

3 Likes

It is safe to make any function call threaded, as long as it meets the required lifetime and trait bounds.

I think my question is answered with your answer because why not use reborrow immutable again after mutable borrow and afterwards mutable borrow again: Why can't you use a mutably borrowed value in-between mutable changes? - #5 by H2CO3

It seems Rust wasn't designed that way or it is too complex to verify for.

I'm increasingly confused by what your actual question or problem is. Threading has nothing to do with exclusivity of mutable borrows. Mutable aliasing causes memory-unsafety in single-threaded code too. It's not because "Rust is not designed" with threads in mind or anything like that. Shared mutability is disallowed because it causes bugs, period. Disallowing it is not a bug, it's a feature.

Well, I thought I couldn't borrow immutable after borrowing mutable because of "multi threaded by default" behavior, but it seems to be another reason why this isn't possible.

People often call regular borrows shared borrows and mutable borrows exclusive borrows. It helps emphasize the exclusivety property, which is a major foundation of Rust's safety.

1 Like

No.

1 Like