12 More on closures, generics, and threads

This chapter covers

This chapter is a bit like the last one: lots of new ideas that are tricky to understand at first. The first section on closures as arguments in functions is probably the hardest but continues what we learned in the last chapter about the three types of closures. Fortunately, the rest of the chapter is made up of similar things to what you’ve learned before. impl Trait is like regular generics but easier to write, Arc is like Rc, and scoped threads are like threads but easier to use; you’ll understand the channel examples fairly easily too because you already know how multiple threads work.

12.1 Closures as arguments

Closures are great. We know how to make our own, but what about closures as arguments in functions? Arguments need to have a type, but what exactly is a closure’s type?

So far, we’ve seen that there are three types of closures, and we know that they can capture variables that are in the same scope. We also know that when a closure accesses a variable in Rust, it can take by value, by reference, or by mutable reference. As we saw in the last chapter, a closure is able to choose on its own between Fn, FnMut, and FnOnce when capturing variables. However, as a function argument or return value, you have to choose one of these three. This is pretty similar to regular type inference. When you write let my_num = 9 or let my_num = 9.0, the compiler can determine the type, but in a function signature, you have to choose the exact type it will be: an i32, a u8, and so on.

A good way to get a feel for closures is by looking at a few function signatures. Here is the one for the .all() method. We saw before that .all() checks an iterator to see whether everything is true (depending on how you decide to return true or false). Part of its signature says this:

fn all<F>(&mut self, f: F) -> bool
where
    F: FnMut(Self::Item) -> bool,

Let’s look at this signature bit by bit:

You see this sort of signature a lot in the iterator methods that take closures. For example, this is the signature for .map():

fn map<B, F>(self, f: F) -> Map<Self, F>
    where
        Self: Sized,
        F: FnMut(Self::Item) -> B,
    {
        Map::new(self, f)
    }

The signature fn map<B, F>(self, f: F) means that the function takes two generic types. F is a function that takes one item from the container implementing .map(), and B is the return type of that function (the item that you pass on). After the where, we see the trait bounds. One is Sized, and the next is the closure signature. It must be an FnMut and do the closure on Self::Item, which is the next item from the iterator. It returns B, which is whatever you choose to pass on. If you look at the iterator methods that we learned in chapter 9, you will see FnMut everywhere.

Now, let’s relax a bit with maybe the simplest possible function that takes a closure:

fn do_something<F>(f: F)
where
    F: FnOnce(),
{
    f();
}

The function signature here simply says that it takes a closure that takes by value (FnOnce) and doesn’t return anything. Now, we can write this closure that takes nothing and put whatever we like inside it. We will create a Vec and then iterate over it just to show what we can do now:

fn do_something<F>(f: F)
where
    F: FnOnce(),
{
    f();
}
 
fn main() {
    let some_vec = vec![9, 8, 10];
    do_something(|| {
        some_vec
            .into_iter()
            .for_each(|x| println!("The number is: {x}"));
    });
}

Here is the output:

The number is: 9
The number is: 8
The number is: 10

Because the closure is an FnOnce() (and because .into_iter() inside takes by value), you can’t call it with some_vec again—it was taken by value and is now gone. Give it a try! The code won’t compile now because we are calling do_something twice:

fn do_something<F>(f: F)
where
    F: FnOnce(),
{
    f();
}
 
fn main() {
    let some_vec = vec![9, 8, 10];
    do_something(|| {
        some_vec
            .into_iter()
            .for_each(|x| println!("The number is: {x}"));
    });
    do_something(|| {
        some_vec
            .into_iter()
            .for_each(|x| println!("The number is: {x}"));
    });
}

The error message is exactly the same as when a value moves in other cases: the value is “moved into” somewhere else and can’t be used again:

9  |     let some_vec = vec![9, 8, 10];
   |         -------- move occurs because `some_vec` has type `Vec<i32>`, which does not implement the `Copy` trait
