enaJ
enaJ

Reputation: 1655

What happens when an Arc is cloned?

I am learning concurrency and want to clarify my understanding on the following code example from the Rust book. Please correct me if I am wrong.

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

fn main() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));

    for i in 0..3 {
        let data = data.clone();
        thread::spawn(move || {
            let mut data = data.lock().unwrap();
            data[0] += i;
        });
    }

    thread::sleep(Duration::from_millis(50));
}

What is happening on the line let data = data.clone()?

The Rust book says

we use clone() to create a new owned handle. This handle is then moved into the new thread.

What is the new "owned handle"? It sounds like a reference to the data?

Since clone takes a &self and returns a Self, is each thread modifying the original data instead of a copy? I guess that is why the code is not using data.copy() but data.clone() here.

The data on the right side is a reference, and the data on the left is a owned value. There is a variable shadowing here.

Upvotes: 57

Views: 17919

Answers (3)

Lukas Kalbertodt
Lukas Kalbertodt

Reputation: 88656

[...] what is happening on let data = data.clone()?

Arc stands for Atomically Reference Counted. An Arc manages one object (of type T) and serves as a proxy to allow for shared ownership, meaning: one object is owned by multiple names. Wow, that sounds abstract, let's break it down!

Shared Ownership

Let's say you have an object of type Turtle 🐢 which you bought for your family. Now the problem arises that you can't assign a clear owner of the turtle: every family-member kind of owns that pet! This means (and sorry for being morbid here) that if one member of the family dies, the turtle won't die with that family-member. The turtle will only die if all members of the family are gone as well. Everyone owns and the last one cleans up.

So how would you express that kind of shared ownership in Rust? You will quickly notice that it's impossible to do with only standard methods: you'd always have to choose one owner and everyone else would only have a reference to the turtle. Not good!

So along come Rc and Arc (which, for the sake of this story, serve the exact same purpose). These allow for shared ownership by tinkering a bit with unsafe-Rust. Let's look at the memory after executing the following code (note: the memory layout is for learning and might not represent the exact same memory layout from the real world):

let annas = Rc::new(Turtle { legs: 4 });

Memory:

  Stack                    Heap
  -----                    ----


  annas:
+--------+               +------------+
| ptr: o-|-------------->| count: 1   |
+--------+               | data: 🐢   |
                         +------------+

We see that the turtle lives on the heap... next to a counter which is set to 1. This counter knows how many owners the object data currently has. And 1 is correct: annas is the only one owning the turtle right now. Let's clone() the Rc to get more owners:

let peters = annas.clone();
let bobs = annas.clone();

Now the memory looks like this:

  Stack                    Heap
  -----                    ----


  annas:
+--------+               +------------+
| ptr: o-|-------------->| count: 3   |
+--------+    ^          | data: 🐢   |
              |          +------------+
 peters:      |
+--------+    |
| ptr: o-|----+
+--------+    ^
              |
  bobs:       |
+--------+    |
| ptr: o-|----+
+--------+

As you can see, the turtle still exists only once. But the reference count was increased and is now 3, which makes sense, because the turtle has three owners now. All those three owners reference this memory block on the heap. That's what the Rust book calls owned handle: each owner of such a handle also kind of owns the underlying object.

(also see "Why is std::rc::Rc<> not Copy?")

Atomicity and Mutability

What's the difference between Arc<T> and Rc<T> you ask? The Arc increments and decrements its counter in an atomic fashion. That means that multiple threads can increment and decrement the counter simultaneously without a problem. That's why you can send Arcs across thread-boundaries, but not Rcs.

Now you notice that you can't mutate the data through an Arc<T>! What if your 🐢 loses a leg? Arc is not designed to allow mutable access from multiple owners at (possibly) the same time. That's why you often see types like Arc<Mutex<T>>. The Mutex<T> is a type that offers interior mutability, which means that you can get a &mut T from a &Mutex<T>! This would normally conflict with the Rust core principles, but it's perfectly safe because the mutex also manages access: you have to request access to the object. If another thread/source currently has access to the object, you have to wait. Therefore, at one given moment in time, there is only one thread able to access T.

Conclusion

[...] is each thread modifying the original data instead of a copy?

As you can hopefully understand from the explanation above: yes, each thread is modifying the original data. A clone() on an Arc<T> won't clone the T, but merely create another owned handle; which in turn is just a pointer that behaves as if it owns the underlying object.

Upvotes: 164

Shepmaster
Shepmaster

Reputation: 430791

std::sync::Arc is a smart pointer, one that adds the following abilities:

An atomically reference counted wrapper for shared state.

Arc (and its non-thread-safe friend std::rc::Rc) allow shared ownership. That means that multiple "handles" point to the same value. Whenever a handle is cloned, a reference counter is incremented. Whenever a handle is dropped, the counter is decremented. When the counter goes to zero, the value that the handles were pointing to is freed.

Note that this smart pointer does not call the underlying clone method of the data; in fact, there may doesn't need to be an underlying clone method! Arc handles what happens when clone is called.

What is the new "owned handle"? It sounds like a reference to the data?

It both is and isn't a reference. In the broader programming and English sense of the word "reference", it is a reference. In the specific sense of a Rust reference (&Foo), it is not a reference. Confusing, right?


The second part of your question is about std::sync::Mutex, which is described as:

A mutual exclusion primitive useful for protecting shared data

Mutexes are common tools in multithreaded programs, and are well-described elsewhere so I won't bother repeating that here. The important thing to note is that a Rust Mutex only gives you the ability to modify shared state. It is up to the Arc to allow multiple owners to have access to the Mutex to even attempt to modify the state.

This is a bit more granular than other languages, but allows for these pieces to be reused in novel ways.

Upvotes: 5

Simon Whitehead
Simon Whitehead

Reputation: 65079

I am not an expert on the standard library internals and I am still learning Rust.. but here is what I can see: (you could check the source yourself too if you wanted).

Firstly, an important thing to remember in Rust is that it is actually possible to step outside the "safe bounds" that the compiler provides, if you know what you're doing. So attempting to reason about how some of the standard library types work internally, with the ownership system as your base of understanding may not make lots of sense.

Arc is one of the standard library types that sidesteps the ownership system internally. It essentially manages a pointer all by itself and calling clone() returns a new Arc that points at the exact same piece of memory the original did.. with an incremented reference count.

So on a high level, yes, clone() returns a new Arc instance and the ownership of that new instance is moved into the left hand side of the assignment. However, internally the new Arc instance still points where the old one did.. via a raw pointer (or as it appears in the source, via a Shared instance, which is a wrapper around a raw pointer). The wrapper around the raw pointer is what I imagine the documentation refers to as an "owned handle".

Upvotes: 6

Related Questions