Contents

Ch.16 — Fearless Concurrency

Chapter 16

Fearless Concurrency


Handling concurrent programming safely and efficiently is another of Rust’s major goals. Historically, programming with threads has been difficult and error-prone; Rust uses its ownership and type systems to turn many concurrency errors into compile-time errors. This chapter covers:

  • 16.1 Using Threads to Run Code Simultaneously — how to create threads to run multiple pieces of code at the same time
  • 16.2 Transfer Data Between Threads with Message Passing — message-passing concurrency, where channels send messages between threads
  • 16.3 Shared-State Concurrency — shared-state concurrency, where multiple threads have access to some piece of data
  • 16.4 Extensible Concurrency with Send and Sync — the traits that extend Rust’s concurrency guarantees to user-defined types as well as types provided by the standard library

The Rust Programming Language · Ch.16 Fearless Concurrency

1

Chapter 16 · Introduction

Concurrency, parallelism and “fearless concurrency”

Definition 16.1 (Concurrent programming). Programming in which different parts of a program execute independently.

Definition 16.2 (Parallel programming). Programming in which different parts of a program execute at the same time.

Both are becoming increasingly important as more computers take advantage of their multiple processors. Historically, programming in these contexts has been difficult and error-prone. Rust hopes to change that.

The Rust team first thought memory safety and preventing concurrency problems were two separate challenges needing different methods. Over time they discovered that the ownership and type systems help manage memory safety and concurrency problems. By leveraging ownership and type checking, many concurrency errors are compile-time errors in Rust rather than runtime errors: instead of spending lots of time reproducing the exact circumstances of a runtime concurrency bug, incorrect code refuses to compile and presents an error explaining the problem. You fix your code while working on it, not after it has shipped to production.

Definition 16.3 (Fearless concurrency). The nickname for this aspect of Rust: it lets you write code that is free of subtle bugs and is easy to refactor without introducing new bugs.

The Rust Programming Language · Ch.16 Fearless Concurrency

2

Chapter 16 · Introduction

A variety of tools, not one dogma

Remark. For simplicity’s sake, the book refers to many of the problems as concurrent rather than being more precise by saying concurrent and/or parallel. For this chapter, mentally substitute concurrent and/or parallel whenever it says concurrent. In the next chapter, where the distinction matters more, it is more specific.

Many languages are dogmatic about the solutions they offer for handling concurrent problems. For example, Erlang has elegant functionality for message-passing concurrency but has only obscure ways to share state between threads.

  • Supporting only a subset of possible solutions is a reasonable strategy for higher-level languages, because a higher-level language promises benefits from giving up some control to gain abstractions.
  • Lower-level languages, however, are expected to provide the solution with the best performance in any given situation, and have fewer abstractions over the hardware.

Therefore, Rust offers a variety of tools for modeling problems in whatever way is appropriate for your situation and requirements: threads, message passing, shared state, and the Send and Sync traits.

The Rust Programming Language · Ch.16 Fearless Concurrency

3

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Processes, threads and the problems they bring

Definition 16.4 (Process). In most current operating systems, the unit in which an executed program’s code is run; the OS manages multiple processes at once.

Definition 16.5 (Thread). The feature that runs independent parts of a program simultaneously, within one program.

For example, a web server could have multiple threads so that it can respond to more than one request at the same time. Splitting the computation into multiple threads to run multiple tasks at the same time can improve performance, but it also adds complexity. Because threads can run simultaneously, there’s no inherent guarantee about the order in which parts of your code on different threads will run. This can lead to problems, such as:

  • Race conditions, in which threads are accessing data or resources in an inconsistent order;
  • Deadlocks, in which two threads are waiting for each other, preventing both threads from continuing;
  • Bugs that only happen in certain situations and are hard to reproduce and fix reliably.

The Rust Programming Language · Ch.16 Fearless Concurrency

4

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

The 1:1 threading model, and thread::spawn

Rust attempts to mitigate the negative effects of threads, but multithreaded programming still takes careful thought and a code structure different from that of single-threaded programs.

Languages implement threads in a few different ways; many operating systems provide an API the language can call to create new threads.

Definition 16.6 (1:1 model). The model of thread implementation used by the Rust standard library, whereby a program uses one operating system thread per one language thread.

Crates implement other threading models with different trade-offs. (Rust’s async system, in the next chapter, is another approach to concurrency as well.)

Creating a new thread with spawn

To create a new thread, call the thread::spawn function and pass it a closure (Chapter 13) containing the code you want to run in the new thread. Listing 16.1, on the next slide, prints some text from a main thread and other text from a new thread.

The Rust Programming Language · Ch.16 Fearless Concurrency

5

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Spawning a thread alongside the main thread

Listing 16.1: Creating a new thread to print one thing while the main thread prints something else — src/main.rs

use std::thread;

use std::time::Duration;

 

fn main() {

    thread::spawn(|| {

        for i in 1..10 {

            println!("hi number {i} from the spawned thread!");

            thread::sleep(Duration::from_millis(1));

        }

    });

 

    for i in 1..5 {

        println!("hi number {i} from the main thread!");

        thread::sleep(Duration::from_millis(1));

    }

}

The Rust Programming Language · Ch.16 Fearless Concurrency

6

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Running Listing 16.1: no guaranteed order

When the main thread of a Rust program completes, all spawned threads are shut down, whether or not they have finished running. The output might be a little different every time, but it will look similar to this:

hi number 1 from the main thread!

hi number 1 from the spawned thread!

hi number 2 from the main thread!

hi number 2 from the spawned thread!

hi number 3 from the main thread!

hi number 3 from the spawned thread!

hi number 4 from the main thread!

hi number 4 from the spawned thread!