10 |     do_something(|| {
   |                  -- value moved into closure here
11 |         some_vec
   |         -------- variable moved due to use in closure
...
15 |     do_something(|| {
   |                  ^^ value used here after move
16 |         some_vec
   |         -------- use occurs due to use in closure

Now, let’s try a closure that takes by reference. To do so, we can say that the closure is an Fn(). This closure will call some_vec as many times as we like, and the variable will still be alive:

fn do_something<F>(f: F)
where
    F: Fn(),
{
    f();
}
 
fn main() {
    let some_vec = vec![9, 8, 10];
    do_something(|| {
        some_vec.iter().for_each(|x| println!("The number is: {x}"));
    });
 
    do_something(|| {
        some_vec.iter().for_each(|x| println!("The number is: {x}"));
    });
}

The output will be the same as the last sample, just printed two times.

But here is something that might seem odd: try changing the F: Fn() on line 3 to F: FnOnce(). The code compiles! This might be unexpected. Let’s find out why.

12.1.1 Some simple closures

To understand how the three closure traits work, let’s take a look at some super-simple functions that each takes a closure. First, we have a function that takes an Fn() closure and calls it two times:

fn takes_fn<F: Fn()>(f: F) {
    f();
    f();
}

As you can see, it’s not FnOnce, so it can be called twice (or three times or more).

Up next is a function that takes an FnMut() closure and calls it twice. This one can also be called more than once:

fn takes_fnmut<F: FnMut()>(mut f: F) {
    f();
    f();
}

Finally, we have a function that takes an FnOnce() closure. It won’t compile if you call it a second time:

fn takes_fnonce<F: FnOnce()>(f: F) {
    f();
    // f();     
}

This won’t work.

You’ll notice that functions that take closures can look extremely simple. This is because closures can capture variables from their environment, so often you don’t need to pass any arguments in. Each of our closures (Fn(), FnMut(), and FnOnce()) takes no arguments and returns nothing, but they can capture variables around them.

Let’s make some actual closures to pass into these functions. Inside main(), we will have a mutable String and three types of closures that capture it. Then we will call each of them:

fn takes_fnonce<F: FnOnce()>(f: F) {
    f();
}
fn takes_fnmut<F: FnMut()>(mut f: F) {
    f();
    f();
}
fn takes_fn<F: Fn()>(f: F) {
    f();
    f();
}
 
fn main() {
    let mut my_string = String::from("Hello there");
 
    let prints_string = || {                      
        println!("{my_string}");
    };
    takes_fn(prints_string);                      
 
    let adds_exclamation_and_prints = || {        
        my_string.push('!');
        println!("{my_string}");
    };
    takes_fnmut(adds_exclamation_and_prints);     
 
    let prints_then_drops = || {                  
        println!("Now dropping {my_string}");
        drop(my_string);
    };
    takes_fnonce(prints_then_drops);
    // takes_fnonce(prints_then_drops);           
}

This closure only needs to capture by reference, so it will be an Fn closure.

The takes_fn function takes the closure as an argument and calls it two times.

This next closure needs to capture by mutable reference, so it will be an FnMut closure.

The takes_fnmut function takes the closure as an argument and calls it two times.

Finally, we have a closure that captures by value, so it will be an FnOnce closure.

The takes_fnonce function takes prints_then_drops but can’t do it again.

Here is the output:

Hello there
Hello there
Hello there!
Hello there!!
Now dropping Hello there!!

You don’t have to name a closure, though, and it’s usually more common to just write the closure out inside the function that uses it as an argument. The following code is the exact same as the code we just saw, except we are just writing the closures instead of giving them names first:

fn takes_fnonce<F: FnOnce()>(f: F) {
    f();
}
fn takes_fnmut<F: FnMut()>(mut f: F) {
    f();
    f();
}
fn takes_fn<F: Fn()>(f: F) {
    f();
    f();
}
 
fn main() {
    let mut my_string = String::from("Hello there");
    takes_fn(|| {
        println!("{my_string}");
    });    
    takes_fnmut(|| {
        my_string.push('!');
        println!("{my_string}");
    });
    takes_fnonce(|| {
        println!("Now dropping {my_string}");
        drop(my_string);
    });
}

Hopefully, that makes sense: when we write a closure we are just writing another kind of function. The regular functions call those closures once or twice. When we call those regular functions, they call the closures once or twice, and our code inside the closures is executed.

Now that we have these basics down, let’s take a look at the signatures for each of the three closure types and see what they can tell us.

12.1.2 The relationship between FnOnce, FnMut, and Fn

There is an interesting relationship among the three closure traits, which we can see from their signature. Let’s take a look at the signature for Fn first. Here is the important part:

pub trait Fn: FnMut

Just as in any other trait, the trait after : is the trait that must be implemented first. This means that a closure needs to implement FnMut before it can implement Fn. So let’s take a look at FnMut:

pub trait FnMut: FnOnce

Interesting! A closure needs FnMut to implement Fn, but before it implements FnMut, it needs FnOnce. Finally, let’s check FnOnce:

pub trait FnOnce

So, FnOnce doesn’t need any other traits to be implemented first. To sum up:

That means that all closures implement FnOnce.

The trait after the : is known as a supertrait. FnOnce is a supertrait of FnMut, and FnMut is a supertrait of Fn. The word super is just the Latin word meaning “over,” so you can think of it as this:

The opposite of this is subtrait, which just means under. Supertrait and subtrait are easy to remember if you imagine implementing the first trait needed (the line above, so super) and going down to the next line to implement the next trait (the line below, so sub).

Why is this useful? Well, it means that if a function takes an FnOnce as an argument, it can also take an Fn instead (because Fn also implements FnOnce) or an FnMut (because FnMut also implements FnOnce). If a function takes an FnMut, it can also take an Fn (because Fn implements FnMut).

We can show this with our previous example. We will get rid of the function takes_fn() and keep takes_fnonce() and takes_fnmut(). It still works:

fn takes_fnonce<F: FnOnce()>(f: F) {
    f();
}
fn takes_fnmut<F: FnMut()>(mut f: F) {
    f();
    f();
}
 
fn main() {
    let mut my_string = String::from("Hello there");
    let prints_string = || {    
        println!("{my_string}");
    };
    takes_fnonce(prints_string);                  
    takes_fnmut(prints_string);                   
    let adds_exclamation_and_prints = || {    
        my_string.push('!');
        println!("{my_string}");
    };
    takes_fnonce(adds_exclamation_and_prints);    
    let prints_then_drops = || {    
        println!("Now dropping {my_string}");
        drop(my_string);
    };
    takes_fnonce(prints_then_drops);              
}

takes_fnonce takes an FnOnce, and Fn implements FnOnce. No problem.

takes_fnmut takes an FnMut, and Fn implements FnMut. Once again, no problem.

takes_fnonce takes an FnOnce, and FnMut implements FnOnce.

Finally, takes_fnonce takes an FnOnce, and FnOnce (of course) implements FnOnce.

This is why you sometimes see books say that Fn is the “most powerful” of the three closure traits because it can be passed in no matter which closure trait is written. At the same time, having a function that takes an Fn is the most restrictive because an Fn closure must implement all three traits. No FnMut or FnOnce can be an argument in a function that wants an Fn.

12.1.3 Closures are all unique

One interesting fact about closures is that one closure is never the same type as another closure, even if the signature is the same. The types are always different because Fn, FnMut, and FnOnce are traits, not concrete types.

Let’s look at an example to prove this. Here is a function that takes a closure of type Fn() -> i32. We’ll make a closure and give it to the function. The function does nothing, so there is no output from the example, but the compiler is happy with this:

fn takes_a_closure_and_does_nothing<F>(f: F)
where
    F: Fn() -> i32,
{}
 
fn main() {
    let my_closure = || 9;       
    takes_a_closure_and_does_nothing(my_closure);
}

Takes nothing, returns an i32

Now, let’s try having it take two closures with the exact same signature to pass them in:

fn takes_two_closures_and_does_nothing<F>(first: F, second: F)
where
    F: Fn() -> i32,
{
}
 
fn main() {
    let first_closure = || 9;
    let second_closure = || 9;
    takes_two_closures_and_does_nothing(first_closure, second_closure);
}

Interestingly, it doesn’t work! Fortunately, the compiler gives us a fantastic error that tells us exactly what the problem is:

error[E0308]: mismatched types
  --> src/main.rs:10:56
   |
8  |     let first_closure = || 9;
   |                         -- the expected closure
9  |     let second_closure = || 9;
   |                          -- the found closure
10 |     takes_two_closures_and_does_nothing(first_closure,
   second_closure);
   |     -----------------------------------                ^^^^^^^^^^^^^^
   expected closure, found a different closure
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected closure `[closure@src/main.rs:8:25: 8:27]`
              found closure `[closure@src/main.rs:9:26: 9:28]`
   = note: no two closures, even if identical, have the same type
   = help: consider boxing your closure and/or using it as a trait object

This makes sense because these closures are unique types that implement the trait Fn, not a concrete type Fn. From the compiler’s point of view, the two arguments look like this:

Since first_closure is some type that implements a trait, and second_closure is some other type that implements a trait, they are not the same type.

The last part of the error message is interesting: consider boxing your closure. We will learn what this message is talking about in the next chapter. (If you are curious about this right away, try doing a search for the term trait object.)

Now, if we just wanted this code to compile, we could inform the compiler that the closures are different types by calling them F and G instead of just F. The compiler will be happy with this:

fn takes_two_closures_and_does_nothing<F, G>(first: F, second: G)
where
    F: Fn() -> i32,
    G: Fn() -> i32,
{
}
 
fn main() {
    let first_closure = || 9;
    let second_closure = || 9;
    takes_two_closures_and_does_nothing(first_closure, second_closure);
}

Hopefully, this clears up some of the mysteries about closures. Let’s finish up with an example that is a little more interesting than the simple ones we have looked at so far.

12.1.4 A closure example

Now let’s put together a closure example that actually does something interesting. In this example, we will create a City struct again. This time, the City struct has more data about years and populations. It has a Vec<u32> for all the years and another, Vec<u32>, for all the populations.

City has a single method called .change_city_data(), which takes a closure. When we use .change_city_data(), it gives us the years and the populations and a closure, so we can do what we want with the data. The closure type is FnMut, so we can change the data without taking ownership. In the following example, we will just have some fun with the closure by making some random changes to the City data. It looks like this:

#[derive(Debug)]
struct City {
    name: String,
    years: Vec<u32>,
    populations: Vec<u32>,
}
 
impl City {
    fn change_city_data<F>(&mut self, mut f: F)              
    
    where
        F: FnMut(&mut Vec<u32>, &mut Vec<u32>),              
    {
        f(&mut self.years, &mut self.populations)            
    }
}
 
fn main() {
    let mut tallinn = City {
        name: "Tallinn".to_string(),
        years: vec![1372, 1834, 1897, 1925, 1959, 1989, 2000, 2010, 2020],
        populations: vec![3_250, 15_300, 58_800,
            119_800, 283_071, 478_974,
            400_378, 406_703, 437_619,
],
        ],
    };
 
    tallinn.change_city_data(|x, y| {                        
        x.push(2030);
        y.push(500_000);
    });
 
    tallinn.change_city_data(|years, populations| {          
        let new_vec = years
            .iter_mut()
            .zip(populations.iter_mut())                     
            .take(3)
            .collect::<Vec<(_, _)>>();                       
        println!("{new_vec:?}");
    });
 
    tallinn.change_city_data(|x, y| {                        
        let position_option = x.iter().position(|x| *x == 1834);
        if let Some(position) = position_option {
            println!(
                "Going to delete {} at position {:?} now.",
                x[position], position
            );
            x.remove(position);
            y.remove(position);
        }
    });
 
    println!(
        "Years left are {:?}\nPopulations left are {:?}",
        tallinn.years, tallinn.populations
    );
}

We bring in self, and f is a generic type F. You could write it "mut closure: GenericClosure" or any other names you choose, but f and F are most common in Rust.

The closure takes mutable vectors of u32, which are the year and population data. How do we make sure that the year and population data get passed in? Well . . .

. . . we do it by calling the closure in and passing in these parameters as the arguments. Now, a user of the function gets access to these parameters and can do whatever they want with the closure every time we use it as long as the signature matches.

We’ll choose x and y for the two variables and can use them to add some data for the year 2030.

Or we can choose the names’ years and populations. Let’s put the data for three years together and print it.

Zips the two together and only takes the first three

Tells Rust to decide the types inside the tuple

For our final random change to the data, let’s delete the data for 1834 if the .position() method finds it.

Running this code will print the result of all the times we called .change_city_ data():

[(1372, 3250), (1834, 15300), (1897, 58800)]
Going to delete 1834 at position 1 now.
Years left are [1372, 1897, 1925, 1959, 1989, 2000, 2010, 2020, 2030]
Populations left are [3250, 58800, 119800, 283071, 478974, 400378, 406703,
437619, 500000]

This might be a good example for you to pick up and experiment for a bit. One idea you could try would be changing the closure inside change_city_data() to take a mutable reference to all of self (the City struct) instead of the two parameters. How would you change the signature? And what changes would you then have to make to the rest of the code to have it compile again?

That’s enough learning about closures for a while! You’ll see closures enough anyway as you start to use Rust more and more. Now, let’s take a look at something else you’ll see: another way to use generics.

12.2 impl Trait

It turns out that Rust has other ways to use generics, and now it’s time to learn the next one: impl Trait. Don’t worry; there are good reasons for having multiple types of generics. We first learned that generics use a type T (or any other name), which then gets decided when the program compiles. Let’s do a quick review of the generics we already know so that we can understand how impl Trait generics are different.

12.2.1 Regular generics compared to impl Trait

Let’s look at a concrete function to start, a very simple one that compares two numbers:

fn print_maximum(one: i32, two: i32) {
    let higher = if one > two { one } else { two };
    println!("{higher} is higher");
}
 
fn main() {
    print_maximum(8, 10);
}

This prints 10 is higher.

But this only takes an i32, so now we will make it generic so we can take in more than just i32s. We need to compare, and we want to print with {}. To do that, our type T will need to have PartialOrd and Display. Remember, this means “only take types that already have PartialOrd and Display.” Here it is as a generic function:

use std::fmt::Display;
 
fn print_maximum<T: PartialOrd + Display>(one: T, two: T) {
    let higher = if one > two { one } else { two };
    println!("{higher} is higher.");
}
 
fn main() {
    print_maximum(8, 10);
}

Now let’s look at impl Trait, which is similar. Instead of a type T, we can bring in an impl Trait. The function will then accept a type that implements that trait. You’ll notice that it involves less typing but otherwise looks pretty similar:

use std::fmt::Display;
 
fn prints_it(input: impl Into<String> + Display) {            
    println!("You can print many things, including {input}");
}
 
fn main() {
    let name = "Tuon";
    let string_name = String::from("Tuon");
    prints_it(name);
    prints_it(string_name);
}

Takes anything that can turn into a String and also implements Display

There are a few differences and limitations when you use impl Trait compared to regular generics. One difference is that for impl Trait you can’t decide the type—the function decides it. Take a look at this example:

use std::fmt::Display;
 
fn prints_it_impl_trait(input: impl Display) {
    println!("You can print many things, including {input}");
}
 
fn prints_it_regular_generic<T: Display>(input: T) {
    println!("You can print many things, including {input}");
}
 
fn main() {
    prints_it_regular_generic::<u8>(100);     
    prints_it_impl_trait(100);                
    prints_it_impl_trait(100u8);              
    // prints_it_impl_trait::<u8>(100);       
}

You can specify u8 if you want.

Here you can’t—it’ll be an i32 because Rust chooses i32 by default.

Well, we could pass in a u8 in this way. But we’re not telling the function what concrete type to choose; we’re just giving it a concrete type that it will react to.

This last one won’t work for just this reason: you can’t decide the type when calling the function.

The difference between the two gets even clearer when we look at the impl Trait version of our first example, the gives_higher() function. Interestingly, this code won’t work:

use std::fmt::Display;
 
fn gives_higher(one: impl PartialOrd + Display, two: impl PartialOrd +
Display) {
    let higher = if one > two { one } else { two };
    println!("{higher} is higher.");
}
 
fn main() {
    gives_higher(8, 10);
}

The code doesn’t work because regular generics specify a type name, like T. Writing T: PartialOrd + Display means, “There is a single type named T, and it will implement PartialOrd and Display.” But writing impl PartialOrd + Display means, “This argument will be some type that implements PartialOrd and Display.” But there is nothing to say that they will be the same type, and PartialOrd is used to compare two variables of the same type! There is no type T to tell the compiler that we are talking about a single type.

The compiler error is pretty funny and one of the rare examples where the compiler is confusing to the reader:

4 |     let higher = if one > two { one } else { two };
  |                     ---   ^^^ expected type parameter `impl PartialOrd
  + Display`, found a different type parameter `impl PartialOrd +
  Display`
  |                     |
  |                     expected because this is `impl PartialOrd + Display`
  |
  = note: expected type parameter `impl PartialOrd + Display` (type
  parameter `impl PartialOrd + Display`)
             found type parameter `impl PartialOrd + Display` (type
  parameter `impl PartialOrd + Display`)

Hopefully, this compiler message will be improved in the future.

Another limitation is that impl Trait can only be a parameter or return type of a regular function (http://mng.bz/46mj). It cannot appear when implementing traits. It can’t be the type of a let binding. And it can’t appear inside a type alias.

So far, we’ve only talked about the disadvantages of impl Trait! But it has a big advantage: we can return impl Trait from a function, and that lets us return closures because their function signatures are traits. In other words, you can write functions that return functions! Let’s see how that works.

12.2.2 Returning closures with impl Trait

Since we can return impl Trait from a function, we can also use it to return a closure. To return a closure, use impl and then the closure signature. Once you return it, you can use it just like any other closure.

Here is a small example of a function that gives you a closure depending on the text you put in. If you put "double" or "triple" in, it multiplies it by 2 or 3; otherwise, it gives you the same number. Let’s also print a message while we’re at it:

fn returns_a_closure(input: &str) -> impl FnMut(i32) -> i32 {
    match input {
        "double" => |mut number| {
            number *= 2;
            println!("Doubling number. Now it is {number}");
            number
        },
        "triple" => |mut number| {
            number *= 3;
            println!("Tripling number. Now it is {number}");
            number
        },
        _ => |number| {
            println!("Sorry, it's the same: {number}.");
            number
        },
    }
}
 
fn main() {
    let my_number = 10;
 
    let mut doubles = returns_a_closure("double");   
    let mut triples = returns_a_closure("triple");   
    let mut does_nothing = returns_a_closure("HI");  
 
    let doubled = doubles(my_number);
    let tripled = triples(my_number);
    let same = does_nothing(my_number);
}

Makes three closures

The output is

Doubling number. Now it is 20
Tripling number. Now it is 30
Sorry, it's the same: 10.

So you can see that returns_a_closure() is just like any other function: it has a return type that you have to follow. Except that its return type is not a number or some other type but a closure of FnMut(i32) -> i32. And if that’s what the closure returns, the compiler will let your code compile.

Here is a bit longer example. Let’s imagine a game where your character is facing monsters that are stronger at night. We can make an enum called TimeOfDay to keep track of the day. Your character is named Simon and has a number called character_ fear, which is an f64. It goes up at night and down during the day. We will make a make_fear_closure() function that not only changes his fear but also does other things like write messages. It could look like this:

enum TimeOfDay {
    Dawn,
    Day,
    Sunset,
    Night,
}
 
fn make_fear_closure(input: TimeOfDay) -> 
impl FnMut(&mut f64) {                                
    match input {
        TimeOfDay::Dawn => |x: &mut f64| {
            *x *= 0.5;
            println!(
                "The morning sun has vanquished the horrible night.
You no longer feel afraid.\n  Fear: {x}"
            );
        },
        TimeOfDay::Day => |x: &mut f64| {
            *x *= 0.2;
            println!("What a nice day!\n  Fear: {x}");
        },
        TimeOfDay::Sunset => |x: &mut f64| {
            *x *= 1.4;
            println!("The sun is almost down! Oh dear.\n Fear: {x}");
        },
        TimeOfDay::Night => |x: &mut f64| {
            *x *= 5.0;
            println!("What a horrible night to have a curse.\n Fear: {x}");
        },
    }
}
 
fn main() {
    use TimeOfDay::*;
    let mut fear = 10.0;                                
 
    let mut make_daytime = make_fear_closure(Day);      
    let mut make_sunset = make_fear_closure(Sunset);
    let mut make_night = make_fear_closure(Night);
    let mut make_morning = make_fear_closure(Dawn);
 
    make_daytime(&mut fear);                            
    make_sunset(&mut fear);
    make_night(&mut fear);
    make_morning(&mut fear);
}

The function takes a TimeOfDay and returns a closure. We use impl FnMut(&mut f64) to say that it needs to change the value.

Starts Simon with 10

Makes four closures to call every time we want to change Simon’s fear

Calls the closures on Simon’s fear. They give a message and change the fear number.

This prints

What a nice day!
 Fear: 2
The sun is almost down! Oh dear.
 Fear: 2.8
What a horrible night to have a curse.
 Fear: 14
The morning sun has vanquished the horrible night.
You no longer feel afraid.
 Fear: 7

Is that the best way to write this code for a video game? Probably not. But it’s good practice for returning closures because being able to return a function like this can be very powerful.

12.3 Arc

You may remember in the previous chapter we used an Rc to give a variable more than one owner. If we are doing the same thing in a thread, we need an Arc. Arc stands for atomic reference counter. Atomic means that it uses atomic operations. Atomic operations are called atomic because they are indivisible (cannot be divided). For computer operations, this means that atomic operations can’t be seen in progress—they are either completed or not completed. This is why they are thread-safe because no other threads can interfere when an atomic operation is happening. Each computer processor does atomic operations in its own way. We don’t need to think about those details, but you might be wondering: If atomic operations happen on the processor level, are there processors that don’t have them? The answer is yes, but they are very rare. We can see a few of them in the documentation on Rust’s atomic types (https://doc.rust-lang.org/std/sync/atomic/index.html):

The atomic types in this module might not be available on all platforms. The atomic types here are all widely available, however, and can generally be relied upon existing. Some notable exceptions are:

So, as long as you are not building your Rust code on a very old or rare computer, you will have access to thread-safe types like Arc.

Atomic operations are important because if two threads write data at the same time, you will get an unexpected result. For example, imagine if you could do something like this in Rust without a thread-safe type like Arc:

let mut x = 10;
 
for i in 0..10 {    
    x += 1;
}
for i in 0..10 {    
    x += 1;
}

Inside thread 1

Inside thread 2

If thread 1 and thread 2 start together, maybe this will happen:

An Arc uses atomic operations to make sure this doesn’t happen (atomic operations don’t allow more than one access at one time), so it is the method you must use when you have threads. You don’t want an Arc for just one thread, though, because Rc is a bit faster. So stick with an Rc unless you have multiple threads.

You can’t change data with just an Arc, though—it’s just a reference counter. You must wrap the data in a Mutex, and then you wrap the Mutex in an Arc. Now it can have multiple owners (because it’s a reference counter); it’s thread-safe (because it’s atomic); and it’s changeable (because it’s inside a Mutex).

So, let’s use a Mutex inside an Arc to change the value of a number. First, let’s set up one thread:

fn main() {
 
    let handle = std::thread::spawn(|| {    
        println!("The thread is working!") 
    });
 
    handle.join().unwrap();                 
    println!("Exiting the program");
}

Just testing the thread

With .join(), we wait here until the thread is done.

So far, this prints

The thread is working!
Exiting the program

Good. Now let’s put it in a for loop for 0..5:

fn main() {
 
    let handle = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("The thread is working!")
        }
    });
 
    handle.join().unwrap();
    println!("Exiting the program");
}

This works, too. We get the following:

The thread is working!
The thread is working!
The thread is working!
The thread is working!
The thread is working!
Exiting the program

Now, let’s make one more thread. Each thread will do the same thing. You can see that the threads are working at the same time. Sometimes, it will say Thread 1 is working! first, but other times Thread 2 is working! is first. This is called concurrency, which comes from a Latin word meaning “running together.”

fn main() {
    let thread1 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 1 is working!")
        }
    });
 
    let thread2 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 2 is working!")
        }
    });
 
    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Exiting the program");
}

