BLukash
BLukash

Reputation: 43

What will happen in Rust if create mutable variable and mutable reference and change one of them in separate thread?

I am a complete newbie in Rust and have just started getting into its ownership system (4-th chapter of the book). Compiler handles a lot of errors that may occur with values and references, but lets imagine situation below:

Let's assume there is code like this, but change_string opens a new thread and perform mutations there.

fn main() {
    let mut s = String::from("Hello");

    // perform in thread, so flow will not be synchronous
    change_string(&mut s);

    s.push_str("..........");
    println!("{}", s);
}

fn change_string(some_string: &mut String) {
    some_string.push_str(", World");
}

Currently I receive Hello, World.........., but what will happen if adding of , World will be in the separate thread?

Upvotes: 2

Views: 880

Answers (1)

rodrigo
rodrigo

Reputation: 98516

Rust will not let you do unsafe things (without using the unsafe keyword), that includes anything related with threads. Why not just try and see how it fails to compile?

fn change_string(some_string: &mut String) {
    std::thread::spawn(move || {
            some_string.push_str(", World");
    });
}

It produces this error:

error[E0621]: explicit lifetime required in the type of `some_string`
  --> src/main.rs:14:5
   |
13 | fn change_string_1(some_string: &mut String) {
   |                                 ----------- help: add explicit lifetime `'static` to the type of `some_string`: `&'static mut std::string::String`
14 |     std::thread::spawn(move || {
   |     ^^^^^^^^^^^^^^^^^^ lifetime `'static` required

This means that you cannot pass a reference with a non-static lifetime to a thread, because the thread could live longer than such reference, and that would be unsafe.

You were probably thinking about issues with data races, but those are actually not the problem. The real problem is that your String may live as long as the thread. Actually, if you use scoped threads, from the crossbeam crate, it will just work:

fn change_string(some_string: &mut String) {
    crossbeam::scope(|s| {
        s.spawn(|_| {
            some_string.push_str(", World");
        });
    }).unwrap();
}

Now it works because the crossbeam::scope() call will not return until all the threads spawned inside are completed. And thus the lifetime of your String is always strictly longer than the thread and everything just works.

But what about data races? There are none, because &mut references in Rust are unique. This means that you cannot have two of them pointing to the same object, so it doesn't matter from what thread you are changing your object (as long as your object implements Sync but most do), you are doing it from just one, and there are no races.

If you try to create two threads to modify the object, even with crossbeam:

fn change_string(some_string: &mut String) {
    crossbeam::scope(|s| {
        s.spawn(|_| {
            some_string.push_str(", World");
        });
        s.spawn(|_| {
            some_string.push_str(", World");
        });
    }).unwrap();
}

You get this error message:

error[E0524]: two closures require unique access to `some_string` at the same time

Remember that &mut means unique access in addition to ability to mutate.

And if you try to move the reference into the closures instead of borrowing it:

fn change_string(some_string: &mut String) {
    crossbeam::scope(|s| {
        s.spawn(move |_| {
            some_string.push_str(", World");
        });
        s.spawn(move |_| {
            some_string.push_str(", World");
        });
    }).unwrap();
}

You get this one in the second closure:

error[E0382]: use of moved value: `some_string`

because the reference was already moved into the first one and cannot be used again.

Of course you can spawn a thread from the thread and that will work:

fn change_string(some_string: &mut String) {
    crossbeam::scope(|s| {
        s.spawn(|s| {
            some_string.push_str(", World");
            s.spawn(|_| {
                some_string.push_str(", World");
            });
        });
    }).unwrap();
}

But notice how it is still totally race free, because once the second thread is created the first one loses access to the unique mutable reference.

Upvotes: 4

Related Questions