hi number 5 from the spawned thread!

The calls to thread::sleep force a thread to stop its execution for a short duration, allowing a different thread to run. The threads will probably take turns, but that isn’t guaranteed: it depends on how your operating system schedules the threads.

  • In this run, the main thread printed first, even though the spawned thread’s print statement appears first in the code.
  • Although the spawned thread was told to print until i is 9, it only got to 5 before the main thread shut down.

Remark. If you only see output from the main thread, or no overlap, increase the numbers in the ranges to give the OS more opportunities to switch between the threads.

The Rust Programming Language · Ch.16 Fearless Concurrency

7

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Waiting for all threads to finish: JoinHandle<T>

Listing 16.1 has two problems:

  • most of the time, the spawned thread is stopped prematurely because the main thread ends;
  • because there is no guarantee on the order in which threads run, we can’t even guarantee that the spawned thread will get to run at all!

Both are fixed by saving the return value of thread::spawn in a variable. Its return type is JoinHandle<T>.

Definition 16.7 (JoinHandle<T>). The owned value returned by thread::spawn. Calling the join method on it waits for its thread to finish.

Listing 16.2 (next slides) uses the JoinHandle<T> of the thread created in Listing 16.1 and calls join to make sure the spawned thread finishes before main exits.

The Rust Programming Language · Ch.16 Fearless Concurrency

8

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Saving the handle returned by thread::spawn

The spawned thread is the same as in Listing 16.1; the only change so far is that its JoinHandle<T> is kept in handle.

Listing 16.2: Saving a JoinHandle<T> from thread::spawn to guarantee the thread is run to completion — src/main.rs

use std::thread;

use std::time::Duration;

 

fn main() {

    let handle = thread::spawn(|| {

        for i in 1..10 {

            println!("hi number {i} from the spawned thread!");

            thread::sleep(Duration::from_millis(1));

        }

    });

The Rust Programming Language · Ch.16 Fearless Concurrency

9

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Calling join blocks until the thread terminates

Listing 16.2 (continued) — src/main.rs

 

    for i in 1..5 {

        println!("hi number {i} from the main thread!");

        thread::sleep(Duration::from_millis(1));

    }

 

    handle.join().unwrap();

}

Calling join on the handle blocks the thread currently running until the thread represented by the handle terminates.

Definition 16.8 (Blocking). Blocking a thread means that thread is prevented from performing work or exiting.

Because the call to join comes after the main thread’s for loop, the main thread runs its loop first and only then waits for the spawned thread.

The Rust Programming Language · Ch.16 Fearless Concurrency

10

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Running Listing 16.2: the spawned thread completes

Output similar to this:

hi number 1 from the main thread!

hi number 2 from the main thread!

hi number 1 from the spawned thread!

hi number 3 from the main thread!

hi number 2 from the spawned thread!

hi number 4 from the main thread!

hi number 3 from the spawned thread!

hi number 4 from the spawned thread!

hi number 5 from the spawned thread!

hi number 6 from the spawned thread!

hi number 7 from the spawned thread!

hi number 8 from the spawned thread!

hi number 9 from the spawned thread!

The two threads continue alternating, but the main thread waits because of the call to handle.join() and does not end until the spawned thread is finished.

This time the spawned thread gets all the way to 9: once the main thread’s loop is done, it blocks on join while the spawned thread prints its remaining lines.

The Rust Programming Language · Ch.16 Fearless Concurrency

11

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

What if join comes before the main loop?

Now see what happens when we instead move handle.join() before the for loop in main. The program starts as before:

src/main.rs

use std::thread;

use std::time::Duration;

 

fn main() {

    let handle = thread::spawn(|| {

        for i in 1..10 {

            println!("hi number {i} from the spawned thread!");

            thread::sleep(Duration::from_millis(1));

        }

    });

The Rust Programming Language · Ch.16 Fearless Concurrency

12

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

join before the loop: the main thread waits first

src/main.rs (continued)

 

    handle.join().unwrap();

 

    for i in 1..5 {

        println!("hi number {i} from the main thread!");

        thread::sleep(Duration::from_millis(1));

    }

}

The main thread will wait for the spawned thread to finish and only then run its for loop, so the output won’t be interleaved anymore (next slide).

The Rust Programming Language · Ch.16 Fearless Concurrency

13

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Where join is called matters

Output with join before the loop — no interleaving:

hi number 1 from the spawned thread!

hi number 2 from the spawned thread!

hi number 3 from the spawned thread!

hi number 4 from the spawned thread!

hi number 5 from the spawned thread!

hi number 6 from the spawned thread!

hi number 7 from the spawned thread!

hi number 8 from the spawned thread!

hi number 9 from the spawned thread!

hi number 1 from the main thread!

hi number 2 from the main thread!

hi number 3 from the main thread!

hi number 4 from the main thread!

The main thread blocks on join until the spawned thread has printed all nine lines, and only then runs its own loop.

Remark. Small details, such as where join is called, can affect whether or not your threads run at the same time.

The Rust Programming Language · Ch.16 Fearless Concurrency

14

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Using move closures with threads

We’ll often use the move keyword with closures passed to thread::spawn, because the closure then takes ownership of the values it uses from the environment, thus transferring ownership of those values from one thread to another.

Chapter 13 (“Capturing References or Moving Ownership”) discussed move in the context of closures. Here the focus is on the interaction between move and thread::spawn.

  • In Listing 16.1, the closure passed to thread::spawn takes no arguments: the spawned thread’s code uses no data from the main thread.
  • To use data from the main thread in the spawned thread, the spawned thread’s closure must capture the values it needs.
  • Listing 16.3 (next slide) attempts to create a vector in the main thread and use it in the spawned thread. However, this won’t work yet, as you’ll see in a moment.