Now, we want to change the value of my_number. Right now, it is an i32. We will change it to an Arc<Mutex<i32>>, an i32 that can be changed, wrapped in an Arc:

let my_number = Arc::new(Mutex::new(0));

Now that we have this, we can clone it, and each clone can go into a different thread. We have two threads, so we will make two clones. One will go into the first thread, and the second, into the second thread:

let my_number = Arc::new(Mutex::new(0));
 
let cloned_1 = Arc::clone(&my_number);
let cloned_2 = Arc::clone(&my_number);

Now that we have thread-safe clones attached to my_number, we can move them into other threads with no problem:

use std::sync::{Arc, Mutex};
 
fn main() {
    let my_number = Arc::new(Mutex::new(0));
 
    let cloned_1 = Arc::clone(&my_number);
    let cloned_2 = Arc::clone(&my_number);
 
    let thread1 = std::thread::spawn(move || {    
        for _ in 0..10 {
            *cloned_1.lock().unwrap() += 1;       
        }
    });
 
    let thread2 = std::thread::spawn(move || {    
        for _ in 0..10 {
            *cloned_2.lock().unwrap() += 1;
        }
    });
 
    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Value is: {my_number:?}");
    println!("Exiting the program");
}

The thread uses the move keyword to take ownership, but it’s only taking ownership of a clone of the Arc, so my_number is still around.

