Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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::new creates a mutex protecting the value 5, and that mutex is assigned to m. So the type of m is Mutex<i32>.
  • The {} block creates a new inner scope. Inside that scope, lock is used to acquire the value, and unwrap is used for error handling. Because MutexGuard implements the Deref trait, we can get a reference to the inner data. So num is a mutable reference.
  • Inside the inner scope, dereferencing * is used to change the value to 6.
  • Because MutexGuard implements the Drop trait, 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());
}
  • counter is essentially a counter, just wrapped in Mutex so that it can be used more easily in multiple threads. Its initial value is 0.
  • handles is an empty Vector.
  • The loop below creates 10 threads, from 0 to 10 (not including 10), and stores each thread’s handle in the empty collection handles.
  • Inside the thread closure, our intention is to move the counter mutex into the closure (so we use move), then acquire the mutex and modify its value. Each thread adds 1. When a thread finishes, num goes out of scope and the mutex is released, so other threads can use it.
  • In the loop from 0 to 10 (not including 10), handles is also iterated over and join is used so that the code continues only after every thread represented by each handle has finished.
  • Finally, the main thread tries to acquire the mutex for counter and 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.