The Rust Programming Language · Ch.16 Fearless Concurrency

15

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Capturing a vector from the main thread

Listing 16.3: Attempting to use a vector created by the main thread in another thread — src/main.rs

use std::thread;

 

fn main() {

    let v = vec![1, 2, 3];

 

    let handle = thread::spawn(|| {

        println!("Here's a vector: {v:?}");

    });

 

    handle.join().unwrap();

}

The closure uses v, so it will capture v and make it part of the closure’s environment. Because thread::spawn runs this closure in a new thread, we should be able to access v inside that new thread. But when we compile this example, we get the error on the next two slides.

The Rust Programming Language · Ch.16 Fearless Concurrency

16

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Error E0373: the closure may outlive v

Compiling Listing 16.3 fails. The first part of the error points at the closure and at the use of v inside it:

$ cargo run

   Compiling threads v0.1.0 (file:///projects/threads)

error[E0373]: closure may outlive the current function, but it borrows `v`,

    which is owned by the current function

 --> src/main.rs:6:32

  |

6 |     let handle = thread::spawn(|| {

  |                                ^^ may outlive borrowed value `v`

7 |         println!("Here's a vector: {v:?}");

  |                                     - `v` is borrowed here

  |

The error continues on the next slide with a note about 'static and a suggested fix.

The Rust Programming Language · Ch.16 Fearless Concurrency

17

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Error E0373 (continued): 'static and the move hint

note: function requires argument type to outlive `'static`

 --> src/main.rs:6:18

  |

6 |       let handle = thread::spawn(|| {

  |  __________________^

7 | |         println!("Here's a vector: {v:?}");

8 | |     });

  | |______^

help: to force the closure to take ownership of `v` (and any other referenced variables),

    use the `move` keyword

  |

6 |     let handle = thread::spawn(move || {

  |                                ++++

 

For more information about this error, try `rustc --explain E0373`.

error: could not compile `threads` (bin "threads") due to 1 previous error

The Rust Programming Language · Ch.16 Fearless Concurrency

18

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Why borrowing v is rejected

Rust infers a borrow of v (println! only needs a reference), but it can’t tell how long the spawned thread will run, so it doesn’t know if the reference will always be valid. Listing 16.4 makes that more likely:

Listing 16.4: A thread with a closure that attempts to capture a reference to v from a main thread that drops v — src/main.rs

use std::thread;

 

fn main() {

    let v = vec![1, 2, 3];

 

    let handle = thread::spawn(|| {

        println!("Here's a vector: {v:?}");

    });

 

    drop(v); // oh no!

 

    handle.join().unwrap();

}

The Rust Programming Language · Ch.16 Fearless Concurrency

19

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

The danger, and the compiler’s advice

If Rust allowed Listing 16.4 to run, the spawned thread might be immediately put in the background without running at all. The spawned thread has a reference to v inside, but the main thread immediately drops v, using the drop function from Chapter 15. Then, when the spawned thread starts to execute, v is no longer valid, so a reference to it is also invalid. Oh no!

To fix the compiler error in Listing 16.3, use the error message’s advice:

help: to force the closure to take ownership of `v` (and any other referenced variables),

    use the `move` keyword

  |

6 |     let handle = thread::spawn(move || {

  |                                ++++

Adding the move keyword before the closure forces the closure to take ownership of the values it’s using, rather than allowing Rust to infer that it should borrow them. Listing 16.5 (next slide) will compile and run as intended.

The Rust Programming Language · Ch.16 Fearless Concurrency

20

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

The fix: a move closure

Listing 16.5: Using the move keyword to force a closure to take ownership of the values it uses — src/main.rs

use std::thread;

 

fn main() {

    let v = vec![1, 2, 3];

 

    let handle = thread::spawn(move || {

        println!("Here's a vector: {v:?}");

    });

 

    handle.join().unwrap();

}

Would a move closure also fix Listing 16.4, where the main thread calls drop? No: what Listing 16.4 tries to do is disallowed for a different reason. With move, v moves into the closure’s environment, and the main thread can no longer call drop on it. We would get the compiler error on the next slides instead.

The Rust Programming Language · Ch.16 Fearless Concurrency

21

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

Error E0382: use of moved value v

Listing 16.4 with move added to the closure:

$ cargo run

   Compiling threads v0.1.0 (file:///projects/threads)

error[E0382]: use of moved value: `v`

  --> src/main.rs:10:10

   |

 4 |     let v = vec![1, 2, 3];

   |         - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait

 5 |

 6 |     let handle = thread::spawn(move || {

   |                                ------- value moved into closure here

 7 |         println!("Here's a vector: {v:?}");

   |                                     - variable moved due to use in closure

...

10 |     drop(v); // oh no!

   |          ^ value used here after move

   |

The Rust Programming Language · Ch.16 Fearless Concurrency

22

Chapter 16 · 16.1 Using Threads to Run Code Simultaneously

move overrides borrowing, not the ownership rules

help: consider cloning the value before moving it into the closure

   |

 6 ~     let value = v.clone();

 7 ~     let handle = thread::spawn(move || {

 8 ~         println!("Here's a vector: {value:?}");

   |

 

For more information about this error, try `rustc --explain E0382`.

error: could not compile `threads` (bin "threads") due to 1 previous error

Rust’s ownership rules have saved us again! Listing 16.3 failed because Rust was conservative and only borrowed v for the thread, so the main thread could theoretically invalidate the spawned thread’s reference. Telling Rust to move ownership of v to the spawned thread guarantees that the main thread won’t use v anymore. Changing Listing 16.4 the same way violates the ownership rules when the main thread uses v.

Remark. The move keyword overrides Rust’s conservative default of borrowing; it doesn’t let us violate the ownership rules. Next: situations in which to use threads.

The Rust Programming Language · Ch.16 Fearless Concurrency

23

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Message passing and channels

One increasingly popular approach to ensuring safe concurrency is message passing, where threads or actors communicate by sending each other messages containing data. The idea, in a slogan from the Go language documentation: “Do not communicate by sharing memory; instead, share memory by communicating.” For message-sending concurrency, Rust’s standard library provides an implementation of channels.

Definition 16.9 (Channel). A general programming concept by which data is sent from one thread to another. A channel has two halves: a transmitter and a receiver. A channel is said to be closed if either the transmitter or receiver half is dropped.

Example 16.10. Imagine a channel as a directional channel of water, such as a stream or a river. Put a rubber duck into the river and it travels downstream to the end of the waterway. The transmitter is the upstream location where you put the duck in; the receiver is where the duck ends up downstream. One part of your code calls methods on the transmitter with the data you want to send; another part checks the receiving end for arriving messages.

The Rust Programming Language · Ch.16 Fearless Concurrency

24

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Creating a channel

The goal is a program with one thread that generates values and sends them down a channel, and another thread that receives the values and prints them out. Simple values illustrate the feature; once you’re familiar with the technique, you could use channels for any threads that need to communicate with each other, such as a chat system or a system where many threads perform parts of a calculation and send the parts to one thread that aggregates the results.

Listing 16.6 creates a channel but does nothing with it. It won’t compile yet, because Rust can’t tell what type of values we want to send over the channel.

Listing 16.6: Creating a channel and assigning the two halves to tx and rx — src/main.rs

use std::sync::mpsc;

 

fn main() {

    let (tx, rx) = mpsc::channel();

}

The Rust Programming Language · Ch.16 Fearless Concurrency

25

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

mpsc: multiple producer, single consumer

A new channel is created with the mpsc::channel function.

Definition 16.11 (mpsc). Short for multiple producer, single consumer: the way Rust’s standard library implements channels, a channel can have multiple sending ends that produce values but only one receiving end that consumes those values.

Imagine multiple streams flowing together into one big river: everything sent down any of the streams will end up in one river at the end. We start with a single producer, and add multiple producers once this example works.

  • mpsc::channel returns a tuple: the first element is the sending end — the transmitter — and the second is the receiving end — the receiver.
  • The abbreviations tx and rx are traditionally used in many fields for transmitter and receiver, so the variables are named that way.
  • The let statement uses a pattern that destructures the tuple (patterns: Chapter 19) — a convenient way to extract the pieces of the tuple returned by mpsc::channel.

The Rust Programming Language · Ch.16 Fearless Concurrency

26

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Sending a value from a spawned thread

Move the transmitter into a spawned thread and send one string to the main thread — like putting a rubber duck in the river upstream, or sending a chat message to another thread.

Listing 16.7: Moving tx to a spawned thread and sending "hi" — src/main.rs

use std::sync::mpsc;

use std::thread;

 

fn main() {

    let (tx, rx) = mpsc::channel();

 

    thread::spawn(move || {

        let val = String::from("hi");

        tx.send(val).unwrap();

    });

}

move moves tx into the closure: the spawned thread must own the transmitter to send through the channel.

The Rust Programming Language · Ch.16 Fearless Concurrency

27

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Receiving the value in the main thread

Listing 16.8 gets the value from the receiver in the main thread — like retrieving the rubber duck from the water at the end of the river, or receiving a chat message.

Listing 16.8: Receiving the value "hi" in the main thread and printing it — src/main.rs

use std::sync::mpsc;

use std::thread;

 

fn main() {

    let (tx, rx) = mpsc::channel();

 

    thread::spawn(move || {

        let val = String::from("hi");

        tx.send(val).unwrap();

    });

 

    let received = rx.recv().unwrap();

    println!("Got: {received}");

}

The Rust Programming Language · Ch.16 Fearless Concurrency

28

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

send, recv and try_recv

The transmitter has a send method that takes the value to send. It returns a Result<T, E>: if the receiver has already been dropped and there’s nowhere to send a value, the send operation returns an error. Here we call unwrap to panic in case of an error; a real application would handle it properly (Chapter 9).

The receiver has two useful methods:

recv

Short for receive. Blocks the main thread’s execution and waits until a value is sent down the channel; once one is sent, recv returns it in a Result<T, E>. When the transmitter closes, recv returns an error to signal that no more values will be coming.

try_recv

Doesn’t block: returns a Result<T, E> immediately — Ok holding a message if one is available, Err if there aren’t any this time. Useful if the thread has other work to do while waiting: a loop can call try_recv every so often, handle a message if one is available, and otherwise do other work until checking again.

The Rust Programming Language · Ch.16 Fearless Concurrency

29

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Running Listing 16.8; ownership through channels

Listing 16.8 uses recv for simplicity: the main thread has no other work to do than wait for messages, so blocking it is appropriate. Running it prints the value from the main thread:

Got: hi

Perfect!

Transferring ownership through channels

The ownership rules play a vital role in message sending because they help you write safe, concurrent code. Preventing errors in concurrent programming is the advantage of thinking about ownership throughout your Rust programs.

An experiment shows how channels and ownership work together to prevent problems: Listing 16.9 tries to use a val value in the spawned thread after sending it down the channel. Try compiling it to see why this code isn’t allowed.

The Rust Programming Language · Ch.16 Fearless Concurrency

30

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Using a value after sending it

Listing 16.9: Attempting to use val after we’ve sent it down the channel — src/main.rs

use std::sync::mpsc;

use std::thread;

 

fn main() {

    let (tx, rx) = mpsc::channel();

 

    thread::spawn(move || {

        let val = String::from("hi");

        tx.send(val).unwrap();

        println!("val is {val}");

    });

 

    let received = rx.recv().unwrap();

    println!("Got: {received}");

}

The Rust Programming Language · Ch.16 Fearless Concurrency

31

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Error E0382: borrow of moved value val

Listing 16.9 prints val after tx.send. A bad idea: once sent, the other thread could modify or drop the value before we use it again, causing errors or unexpected results from inconsistent or nonexistent data. Rust refuses to compile it:

$ cargo run

   Compiling message-passing v0.1.0 (file:///projects/message-passing)

error[E0382]: borrow of moved value: `val`

  --> src/main.rs:10:27

   |

 8 |         let val = String::from("hi");

   |             --- move occurs because `val` has type `String`, which does not implement the `Copy` trait

 9 |         tx.send(val).unwrap();

   |                 --- value moved here

10 |         println!("val is {val}");

   |                           ^^^ value borrowed here after move

 

For more information about this error, try `rustc --explain E0382`.

error: could not compile `message-passing` (bin "message-passing") due to 1 previous error

The Rust Programming Language · Ch.16 Fearless Concurrency

32

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

send takes ownership; sending multiple values

Our concurrency mistake has caused a compile-time error. The send function takes ownership of its parameter, and when the value is moved the receiver takes ownership of it. This stops us from accidentally using the value again after sending it; the ownership system checks that everything is okay.

Sending multiple values

Listing 16.8 compiled and ran, but it didn’t clearly show that two separate threads were talking to each other over the channel.

Listing 16.10 (next two slides) makes modifications that prove the code is running concurrently: the spawned thread now sends multiple messages and pauses for a second between each message.

The Rust Programming Language · Ch.16 Fearless Concurrency

33

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

A spawned thread with a vector of messages

Listing 16.10: Sending multiple messages and pausing between each one — src/main.rs

use std::sync::mpsc;

use std::thread;

use std::time::Duration;

 

fn main() {

    let (tx, rx) = mpsc::channel();

 

    thread::spawn(move || {

        let vals = vec![

            String::from("hi"),

            String::from("from"),

            String::from("the"),

            String::from("thread"),

        ];

The Rust Programming Language · Ch.16 Fearless Concurrency

34

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Treating the receiver as an iterator

Listing 16.10 (continued) — src/main.rs

 

        for val in vals {

            tx.send(val).unwrap();

            thread::sleep(Duration::from_secs(1));

        }

    });

 

    for received in rx {

        println!("Got: {received}");

    }

}

  • The spawned thread has a vector of strings to send to the main thread. It iterates over them, sending each individually, and pauses between each by calling thread::sleep with a Duration value of one second.
  • The main thread no longer calls recv explicitly: it treats rx as an iterator, printing each value received. When the channel is closed, iteration ends.

The Rust Programming Language · Ch.16 Fearless Concurrency

35

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Running Listing 16.10; multiple producers

Output, with a one-second pause between each line:

Got: hi

Got: from

Got: the

Got: thread

There is no code that pauses or delays in the main thread’s for loop, so we can tell that the main thread is waiting to receive values from the spawned thread.

Creating multiple producers

mpsc is an acronym for multiple producer, single consumer. To put it to use, expand Listing 16.10 to create multiple threads that all send values to the same receiver. This is done by cloning the transmitter, as shown in Listing 16.11 (next three slides).

The Rust Programming Language · Ch.16 Fearless Concurrency

36

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Cloning the transmitter

Listing 16.11: Sending multiple messages from multiple producers — src/main.rs

use std::sync::mpsc;

use std::thread;

use std::time::Duration;

 

fn main() {

    // --snip--

 

    let (tx, rx) = mpsc::channel();

 

    let tx1 = tx.clone();

    thread::spawn(move || {

        let vals = vec![

            String::from("hi"),

            String::from("from"),

            String::from("the"),

The Rust Programming Language · Ch.16 Fearless Concurrency

37

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

First producer sends through tx1; second spawned thread

Listing 16.11 (continued) — src/main.rs

            String::from("thread"),

        ];

 

        for val in vals {

            tx1.send(val).unwrap();

            thread::sleep(Duration::from_secs(1));

        }

    });

 

    thread::spawn(move || {

        let vals = vec![

            String::from("more"),

            String::from("messages"),

            String::from("for"),

            String::from("you"),

The Rust Programming Language · Ch.16 Fearless Concurrency

38

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Second producer sends through the original tx

Listing 16.11 (continued) — src/main.rs

        ];

 

        for val in vals {

            tx.send(val).unwrap();

            thread::sleep(Duration::from_secs(1));

        }

    });

 

    for received in rx {

        println!("Got: {received}");

    }

 

    // --snip--

}

The Rust Programming Language · Ch.16 Fearless Concurrency

39

Chapter 16 · 16.2 Transfer Data Between Threads with Message Passing

Two producers, one receiver

Before creating the first spawned thread, Listing 16.11 calls clone on the transmitter. This gives a new transmitter, passed to the first spawned thread; the original transmitter goes to a second spawned thread. That gives two threads, each sending different messages to the one receiver.

Output should look something like this:

Got: hi

Got: more

Got: from

Got: messages

Got: for

Got: the

Got: thread

Got: you

You might see the values in another order, depending on your system. This is what makes concurrency interesting as well as difficult.

Experiment with thread::sleep, giving it various values in the different threads: each run will be more nondeterministic and create different output each time.

The Rust Programming Language · Ch.16 Fearless Concurrency

40

Chapter 16 · 16.3 Shared-State Concurrency

Communicating by sharing memory

Having seen how channels work, we turn to a different method of concurrency. Message passing is a fine way to handle concurrency, but it’s not the only way. Another method is for multiple threads to access the same shared data. Consider this part of the Go slogan again: “Do not communicate by sharing memory.”

What would communicating by sharing memory look like? And why would message-passing enthusiasts caution not to use memory sharing?

Channels ≈ single ownership

In any programming language, once you transfer a value down a channel, you should no longer use that value.

Shared memory ≈ multiple ownership

Multiple threads can access the same memory location at the same time.

As Chapter 15 showed with smart pointers, multiple ownership can add complexity because these different owners need managing. Rust’s type system and ownership rules greatly assist in getting this management correct. An example: mutexes, one of the more common concurrency primitives for shared memory.

The Rust Programming Language · Ch.16 Fearless Concurrency

41

Chapter 16 · 16.3 Shared-State Concurrency

Controlling access with mutexes

Definition 16.12 (Mutex). Abbreviation for mutual exclusion: a mutex allows only one thread to access some data at any given time. To access the data, a thread must first signal that it wants access by asking to acquire the mutex’s lock.

Definition 16.13 (Lock). A data structure that is part of the mutex and keeps track of who currently has exclusive access to the data. The mutex is therefore described as guarding the data it holds via the locking system.

Mutexes have a reputation for being difficult to use because you have to remember two rules:

  1. You must attempt to acquire the lock before using the data.
  2. When you’re done with the data that the mutex guards, you must unlock the data so that other threads can acquire the lock.

The Rust Programming Language · Ch.16 Fearless Concurrency

42

Chapter 16 · 16.3 Shared-State Concurrency

A mutex is a panel with one microphone

Example 16.14. Imagine a panel discussion at a conference with only one microphone.

  • Before a panelist can speak, they have to ask or signal that they want to use the microphone.
  • When they get the microphone, they can talk for as long as they want to and then hand the microphone to the next panelist who requests to speak.
  • If a panelist forgets to hand the microphone off when they’re finished with it, no one else is able to speak.
  • If management of the shared microphone goes wrong, the panel won’t work as planned!

Management of mutexes can be incredibly tricky to get right, which is why so many people are enthusiastic about channels. However, thanks to Rust’s type system and ownership rules, you can’t get locking and unlocking wrong.

The Rust Programming Language · Ch.16 Fearless Concurrency

43

Chapter 16 · 16.3 Shared-State Concurrency

The API of Mutex<T>

To see how to use a mutex, start with a mutex in a single-threaded context:

Listing 16.12: Exploring the API of Mutex<T> in a single-threaded context for simplicity — src/main.rs

use std::sync::Mutex;

 

fn main() {

    let m = Mutex::new(5);

 

    {

        let mut num = m.lock().unwrap();

        *num = 6;

    }

 

    println!("m = {m:?}");

}

The Rust Programming Language · Ch.16 Fearless Concurrency

44

Chapter 16 · 16.3 Shared-State Concurrency

new, lock, and why you can’t forget to lock

  • As with many types, a Mutex<T> is created with the associated function new.
  • To access the data inside the mutex, use the lock method to acquire the lock. This call blocks the current thread so that it can’t do any work until it’s our turn to have the lock.
  • The call to lock would fail if another thread holding the lock panicked. In that case, no one would ever be able to get the lock, so the code chooses to unwrap and have this thread panic if it is in that situation.
  • After acquiring the lock, the return value, named num here, can be treated as a mutable reference to the data inside.

Remark. The type system ensures that we acquire a lock before using the value in m. The type of m is Mutex<i32>, not i32, so we must call lock to be able to use the i32 value. We can’t forget; the type system won’t let us access the inner i32 otherwise.

The Rust Programming Language · Ch.16 Fearless Concurrency

45

Chapter 16 · 16.3 Shared-State Concurrency

MutexGuard releases the lock automatically

The call to lock returns a type called MutexGuard, wrapped in a LockResult that Listing 16.12 handles with the call to unwrap.

Definition 16.15 (MutexGuard). The smart pointer returned by lock. It implements Deref to point at the inner data, and has a Drop implementation that releases the lock automatically when the MutexGuard goes out of scope.

  • In Listing 16.12 the guard goes out of scope at the end of the inner scope, so the lock is released there.
  • As a result, we don’t risk forgetting to release the lock and blocking the mutex from being used by other threads, because the lock release happens automatically.
  • After dropping the lock, we can print the mutex value and see that we were able to change the inner i32 to 6.

The Rust Programming Language · Ch.16 Fearless Concurrency

46

Chapter 16 · 16.3 Shared-State Concurrency

Shared access to Mutex<T>: ten threads, one counter

Now share a value between threads: 10 threads each increment a counter by 1, from 0 to 10. Listing 16.13 has a compiler error that teaches more about using Mutex<T> and how Rust helps us use it correctly.

Listing 16.13: Ten threads, each incrementing a counter guarded by a Mutex<T> — src/main.rs

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;

The Rust Programming Language · Ch.16 Fearless Concurrency

47

Chapter 16 · 16.3 Shared-State Concurrency

Joining the threads and printing the result

Listing 16.13 (continued) — src/main.rs

        });

        handles.push(handle);

    }

 

    for handle in handles {

        handle.join().unwrap();

    }

 

    println!("Result: {}", *counter.lock().unwrap());

}

  • counter holds an i32 inside a Mutex<T>, as in Listing 16.12. Iterating over a range creates 10 threads, all given the same closure: it moves the counter into the thread, acquires a lock on the Mutex<T> by calling lock, and adds 1 to the value in the mutex.
  • When a thread finishes running its closure, num goes out of scope and releases the lock so that another thread can acquire it.

The Rust Programming Language · Ch.16 Fearless Concurrency

48

Chapter 16 · 16.3 Shared-State Concurrency

Error E0382: counter moved in a previous iteration

In the main thread, we collect all the join handles and, as in Listing 16.2, call join on each to make sure all the threads finish. Then the main thread acquires the lock and prints the result. We hinted that this example wouldn’t compile. Now let’s find out why:

$ cargo run

   Compiling shared-state v0.1.0 (file:///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

The error continues on the next slide.

The Rust Programming Language · Ch.16 Fearless Concurrency

49

Chapter 16 · 16.3 Shared-State Concurrency

A lock can’t be moved into multiple threads

 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 message states that the counter value was moved in the previous iteration of the loop. Rust is telling us that we can’t move the ownership of lock counter into multiple threads.

The fix to try next: the multiple-ownership method from Chapter 15.

The Rust Programming Language · Ch.16 Fearless Concurrency

50

Chapter 16 · 16.3 Shared-State Concurrency

Multiple ownership with multiple threads: try Rc<T>

In Chapter 15, the smart pointer Rc<T> gave a value multiple owners by creating a reference-counted value. Do the same here: wrap the Mutex<T> in Rc<T> and clone the Rc<T> before moving ownership to the thread.

Listing 16.14: Attempting to use Rc<T> to allow multiple threads to own the Mutex<T> — src/main.rs

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 || {

The Rust Programming Language · Ch.16 Fearless Concurrency

51

Chapter 16 · 16.3 Shared-State Concurrency

Each thread gets its own Rc clone

Listing 16.14 (continued) — src/main.rs

            let mut num = counter.lock().unwrap();

 

            *num += 1;

        });

        handles.push(handle);

    }

 

    for handle in handles {

        handle.join().unwrap();

    }

 

    println!("Result: {}", *counter.lock().unwrap());

}

Once again, we compile and get… different errors! The compiler is teaching us a lot (next two slides).

The Rust Programming Language · Ch.16 Fearless Concurrency

52

Chapter 16 · 16.3 Shared-State Concurrency

Error E0277: Rc<Mutex<i32>> cannot be sent between threads

$ cargo run

   Compiling shared-state v0.1.0 (file:///projects/shared-state)

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

The Rust Programming Language · Ch.16 Fearless Concurrency

53

Chapter 16 · 16.3 Shared-State Concurrency

The reason: Send is not implemented for Rc

   |

   = 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`

  --> /rustc/2d8144b7880597b6e6d3dfd63a9a9efae3f533d3/library/std/src/thread/functions.rs:125:0

 

For more information about this error, try `rustc --explain E0277`.

error: could not compile `shared-state` (bin "shared-state") due to 1 previous error

Wow, that error message is very wordy! The important part: `Rc<Mutex<i32>>` cannot be sent between threads safely. The compiler also tells us why: the trait `Send` is not implemented for `Rc<Mutex<i32>>`. Send (next section) is one of the traits that ensures the types used with threads are meant for use in concurrent situations.

The Rust Programming Language · Ch.16 Fearless Concurrency

54

Chapter 16 · 16.3 Shared-State Concurrency

Atomic reference counting with Arc<T>

Unfortunately, Rc<T> is not safe to share across threads. It adds to the reference count for each call to clone and subtracts when each clone is dropped, but it doesn’t use any concurrency primitives to make sure that changes to the count can’t be interrupted by another thread. This could lead to wrong counts — subtle bugs that could in turn lead to memory leaks or a value being dropped before we’re done with it. We need a type exactly like Rc<T>, but that changes the reference count in a thread-safe way.

Definition 16.16 (Arc<T>). A type like Rc<T> that is safe to use in concurrent situations. The a stands for atomic: it’s an atomically reference-counted type.

Atomics are an additional kind of concurrency primitive not covered in detail here (see the standard library documentation for std::sync::atomic). For now, just know that atomics work like primitive types but are safe to share across threads.

The Rust Programming Language · Ch.16 Fearless Concurrency

55

Chapter 16 · 16.3 Shared-State Concurrency

Why not make everything atomic? Switching to Arc<T>

Why aren’t all primitive types atomic, and standard library types implemented with Arc<T> by default? Thread safety comes with a performance penalty that you only want to pay when you really need to. Operating on values within a single thread, code runs faster without the guarantees atomics provide.

Arc<T> and Rc<T> have the same API, so the fix changes the use line, the call to new, and the call to clone. Listing 16.15 finally compiles and runs.

Listing 16.15: Using an Arc<T> to wrap the Mutex<T> to be able to share ownership across multiple threads — src/main.rs

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 {

The Rust Programming Language · Ch.16 Fearless Concurrency

56

Chapter 16 · 16.3 Shared-State Concurrency

Each thread gets its own Arc clone

Listing 16.15 (continued) — src/main.rs

        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());

}

The Rust Programming Language · Ch.16 Fearless Concurrency

57

Chapter 16 · 16.3 Shared-State Concurrency

Result: 10

Listing 16.15 prints the following:

Result: 10

We did it! Counting from 0 to 10 may not seem very impressive, but it taught us a lot about Mutex<T> and thread safety. This program’s structure can also do more complicated operations than incrementing a counter: divide a calculation into independent parts, split those parts across threads, and then use a Mutex<T> to have each thread update the final result with its part.

Remark. For simple numerical operations, there are types simpler than Mutex<T> in the std::sync::atomic module of the standard library. They provide safe, concurrent, atomic access to primitive types. This example used Mutex<T> with a primitive type to concentrate on how Mutex<T> works.

The Rust Programming Language · Ch.16 Fearless Concurrency

58

Chapter 16 · 16.3 Shared-State Concurrency

Comparing RefCell<T>/Rc<T> and Mutex<T>/Arc<T>

You might have noticed that counter is immutable, yet we could get a mutable reference to the value inside it: Mutex<T> provides interior mutability, as the Cell family does. In the same way RefCell<T> (Chapter 15) allowed mutating contents inside an Rc<T>, Mutex<T> is used to mutate contents inside an Arc<T>.

Table 16.1: Single-threaded types and their thread-safe counterparts

RoleSingle-threaded (Ch.15)Across threads (Ch.16)
Multiple ownershipRc<T>Arc<T> (atomic reference count)
Interior mutabilityRefCell<T>Mutex<T> (lock guards the data)
Logic-error riskreference cycles → memory leaksdeadlocks

The Rust Programming Language · Ch.16 Fearless Concurrency

59

Chapter 16 · 16.3 Shared-State Concurrency

Mutex<T> and the risk of deadlocks

Rust can’t protect you from all kinds of logic errors when you use Mutex<T>. Recall from Chapter 15 that using Rc<T> came with the risk of creating reference cycles, where two Rc<T> values refer to each other, causing memory leaks. Similarly, Mutex<T> comes with the risk of creating deadlocks.

Definition 16.17 (Deadlock). A situation that occurs when an operation needs to lock two resources and two threads have each acquired one of the locks, causing them to wait for each other forever.

If you’re interested in deadlocks, try creating a Rust program that has a deadlock; then research deadlock mitigation strategies for mutexes in any language and have a go at implementing them in Rust. The standard library API documentation for Mutex<T> and MutexGuard offers useful information.

The chapter rounds out with the Send and Sync traits and how to use them with custom types.

The Rust Programming Language · Ch.16 Fearless Concurrency

60

Chapter 16 · 16.4 Extensible Concurrency with Send and Sync

Concurrency in the language: the marker traits

Interestingly, almost every concurrency feature in this chapter so far has been part of the standard library, not the language. Your options for handling concurrency are not limited to the language or the standard library; you can write your own concurrency features or use those written by others.

However, among the key concurrency concepts embedded in the language rather than the standard library are the std::marker traits Send and Sync.

Transferring ownership between threads

Definition 16.18 (Send). The marker trait indicating that ownership of values of the type implementing Send can be transferred between threads.

Almost every Rust type implements Send, with some exceptions, including Rc<T>: if you cloned an Rc<T> value and tried to transfer ownership of the clone to another thread, both threads might update the reference count at the same time. So Rc<T> is implemented for single-threaded situations where you don’t want to pay the thread-safe performance penalty.

The Rust Programming Language · Ch.16 Fearless Concurrency

61

Chapter 16 · 16.4 Extensible Concurrency with Send and Sync

Send is checked by the type system

Rust’s type system and trait bounds ensure that you can never accidentally send an Rc<T> value across threads unsafely.

  • In Listing 16.14, trying to do so produced the error the trait `Send` is not implemented for `Rc<Mutex<i32>>`.
  • Switching to Arc<T>, which does implement Send, made the code compile (Listing 16.15).

Remark. Any type composed entirely of Send types is automatically marked as Send as well. Almost all primitive types are Send, aside from raw pointers, which Chapter 20 discusses.

The Rust Programming Language · Ch.16 Fearless Concurrency

62

Chapter 16 · 16.4 Extensible Concurrency with Send and Sync

Accessing from multiple threads: Sync

Definition 16.19 (Sync). The marker trait indicating that it is safe for the type implementing Sync to be referenced from multiple threads. In other words, any type T implements Sync if &T (an immutable reference to T) implements Send, meaning the reference can be sent safely to another thread.

Similar to Send, primitive types all implement Sync, and types composed entirely of types that implement Sync also implement Sync.

Table 16.2: Sync and the smart pointers discussed in the book

TypeSync?Why
Rc<T>NoSame reasons it doesn’t implement Send
RefCell<T>, Cell<T> familyNoRefCell<T>’s runtime borrow checking is not thread-safe
Mutex<T>YesCan be used to share access with multiple threads (16.3)

The Rust Programming Language · Ch.16 Fearless Concurrency

63

Chapter 16 · 16.4 Extensible Concurrency with Send and Sync

Implementing Send and Sync manually is unsafe

  • Types composed entirely of other types that implement Send and Sync automatically implement Send and Sync too, so we don’t have to implement those traits manually.
  • As marker traits, they don’t even have any methods to implement. They’re just useful for enforcing invariants related to concurrency.
  • Manually implementing these traits involves implementing unsafe Rust code (Chapter 20).

Remark. Building new concurrent types not made up of Send and Sync parts requires careful thought to uphold the safety guarantees. “The Rustonomicon” has more information about these guarantees and how to uphold them.

The Rust Programming Language · Ch.16 Fearless Concurrency

64

Chapter 16 · Summary

Chapter summary

  • The Rust standard library provides channels for message passing and smart pointer types, such as Mutex<T> and Arc<T>, that are safe to use in concurrent contexts.
  • The type system and the borrow checker ensure that code using these solutions won’t end up with data races or invalid references. Once your code compiles, you can rest assured that it will happily run on multiple threads without the kinds of hard-to-track-down bugs common in other languages.
  • Because very little of how Rust handles concurrency is part of the language, many concurrency solutions are implemented as crates. These evolve more quickly than the standard library, so search online for the current, state-of-the-art crates to use in multithreaded situations.

Concurrent programming is no longer a concept to be afraid of: go forth and make your programs concurrent, fearlessly!

Remark. This isn’t the last you’ll see of concurrency: Chapter 17 focuses on async programming, and the project in Chapter 21 uses this chapter’s concepts in a more realistic situation than the smaller examples here.

The Rust Programming Language · Ch.16 Fearless Concurrency

65