Locks the Mutex and changes the value here

Only the clone goes into thread 2.

The program prints this every time:

Value is: Mutex { data: 20, poisoned: false, .. }
Exiting the program

It was a success!

We can then join the threads together in a single for loop, which lets us use many more threads without having to write a lot of extra code. We need to save the handles somewhere so we can call .join() on each one outside of the loop, as we learned before:

use std::sync::{Arc, Mutex};
 
fn main() {
    let my_number = Arc::new(Mutex::new(0));
    let mut handle_vec = vec![];                                        
 
    for _ in 0..10 {                                                    
        let my_number_clone = Arc::clone(&my_number);                   
        let handle = std::thread::spawn(move || {                       
            for _ in 0..10 {
                *my_number_clone.lock().unwrap() += 1;
            }
        });
        handle_vec.push(handle);                                        
    }
 
    handle_vec.into_iter().for_each(|handle| handle.join().unwrap());   
    println!("{my_number:?}");
}

Our JoinHandles will go inside here.

Let’s use 10 threads this time.

Makes a clone before starting the thread

Uses move to make the thread own the clone

Saves the JoinHandle so we can call .join() on it outside of the loop

Finally, calls .join() on all the handles

Finally, this prints Mutex { data: 100, poisoned: false, .. }.

This looks complicated, but Arc<Mutex<SomeType>>> is used very often in Rust, and using this pattern quickly becomes natural. In the meantime, you could also rewrite your code to make it easier for you to read while you are still getting used to the syntax. Here is the same code with one more use statement and two functions. The functions don’t do anything new, but they move some code out of main() and might make reasoning about the code a little easier:

