calvin
calvin

Reputation: 2925

Why is mutating an owned value and borrowed reference safe in Rust?

In How does Rust prevent data races when the owner of a value can read it while another thread changes it?, I understand I need &mut self, when we want to mutate an object, even when the method is called with an owned value.

But how about primitive values, like i32? I ran this code:

fn change_aaa(bbb: &mut i32) {
    *bbb = 3;
}

fn main() {
    let mut aaa: i32 = 1;
    change_aaa(&mut aaa); // somehow run this asynchronously
    aaa = 2; // ... and will have data race here
}

My questions are:

  1. Is this safe in a non concurrent situation? According to The Rust Programming Language, if we think of the owned value as a pointer, it is not safe according the following rules, however, it compiles.

    Two or more pointers access the same data at the same time.

    At least one of the pointers is being used to write to the data.

    There’s no mechanism being used to synchronize access to the data.

  2. Is this safe in a concurrent situation? I tried, but I find it hard to put change_aaa(&mut aaa) into a thread, according to Why can't std::thread::spawn accept arguments in Rust? and How does Rust prevent data races when the owner of a value can read it while another thread changes it?. However, is it designed to be hard or impossible to do this, or just because I am unfamiliar with Rust?

Upvotes: 2

Views: 741

Answers (2)

user4815162342
user4815162342

Reputation: 154876

The signature of change_aaa doesn't allow it to move the reference into another thread. For example, you might imagine a change_aaa() implemented like this:

fn change_aaa(bbb: &mut i32) {
    std::thread::spawn(move || {
        std::thread::sleep(std::time::Duration::from_secs(1));
        *bbb = 100; // ha ha ha - kaboom!
    });
}

But the above doesn't compile. This is because, after desugaring the lifetime elision, the full signature of change_aaa() is:

fn change_aaa<'a>(bbb: &'a mut i32)

The lifetime annotation means that change_aaa must support references of any lifetime 'a chosen by the caller, even a very short one, such as one that invalidates the reference as soon as change_aaa() returns. And this is exactly how change_aaa() is called from main(), which can be desugared to:

let mut aaa: i32 = 1;
{
    let aaa_ref = &mut aaa;
    change_aaa(aaa_ref);
    // aaa_ref goes out of scope here, and we're free to mutate
    // aaa as we please
}
aaa = 2; // ... and will have data race here

So the lifetime of the reference is short, and ends just before the assignment to aaa. On the other hand, thread::spawn() requires a function bound with 'static lifetime. That means that the closure passed to thread::spawn() must either only contain owned data, or references to 'static data (data guaranteed to last until the end of the program). Since change_aaa() accepts bbb with with lifetime shorter than 'static, it cannot pass bbb to thread::spawn().

To get a grip on this you can try to come up with imaginative ways to write change_aaa() so that it writes to *bbb in a thread. If you succeed in doing so, you will have found a bug in rustc. In other words:

However, is it designed to be hard or impossible to do this, or just because I am unfamiliar with Rust?

It is designed to be impossible to do this, except through types that are explicitly designed to make it safe (e.g. Arc to prolong the lifetime, and Mutex to make writes data-race-safe).

Upvotes: 4

Masklinn
Masklinn

Reputation: 42217

Is this safe in a non concurrent situation? According to this post, if we think owned value if self as a pointer, it is not safe according the following rules, however, it compiles.

Two or more pointers access the same data at the same time. At least one of the pointers is being used to write to the data. There’s no mechanism being used to synchronize access to the data.

It is safe according to those rules: there is one pointer accessing data at line 2 (the pointer passed to change_aaa), then that pointer is deleted and another pointer is used to update the local.

Is this safe in a concurrent situation? I tried, but I find it hard to put change_aaa(&mut aaa) into a thread, according to post and post. However, is it designed to be hard or impossible to do this, or just because I am unfamiliar with Rust?

While it is possible to put change_aaa(&mut aaa) in a separate thread using scoped threads, the corresponding lifetimes will ensure the compiler rejects any code trying to modify aaa while that thread runs. You will essentially have this failure:

fn main(){
    let mut aaa: i32 = 1;
    let r = &mut aaa;
    aaa = 2;
    println!("{}", r);
}
error[E0506]: cannot assign to `aaa` because it is borrowed
  --> src/main.rs:10:5
   |
9  |     let r = &mut aaa;
   |             -------- borrow of `aaa` occurs here
10 |     aaa = 2;
   |     ^^^^^^^ assignment to borrowed `aaa` occurs here
11 |     println!("{}", r);
   |                    - borrow later used here

Upvotes: 1

Related Questions