16.3 Concurrent Shared State
16.3.1 Implementing Concurrency With Shared State
Remember the famous Go saying:
Do not communicate by sharing memory; instead, share memory by communicating.
The previous article, 16.2. Message Passing For Cross-Thread Data Transfer, implemented concurrency through communication. This article explains how to implement concurrency through shared memory. Go does not recommend this approach, but Rust supports concurrency through shared state.
The Channel from the previous article, 16.2. Message Passing For Cross-Thread Data Transfer, is somewhat like single ownership: once a value’s ownership is transferred into the Channel, you can no longer use it. Concurrent shared memory is somewhat like multiple ownership: multiple threads can access the same memory at the same time.
16.3.2 Using Mutex to Allow Only One Thread to Access Data
Mutex is short for mutual exclusion.
At any one time, Mutex allows only one thread to access certain data.
To access the data, a thread must first obtain the lock. In Rust, that means calling the lock method. The lock data structure is part of Mutex, and it keeps track of which thread has exclusive access to the data. Mutex is often described as protecting the data it holds by locking the system around it.
16.3.3 Two Rules of Mutex
- Before using the data, you must try to acquire the lock.
- After using the data protected by
Mutex, you must unlock it so that other threads can acquire the lock.
16.3.4 The Mutex<T> API
Create a Mutex<T> with Mutex::new, passing in the data to be protected. Mutex<T> is effectively a smart pointer.
Before accessing the data, use the lock method to acquire the lock. This method blocks the current thread. The lock method can also fail, so its return value is wrapped in Result. If it succeeds, the value inside Ok is a MutexGuard smart pointer, which implements Deref and Drop.
Take a look at this example:
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("m = {m:?}");
}
Mutex::newcreates a mutex protecting the value5, and that mutex is assigned tom. So the type ofmisMutex<i32>.- The
{}block creates a new inner scope. Inside that scope,lockis used to acquire the value, andunwrapis used for error handling. BecauseMutexGuardimplements theDereftrait, we can get a reference to the inner data. Sonumis a mutable reference. - Inside the inner scope, dereferencing
*is used to change the value to6. - Because
MutexGuardimplements theDroptrait, the mutex is automatically unlocked when the inner scope ends. - Finally, the updated contents inside the mutex are printed.
Output:
m = Mutex { data: 6, poisoned: false, .. }
16.3.5 Sharing Mutex<T> Across Threads
Take a look at this example:
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
counteris essentially a counter, just wrapped inMutexso that it can be used more easily in multiple threads. Its initial value is0.handlesis an emptyVector.- The loop below creates 10 threads, from 0 to 10 (not including 10), and stores each thread’s
handlein the empty collectionhandles. - Inside the thread closure, our intention is to move the
countermutex into the closure (so we usemove), then acquire the mutex and modify its value. Each thread adds 1. When a thread finishes,numgoes out of scope and the mutex is released, so other threads can use it. - In the loop from 0 to 10 (not including 10),
handlesis also iterated over andjoinis used so that the code continues only after every thread represented by eachhandlehas finished. - Finally, the main thread tries to acquire the mutex for
counterand prints it.
Output:
$ cargo run
Compiling shared-state v0.1.0 (/tmp/projects/shared-state)
error[E0382]: borrow of moved value: `counter`
--> src/main.rs:21:29
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
...
8 | for _ in 0..10 {
| -------------- inside of this loop
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
...
21 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value borrowed here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
The error occurs because ownership was already moved into the thread in the previous iteration of the loop, so this iteration can no longer take ownership of it.
So how can we put counter into multiple threads—that is, how can multiple threads own it?
16.3.6 Multiple Ownership Across Threads
In 15.5. Rc<T> - Reference-Counting Smart Pointer and Shared Ownership, we introduced a smart pointer with multiple ownership called Rc<T>. We can simply wrap counter in Rc:
#![allow(unused)]
fn main() {
let counter = Rc::new(Mutex::new(0));
}
Inside the loop, we need to clone it into the thread. Here we use variable shadowing to set the new counter value to a clone of the old one:
#![allow(unused)]
fn main() {
let counter = Rc::clone(&counter);
}
Modified code (remember to import Rc before using it):
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Output:
error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
| | |
| | required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<std::sync::Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /Users/stanyin/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/thread/functions.rs:128:8
|
125 | pub fn spawn<F, T>(f: F) -> JoinHandle<T>
| ----- required by a bound in this function
...
128 | F: Send + 'static,
| ^^^^ required by this bound in `spawn`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
Look at this part of the error message: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely. Rc<Mutex<i32>> cannot be transferred safely between threads. The compiler also tells us why: the trait `Send` is not implemented for `Rc<std::sync::Mutex<i32>>`. Rc<Mutex<i32>> does not implement the Send trait (which will be covered in the next article, 16.4. Extending Concurrency with Send and Sync Traits). Only types that implement Send can be transferred safely between threads.
In 15.5. Rc<T> - Reference-Counting Smart Pointer and Shared Ownership, we also said that Rc<T> cannot be used in multithreaded scenarios: Rc<T> cannot be safely shared across threads. It cannot guarantee that a count update will not be interrupted by another thread. That could cause an incorrect count, which could then lead to a memory leak or deleting a value before we are done with it. What we need is a type exactly like Rc<T> that updates the reference count in a thread-safe way.
So what should multithreaded code use? There is a smart pointer called Arc<T> that can handle this scenario.
16.3.7 Using Arc<T> for Atomic Reference Counting
Arc<T> is similar to Rc<T>, but it can be used in concurrent scenarios. The A in Arc stands for Atomic, meaning it is an atomic reference-counted type. Atomics are another concurrency primitive. This article will not go into Arc<T> in great detail; it is enough to know that atomics work like primitive types but can be safely shared across threads. For more information, see the official Rust documentation.
Why, then, are all primitive types not atomic by default? Why doesn’t the standard library use Arc<T> everywhere? Because thread safety has a performance cost that you only want to pay when you need it.
Fortunately, Arc<T> and Rc<T> have the same API, so the earlier code is easy to change (remember to import Arc before using it):
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
16.3.8 RefCell<T>/Rc<T> vs. Mutex<T>/Arc<T>
Mutex<T> provides interior mutability, just like the Cell family. We generally use RefCell<T> wrapped in Rc<T> to get a shared-ownership data type with interior mutability. Likewise, Mutex<T> can be used to mutate the contents inside Arc<T>.
When using Mutex<T>, Rust cannot protect you from various logical errors. Using Rc<T> carries the risk of creating reference cycles, where two Rc<T> values reference each other and cause a memory leak. Similarly, Mutex<T> also carries the risk of creating a deadlock. This happens when an operation needs to lock two resources and two threads each acquire one lock, causing them to wait forever for each other. The standard library API docs for Mutex<T> and MutexGuard are useful. See the Mutex<T> API docs and the MutexGuard API docs.