use std::sync::{Arc, Mutex};
use std::thread::spawn;                                      
 
fn make_arc(number: i32) -> Arc<Mutex<i32>> {                
    Arc::new(Mutex::new(number))
}
 
fn new_clone(input: &Arc<Mutex<i32>>) -> Arc<Mutex<i32>> {   
    Arc::clone(&input)
}
 
fn main() {
    let mut handle_vec = vec![];
    let my_number = make_arc(0);
 
    for _ in 0..10 {
        let my_number_clone = new_clone(&my_number);
        let handle = spawn(move || {
            for _ in 0..10 {
                let mut value_inside = my_number_clone.lock().unwrap();
                *value_inside += 1;
            }
        });
        handle_vec.push(handle);
    }
    handle_vec.into_iter().for_each(|handle| handle.join().unwrap());
    println!("{my_number:?}");
}

We can write spawn to start a new thread.

A function that makes a Mutex wrapped in an Arc. We’re using it to shrink the code a bit and make it easier to understand.

Same here—just a function to make this thread example easier to read.

After all this learning about threads, we have another kind of thread to learn. But don’t worry, this one is actually easier to use! Let’s take a look.

12.4 Scoped threads

Scoped threads are a fairly recent addition to Rust, as they were only stabilized in August 2022 when Rust 1.63 was released. Remember in the last example how you had to clone the Arc for regular threads and use move to take ownership because regular threads need a 'static guarantee? Scoped threads don’t need this because they are guaranteed to live inside a single scope (inside the {} curly brackets). Here’s what the documentation says (http://mng.bz/5onD): “Unlike non-scoped threads, scoped threads can borrow non-'static data, as the scope guarantees all threads will be joined at the end of the scope.”

