Arc and Mutex in Rust
Written by Piotr Sarnacki on May 28, 2022
Arc and Mutex in Rust
When writing concurrent Rust you will encounter `Arc` and `Mutex` types sooner or later. And although `Mutex` might already sound familiar as it's a concept known in many languages, chances are you haven't heard about `Arc` before Rust. What's more, you can't fully understand these concepts without tying them to the ownership model in Rust. This post is my take on understanding `Arc` and `Mutex`in Rust.
Typically when you share data in a concurrent environment you either share memory or pass data as messages. You might often hear that passing messages (for example by using channels) is a preferred way to handle concurrency, but in Rust I don't think the safety or correctness differences are as big as in other languages due to the ownership model in Rust. Or more specifically: you can't have a data race in safe Rust. That's why when I choose between message passing or memory sharing in Rust, I do it mostly in relation to convinience, not safety.
If you choose to share data by sharing memory you will quickly encounter that you can't do much without `Arc` and `Mutex`. `Arc` is a smart pointer that let's you safely share a value between multiple threads. `Mutex` is a wrapper over another type, which allows safe mutability across threads. In order to fully understand these concepts, though, let's dive into the ownership model.
Ownership in Rust
If you tried to distill the ownership model in Rust, you would probably get the following points:
- a value can have only one owner
- you can have multiple shared immutable references to a value
- you can have only one mutable reference to a value
Let's see how it plays out. Given a `User` struct containing a `String` field named `name` we create a thread and print out a message for the user:
1use std::thread::spawn;
2
3#[derive(Debug)]
4struct User {
5 name: String
6}
7
8fn main() {
9 let user = User { name: "drogus".to_string() };
10
11 spawn(move || {
12 println!("Hello from the first thread {}", user.name);
13 }).join().unwrap();
14}So far so good, the program compiles and prints the message. Now imagine we need to add a second thread that also has access to the `user` instance:
1fn main() {
2 let user = User { name: "drogus".to_string() };
3
4 let t1 = spawn(move || {
5 println!("Hello from the first thread {}", user.name);
6 });
7
8 let t2 = spawn(move || {
9 println!("Hello from the second thread {}", user.name);
10 });
11
12 t1.join().unwrap();
13 t2.join().unwrap();
14}With this code we get the following error:
error[E0382]: use of moved value: `user.name`
--> src/main.rs:15:20
|
11 | let t1 = spawn(move || {
| ------- value moved into closure here
12 | println!("Hello from the first thread {}", user.name);
| --------- variable moved due to use in closure
...
15 | let t2 = spawn(move || {
| ^^^^^^^ value used here after move
16 | println!("Hello from the second thread {}", user.name);
| --------- use occurs due to use in closure
|
= note: move occurs because `user.name` has type `String`, which does not implement the `Copy` traitWhat does the compiler want? The error reads "use of moved value `user.name`". The compiler is even nice enough to point us to specific places where the problem occurs. We first move the value to the first thread on line 11 and then we try to do the same thing with the second thread on line 15. If you look at the ownership rules, this shouldn't be surprising. A value can have only one owner. With the current version of the code we need to "move" the value to the first thread if we want to use it, and thus we can't move it to the other thread. It already changed ownership. But we don't mutate the data, right? Which means we can have multiple shared references. Let's try that.
1fn main() {
2 let user = User { name: "drogus".to_string() };
3
4 let t1 = spawn(|| {
5 println!("Hello from the first thread {}", &user.name);
6 });
7
8 let t2 = spawn(|| {
9 println!("Hello from the second thread {}", &user.name);
10 });
11
12 t1.join().unwrap();
13 t2.join().unwrap();
14}I removed the `move` keyword in the thread closures and I made the threads borrow the `user` value immutably, or in other words get a shared reference, which is represented by the ampersand. With this code we get the following:
error[E0373]: closure may outlive the current function, but it borrows `user.name`, which is owned by the current function
--> src/main.rs:15:20
|
15 | let t2 = spawn(|| {
| ^^ may outlive borrowed value `user.name`
16 | println!("Hello from the first thread {}", &user.name);
| --------- `user.name` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:15:14
|
15 | let t2 = spawn(|| {
| ____________^
16 | | println!("Hello from the second thread {}", &user.name);
17 | | });
| |____^
help: to force the closure to take ownership of `user.name` (and any other referenced variables), use the `move` keyword
|
15 | let t2 = spawn(move || {
| ++++Now the error says that the closure can outlive the function. In other words the Rust compiler can't guarantee that the closure in the thread will finish before the `main()` function finishes. Threads are borrowing the user struct, but it's still owned by the main function. In this scenario if the main function finishes, the user struct goes out of scope and the memory is dropped. Thus if it was allowed to share the value with threads in that manner, there could be a scenario when a thread is trying to read freed memory. Which is an undefined behaviour and we certainly don't want that.
The note also says that it may help to move the variable `user` to the thread in order to avoid borrowing, but we're just coming from that scenario, so it's no good. Now, there are two easy solutions to fix this and one of them is to use `Arc`, but let's explore the other solution first: scoped threads.
Scoped threads
Scoped threads is a feature available either from an excellent crossbeam crateor as an experimental nightly feature in Rust. For the purpose of this article I will use crossbeam, but the API is very similar for both versions. After adding `crossbeam = "0.8"` to dependencies in `Cargo.toml` this code will work without problems
1use crossbeam::scope;
2
3#[derive(Debug)]
4struct User {
5 name: String,
6}
7
8fn main() {
9 let user = User {
10 name: "drogus".to_string(),
11 };
12
13 scope(|s| {
14 s.spawn(|_| {
15 println!("Hello from the first thread {}", &user.name);
16 });
17
18 s.spawn(|_| {
19 println!("Hello from the second thread {}", &user.name);
20 });
21 })
22 .unwrap();
23}The way scoped threads work is all of the threads created in the scope are guaranteed to be finished before the `scope` closure finishes. Or in other words
One interesting thing to note here is that as a human reader we would have interpreted both of these programs as valid. In the version that Rust rejects we join both threads before the `main()` function finishes, so it would be actually safe to share the user value with the threads. This is, unfortunately, something that you may encounter when writing in Rust. Writing a compiler that would accept all of the valid programs is not possible, thus we're left with the next best thing: a compiler that will reject all invalid programs at a cost of being overly strict. Scoped threads is a future written specifically to allow us to write this code in a way that compiler can accept.
As useful as the scoped threads feature is, however, you can't always use it, for example when writing async code. Let's get to the `Arc` solution then.
Arc to the rescue
`Arc` is a smart pointer enabling sharing data between threads. Its name is a shortcut for "atomic reference counter". The way `Arc` works is essentially to wrap a value we're trying to share and act as a pointer to it. `Arc` keeps track of all of the copies of the pointer and as soon as the last pointer goes out of scope it can safely free the memory. The solution to our small problem with `Arc` would look something like this:
1use std::thread::spawn;
2use std::sync::Arc;
3
4#[derive(Debug)]
5struct User {
6 name: String
7}
8
9fn main() {
10 let user_original = Arc::new(User { name: "drogus".to_string() });
11
12 let user = user_original.clone();
13 let t1 = spawn(move || {
14 println!("Hello from the first thread {}", user.name);
15 });
16
17 let user = user_original.clone();
18 let t2 = spawn(move || {
19 println!("Hello from the first thread {}", user.name);
20 });
21
22 t1.join().unwrap();
23 t2.join().unwrap();
24}Let's go through it step by step. First, on line 10, we create a user value, but we also wrap it with an `Arc`. Now the value is stored in memory and `Arc`acts only as a pointer. Whenever we clone the `Arc` we only clone the reference, not the user value itself. On lines 12 and 17 we clone the `Arc`and thus a copy of the pointer is moved to each of the threads. As you can see `Arc` allows us to share the data regardless of the lifetimes. In this example we will have three pointers to the user value. One created when an `Arc`is created, one created by cloning before starting the first thread and moved to the first thread and one created by cloning before starting the second thread and moved the first thread. As long as any of these pointers is alive, Rust will not free the memory. But when both the threads and the main function finish, all of the `Arc`pointers will get out of scope, dropped and as soon as the last one drops, it will also drop the user value.
`Send` and `Sync`
Let's go a bit deeper, though. If you look at the `Arc` documentation, you will see it implements Send and Sync traits, but only if the wrapped type also implements both `Send` and `Sync`. In order to understand what it means and why it's implemented this way let's start by defining `Send` and `Sync`.
The Rustonomicon defines Send and Sync as:
Feel free to read about these traits on Rustonomicon, but I'll also try to share my understanding here. Both `Send` and `Sync` are traits acting as markers - they don't have any implemented methods nor they require you to implement anything. What they allow is to notify the compiler about a type's ability to be shared or sent between threads. Let's start with `Send`, which is a bit more straightforward. What it means is that you can't send type which is `!Send`(read: not `Send`) to another thread. For example you can't send it through a channel nor can you move it to a thread. For example this code will not compile:
1#![feature(negative_impls)]
2
3#[derive(Debug)]
4struct Foo {}
5impl !Send for Foo {}
6
7fn main() {
8 let foo = Foo {};
9 spawn(move || {
10 dbg!(foo);
11 });
12}`Send` and `Sync` are autoderived, meaning that for example if all of the attributes of a type are `Send`, the type will also be `Send`. This code uses an experimental feature called `negative_impls`, which lets us tell the compiler "I explicitly want to mark this type as `!Send`". Trying to compile this code will result in an error:
`Foo` cannot be sent between threads safelyThe same would happen if you created a channel to send `foo` to a thread. So now what with `Arc`? As you might have guessed it will also not help, this will also error out in the same way (and the same would be true for a `!Sync` type as `Arc` needs both traits):
1#![feature(negative_impls)]
2
3#[derive(Debug)]
4struct Foo {}
5impl !Send for Foo {}
6
7fn main() {
8 let foo = Arc::new(Foo {});
9 spawn(move || {
10 dbg!(foo);
11 });
12}Now, why is that the case? Isn't `Arc` supposed to be wrapping our type and give it more capabilities? While this is certainly true, `Arc` can't magically make our type threadsafe. I will give you a more in-depth example to show you why at the end of this article, but for now let's continue with learning on how to use these types.
What we learned so far is this: `Arc` enables us to share references to types that are `Send + Sync` between threads without us having to worry about lifetimes (because it's not a regular reference, but rather a smart pointer).
Modifying data with Mutex
Now let's talk about `Mutex`. Mutexes in many languages are treated like semaphores. You create a mutex object and you can guard a certain piece (or pieces) of the code with the mutex in a way that only one thread at a time can access the guarded place. In Rust `Mutex` behaves more like a wrapper. It consumes the underlying value and let's you access it only after locking the mutex. Typically `Mutex` is used with conjunction with `Arc` to make it easier to share it between threads. Let's look at the following example:
1use std::time::Duration;
2use std::{thread, thread::sleep};
3use std::sync::{Arc, Mutex};
4
5struct User {
6 name: String
7}
8
9fn main() {
10 let user_original = Arc::new(Mutex::new(User { name: String::from("drogus") }));
11
12 let user = user_original.clone();
13 let t1 = thread::spawn(move || {
14 let mut locked_user = user.lock().unwrap();
15 locked_user.name = String::from("piotr");
16 // after locked_user goes out of scope, mutex will be unlocked again,
17 // but you can also explicitly unlock it with:
18 // drop(locked_user);
19 });
20
21 let user = user_original.clone();
22 let t2 = thread::spawn(move || {
23 sleep(Duration::from_millis(10));
24
25 // it will print: Hello piotr
26 println!("Hello {}", user.lock().unwrap().name);
27 });
28
29 t1.join().unwrap();
30 t2.join().unwrap();
31}Let's go over it. in the first line of the `main()` function we create an instance of the `User` struct and we wrap it with a `Mutex` and an `Arc`. With an `Arc` we can easily clone the pointer and thus share the mutex between threads. In the 13th line you can see the mutex is locked and since that moment the underlying value can be used exclusively by this thread. Then we modify the value in the next line. The mutex is unlocked once the locked guard goes out of scope or we manually drop it with `drop(locked_user)`.
In the second thread we wait 10ms and print the name, which should be the name updated in the first thread. This time locking is done in one line, so the mutex will be dropped in the same statement.
One more thing that is worth mentioning is the `unwrap()` method we call after `lock()`. `Mutex` from the standard library has a notion of being poisoned. If a thread panics while the mutex is locked we can't be certain if the value inside `Mutex` is still valid and thus the default behaviour is to return an error instead of a guard. So `Mutex` can either return an `Ok()` variant with the wrapped value as an argument or an error. You can read more about it in the docs. In general leaving `unrwap()` methods in the production code is not recommended, but in case of `Mutex` it might be a valid strategy - if a mutex has been poisoned we might decide that the application state is invalid and crash the application.
Another interesting thing about `Mutex` is that as long as a type inside the `Mutex` is `Send`, Mutex will also be Sync. This is because `Mutex` ensures that only one thread can get access to the underlying value and thus it's safe to share `Mutex` between threads.
`Mutex`: add `Sync` to a `Send` type
As you may remember from the beginning of the article, `Arc` needs an underlying type to be `Send + Sync` in order for `Arc` to be `Send + Sync` too. `Mutex`only requires and underlying type to be `Send` in order for `Mutex` to be \`Send
`Mutex` without `Arc`?
An interesting question that you may ask is if `Mutex` can be used without `Arc`. I encourage you to think about it a little before reading further: what does it mean that `Mutex` is `Send + Sync` for types that are `Send`?
If you get back to the first part of this post you can see what it means for the `Arc` type and in case of `Mutex` it means a very similar thing. If we can use something like scope threads it's entirely possible to use `Mutex` without `Arc`:
1use crossbeam::scope;
2use std::{sync::Mutex, thread::sleep, time::Duration};
3
4#[derive(Debug)]
5struct User {
6 name: String,
7}
8
9fn main() {
10 let user = Mutex::new(User {
11 name: "drogus".to_string(),
12 });
13
14 scope(|s| {
15 s.spawn(|_| {
16 user.lock().unwrap().name = String::from("piotr");
17 });
18
19 s.spawn(|_| {
20 sleep(Duration::from_millis(10));
21
22 // should print: Hello piotr
23 println!("Hello {}", user.lock().unwrap().name);
24 });
25 })
26 .unwrap();
27}In this program we achieve the same goal. We are accessing a value behind a mutex in two separate threads, but we share mutexes by reference and not by using an `Arc`. But again, this is not always possible, for example in async code, so `Mutex` is very often used along with `Arc`.
Summary
I'm hoping that in this article I helped you understand what are `Arc` and `Mutex` types in Rust and how to use them. To sum it up I would say you would typically use `Arc` whenever you want to share data between threads and you can't do so using regular references. You would also use `Mutex` if you need to modify data you share between threads. And then you would use `Arc<Mutex<...>>` whenever you want to modify data you share between threads and you can't share a mutex using references.
Bonus: Why `Arc` needs type to be `Sync`
Now let's get back to the question of "why `Arc` needs the underlying type to be both `Send` and `Sync` to mark it as `Send` and `Sync`). Feel free to ignore this last section, though, it's not really needed for you to use `Arc` and `Mutex` in your code. It might help you understand markter traits a bit better.
Lets take `Cell` as an example. `Cell` wraps another type and enables "interior mutability" or in other words it allows us to modify a value inside an immutable struct. `Cell` is `Send`, but it's `!Sync`.
An example of using `Cell` would be:
1use std::cell::Cell;
2
3struct User {
4 age: Cell<usize>
5}
6
7fn main() {
8 let user = User { age: Cell::new(30) };
9
10 user.age.set(36);
11
12 // will print: Age: 36
13 println!("Age: {}", user.age.get());
14}`Cell` is useful in some situations, but it isn't thread safe or in other words it's `!Sync`. If you somehow shared a value wrapped in a cell between multiple threads you could modify the same place in memory from two threads, for example:
1// this example will not compile, `Cell` is `!Sync` and thus
2// `Arc` will be `!Sync` and `!Send`
3use std::cell::Cell;
4
5struct User {
6 age: Cell<usize>
7}
8
9fn main() {
10 let user_original = Arc::new(User { age: Cell::new(30) });
11
12 let user = user_original.clone();
13 std::thread::spawn(move || {
14 user.age.set(2);
15 });
16
17 let user = user_original.clone();
18 std::thread::spawn(move || {
19 user.age.set(3);
20 });
21}If that worked, it could result in an undefined behaviour. That's why `Arc` will not work with any type that is not `Send` nor `Sync`. At the same time `Cell`is `Send`, meaning that you can send it between threads. Why is that? Sending, or in other words moving, will not make a value accessible from more than one thread, it will have to always be only one thread. Once you move it to another, the previous thread doesn't own the value anymore. With that in mind, we can always mutate a `Cell` locally.
Bonus: why `Arc` needs type
At this point you might also wonder why `Arc` will not provide the `Send` trait for a `!Send` type, either. One of the types in Rust which is `!Send` is `Rc`. `Rc` is a cousin of `Arc`, but it's not "atomic", `Rc` expands to just "reference counter". Its role is pretty much the same as `Arc`, but it can only be used in a single thread. Not only it can't be shared between threads, but also it can't be moved between threads. Let's see why.
1// this code won't compile, Rc is !Send and !Sync
2use std::rc::Rc;
3
4fn main() {
5 let foo = Rc::new(1);
6
7 let foo_clone = foo.clone();
8 std::thread::spawn(move || {
9 dbg!(foo_clone);
10 });
11
12 let foo_clone = foo.clone();
13 std::thread::spawn(move || {
14 dbg!(foo_clone);
15 });
16}This example won't compile, because `Rc` is `!Sync + !Send`. Its internal counter is not atomic and thus sharing it between threads could result in an inaccurate count of references. Now imagine that `Arc` would make `!Send` types `Send`:
1use std::rc::Rc;
2use std::sync::Arc;
3
4#[derive(Debug)]
5struct User {
6 name: Rc<String>,
7}
8unsafe impl Send for User {}
9unsafe impl Sync for User {}
10
11fn main() {
12 let foo = Arc::new(User {
13 name: Rc::new(String::from("drogus")),
14 });
15
16 let foo_clone = foo.clone();
17 std::thread::spawn(move || {
18 let name = foo_clone.name.clone();
19 });
20
21 let foo_clone = foo.clone();
22 std::thread::spawn(move || {
23 let name = foo_clone.name.clone();
24 });
25}This example will compile, but it's wrong, please don't do it in your actual code! In here I define a `User` struct, which holds an `Rc` inside. Because `Send` and `Sync` are autoderived and `Rc` is `!Send + !Sync`, the `User` struct should also be `!Send + !Sync`, but we can explicitly tell the compiler to mark it differently, in this case `Send + Sync`, using `unsafe impl` syntax.
Now you can clearly see what would go wrong if `Arc` allowed `!Send` types to be moved between threads. In the example `Arc` clones are moved into separate threads and then nothing is stopping us from cloning the `Rc` type. And because `Rc` type is not thread safe, it could result in an inacurate count of references and thus could either free memory too soon or it could not free it at all even though it should.
I know that this article is a long one, so kudos too all of you that got it all the way here, thanks!
---
If you like this post please consider following me on Twitter.