Nice! That means you don’t need to use .join() either because the threads will be automatically joined at the end of the scope. Let’s look at the difference. With a regular thread, you use thread::spawn() to start a thread:

use std::thread;
 
fn main() {
    thread::spawn(|| {   
                         
    });
    thread::spawn(|| {   
                         
    });
                         
}

Do more thread stuff.

Don’t forget to join them here; otherwise, main() might end before the threads do.

With scoped threads, you start with a scope, using thread::scope(). The threads will only live inside there. Then you use the closure that scope gives you to spawn the threads:

use std::thread;
 
fn main() {
    thread::scope(|s| {   
        s.spawn(|| {
                          
        });
        s.spawn(|| {
                          
        });#
    });                   
}

We are just calling it "s" here. You could call it anything.

Do thread stuff.

The threads automatically join here, so there’s no need to think about JoinHandles.

Now, let’s take our previous example and put it in scoped threads instead. Look at how much simpler the code is. You still need a Mutex because more than one thread is changing my_number, but you don’t need an Arc anymore. You don’t need to use move because the threads aren’t forced to take ownership: they can just borrow the values because the threads are guaranteed to not exist after the scope is over:

use std::sync::Mutex;
use std::thread;
 
fn main() {
    let my_number = Mutex::new(0);
    thread::scope(|s| {
        s.spawn(|| {
            for _ in 0..10 {
                *my_number.lock().unwrap() += 1;
            }
        });
        s.spawn(|| {
            for _ in 0..10 {
                *my_number.lock().unwrap() += 1;
            }
        });
    });
}

In fact, you don’t need a Mutex at all if only one thread is using your data. Scoped threads follow all the regular borrowing rules in Rust, so if only one has a mutable borrow, there will be no problem. Let’s add two regular numbers (one mutable, one immutable) to our scoped threads and take a look:

use std::sync::Mutex;
use std::thread;
 
fn main() {
    let mutex_number = Mutex::new(0);                
    let mut regular_mut_number = 0;                  
    let regular_unmut_number = 0;                    
 
    thread::scope(|s| {
        s.spawn(|| {
            for _ in 0..3 {
                *mutex_number.lock().unwrap() += 1;
                regular_mut_number += 1;
                println!("Multiple immutable borrows is fine! {regular_unmut_number}");
            }
        });
        s.spawn(|| {
            for _ in 0..3 {
                *mutex_number.lock().unwrap() += 1;
                // regular_mut_number += 1;          
                println!("Borrowing {regular_unmut_number} here too, it's
                just fine!");
            }
        });
    });
 
    println!("mutex_number: {mutex_number:?}");
    println!("regular_mut_number: {regular_mut_number}");
}

Both threads use this, so we use a thread-safe Mutex.

Only one thread will modify this, so there’s no need for a Mutex.

This variable isn’t variable, so both threads can borrow it.

This part is commented out because it won’t work: it’s two mutable references at the same time, which is never allowed in Rust.

The output will look different each time because of the multiple threads working at the same time but will be something similar to this:

Borrowing 0 here too, it's just fine!
Multiple immutable borrows is fine! 0
Multiple immutable borrows is fine! 0
Borrowing 0 here too, it's just fine!
Multiple immutable borrows is fine! 0
Borrowing 0 here too, it's just fine!
mutex_number: Mutex { data: 6, poisoned: false, .. }
regular_mut_number: 3

If you want to see a busier (but easier to read) example, just try something like this:

use std::thread;
 
fn main() { 
    thread::scope(|s| {
        for thread_number in 0..1000 {
            s.spawn(move|| {
                println!("Thread number {thread_number}");   
            });
        };
    });
}

If you run this, you will see that there are, indeed, a lot of threads running at the same time. The output will be different every time. One example while writing the book looked like this:

Thread number 2
Thread number 305
Thread number 176
Thread number 50
Thread number 175
Thread number 3
Thread number 4
Thread number 5
Thread number 6
Thread number 7

So, if you are okay with your threads only living inside a single scope, be sure to check out scoped threads. Regular threads have the advantage of living forever as long as your program is running, but scoped threads are easy to spawn and use if you have a task to accomplish and don’t need them after the task is done.

12.5 Channels

Using a channel in the Rust standard library is an easy way to send information to one receiver, even between threads. Channels are fairly popular because they are thread-safe but pretty simple to put together. The flexibility of channels is another reason for their popularity. A channel has one or more senders and one receiver, which you can put wherever you want, such as on other structs and inside other functions. But once you have opened a channel between them, you can send from a sender to the receiver no matter where they are located.

12.5.1 Channel basics

You can create a channel in Rust with the channel() function in std::sync::mpsc. The letters mpsc stand for “multiple producer, single consumer”—in other words, “many senders sending to one place.” The name channel is well chosen because an mpsc channel is like the channels of a river: you can have many small streams, but they all flow into the same larger river downstream. To start a channel, use the channel() function, which creates a Sender and a Receiver. These two are tied together, and both hold the same generic type. You can see this in the function signature:

pub fn channel<T>() -> (Sender<T>, Receiver<T>)

One sends a T; the other receives a T. Simple!

The output of the channel() function is a tuple, so the best way to start out is to choose one name for the sender and one for the receiver (destructuring). Usually, you see something like let (sender, receiver) = channel(); to start. Because the function is generic, Rust won’t know the type if all you do is type channel():

use std::sync::mpsc::channel;
 
fn main() {
    let (sender, receiver) = channel();
}

The compiler says

error[E0282]: type annotations needed for `(std::sync::mpsc::Sender<T>,
std::sync::mpsc::Receiver<T>)`
  --> src\main.rs:30:30
   |
30 |     let (sender, receiver) = channel();
   |         ------------------   ^^^^^^^ cannot infer type for type
   parameter `T` declared on the function `channel`
   |         |
   |         consider giving this pattern the explicit type
   `(std::sync::mpsc::Sender<T>, std::sync::mpsc::Receiver<T>)`,
   where the type parameter `T` is specified

It suggests adding a type for the Sender and Receiver. You could specify the type if you want:

use std::sync::mpsc::{channel, Sender, Receiver};
 
fn main() {
    let (sender, receiver): (Sender<i32>, Receiver<i32>) = channel();
}

But you don’t have to. Once you start using the Sender and Receiver, Rust will be able to infer the type.

12.5.2 Implementing a channel

Let’s look at the simplest way to use a channel:

use std::sync::mpsc::channel;
 
fn main() {
    let (sender, receiver) = channel();
 
    sender.send(5);
    receiver.recv();      
}

recv stands for “receive,” not “rec v.”

We have sent a 5, which is an i32, from the Sender to the Receiver, so now Rust knows the type.

Each of these methods might fail, so they each return a Result. The .send() method for sender returns a Result<(), SendError<i32>> and the receiver’s method returns a Result<i32, RecvError>. You can use .unwrap() to see whether the sending works or use better error handling. Let’s add .unwrap() and also println! to see what we get:

use std::sync::mpsc::channel;
 
fn main() {
    let (sender, receiver) = channel();
 
    sender.send(5).unwrap();
    println!("{}", receiver.recv().unwrap());
}

This prints 5, showing that we successfully sent the value from the Sender to the Receiver.

A channel is like an Arc because you can clone it and send the clones into other threads. Let’s make two threads and send values to receiver:

use std::sync::mpsc::channel;
 
fn main() {
    let (sender, receiver) = channel();
    let sender_clone = sender.clone();
 
    std::thread::spawn(move || {
        sender.send("Send a &str this time").unwrap();             
        sender.send("Send a &str this time").unwrap();             
    });
 
    std::thread::spawn(move || {
        sender_clone.send("And here is another &str").unwrap();    
        sender_clone.send("And here is another &str").unwrap();    
    });
 
    while let Ok(res) = receiver.recv() {
        println!("{res}");
    }
}

Moves sender in

Moves sender_clone in

This will print those four &strs in the order the Receiver gets them.

Be careful, though: .recv() is a blocking function. A Sender gets dropped at the end of its scope (same as any other variable in Rust), but the Receiver using .recv() will keep blocking if the Sender is still alive. So, if a Sender thread is taking a long time to process before sending, the Receiver will just keep waiting.

In fact, our Receiver does wait quite a bit in this example. If you change the last part to while let Ok(res) = receiver.try_recv(), you probably won’t see any output because the Receiver will quickly see whether there is anything to receive, see that nothing has been sent yet, and give up right away.

In addition, if you change .recv() to .try_recv(), you might get a panic because the Receiver gets dropped after trying just once while the Senders are still trying to send. That’s because we’re using .unwrap() here, of course. In real code, you don’t want to.unwrap() everywhere.

Let’s finish up by quickly looking at how the .send() and .recv() methods can fail. The .send() method will always fail if the Receiver has been dropped. We can easily try this out by dropping the Receiver and trying to send:

use std::sync::mpsc::channel;
 
fn main() {
    let (sender, receiver) = channel();
 
    drop(receiver);
    if let Err(e) = sender.send(5) {
        println!("Got an error: {e}")
    }
}

This will print

Got an error: sending on a closed channel

That’s easy enough. Meanwhile, .recv() will return an Err if the Sender has been dropped and there is no more data to receive. However, if there is still sent data for the Receiver to receive, it will return Ok with the data inside even if the Sender has been dropped.

For example, if the Sender sends twice and the Receiver tries to receive three times, it will keep blocking and the program will never end:

use std::sync::mpsc::channel;
 
fn main() {
    let (sender, receiver) = channel();
 
    sender.send(5).unwrap();
    sender.send(5).unwrap();
    
    println!("{:?}", receiver.recv());
    println!("{:?}", receiver.recv());
    println!("{:?}", receiver.recv());
}

But if the Sender is first dropped, the .recv() method will not block anymore on the third try. Instead, it will recognize that the data has all been received and the channel has been closed, so it returns an Err instead of blocking:

use std::sync::mpsc::channel;
 
fn main() {
    let (sender, receiver) = channel();
 
    sender.send(5).unwrap();
    sender.send(5).unwrap();
    drop(sender);
    
    println!("{:?}", receiver.recv());
    println!("{:?}", receiver.recv());
    println!("{:?}", receiver.recv());
}

The output is

Ok(5)
Ok(5)
Err(RecvError)

This chapter was a lot of work. We learned how to pass closures into your own functions by picking among Fn, FnMut, and FnOnce and writing the closure’s signature. We also learned about impl Trait and how it differs from regular generics. Finally, we got to some more thread-related functionality: Arc (like Rc but thread-safe), scoped threads as an alternative to regular threads, and channels as another thread-safe way to pass information from one end to another.

After all the hard work in the latest chapters, we are now a little bit more than halfway through the book. Here is a pleasant surprise: you’ve already covered a lot of Rust’s most difficult concepts. Good work! There is a lot more content to learn as we move toward the end of the book, but the learning curve will not be as steep. The next chapter will be easy compared to this one, as we learn to read documentation and about the smart pointer known as a Box and what makes it so useful.

Summary