11 Multiple threads and a lot more

This chapter covers

You’re getting pretty good at Rust by now, so it’s time to take a look at some more advanced types. This chapter doesn’t really have a single theme. Instead, we’ll look in turn at some advanced subjects: Cow, type aliases, Rc, and multiple threads. Understanding how multiple threads work is probably the hardest part of this chapter. The famous Cow type (yes, that’s its real name) is a little bit tricky, too. You’ll probably like the Rc (reference counter) type as it gives you a bit of extra flexibility when it comes to Rust’s ownership rules.

11.1 Importing and renaming inside a function

Usually, you write use at the top of the program like this:

use std::cell::{Cell, RefCell};

But we saw that you can do this anywhere, especially in functions with enums that have long names. Here is an example:

enum MapDirection {
    North,
    NorthEast,
    East,
    SouthEast,
    South,
    SouthWest,
    West,
    NorthWest,
}
 
fn give_direction(direction: &MapDirection) {
    match direction {
        MapDirection::North => println!("You are heading north."),
        MapDirection::NorthEast => println!("You are heading northeast."), 
    }
}

So much more left to type before the code will compile.

So now we will import MapDirection inside the function. That means that inside the function, you can simply write North , NorthEast, and so on:

enum MapDirection {
    North,
    NorthEast,
    East,
    SouthEast,
    South,
    SouthWest,
    West,
    NorthWest,
}
 
fn give_direction(direction: &MapDirection) {
    use MapDirection::*;      
 
    match direction {
        North =>              
    }
}

Imports everything in MapDirection

And so on for each variant until the code compiles.

We’ve seen that ::* means “import everything after the ::.” In our case, that means North, NorthEast, and so on—all the way to NorthWest. When you import other people’s code, you can do that, too, but if the code is very large, you might have problems. What if it has some items that have the same name as those in your code? It’s usually best not to use ::* all the time unless you’re sure. A lot of times you see a section called prelude in other people’s code with all the main items you probably need. Then you will usually use it like this: name::prelude::*. We will talk about this more in the sections for modules and crates.

If you have duplicate names or you have some reason to change a type name, you can use as to do it. This can be done with any type:

fn main() {
    use String as S;
    let my_string = S::from("Hi!");
}

You might find this useful when using someone else’s code and aren’t satisfied with the naming. This enum is a bit awkwardly named:

enum FileState {
    CannotAccessFile,
    FileOpenedAndReady,
    NoSuchFileExists,
    SimilarFileNameInNextDirectory,
}

Let’s try importing this enum’s variants and giving them all a different name. Since 2021, in Rust, you can even change their names to another language:

enum FileState {
    CannotAccessFile,
    FileOpenedAndReady,
    NoSuchFileExists,
    SimilarFileNameInNextDirectory,
}
 
fn give_filestate(input: &FileState) {
    use FileState::{
        CannotAccessFile as NoAccess,
        FileOpenedAndReady as 잘됨,       
        NoSuchFileExists as NoFile,
        SimilarFileNameInNextDirectory as OtherDirectory
    };
    match input {
        NoAccess => println!("Can't access file."),
        잘됨 => println!("Here is your file"),
        NoFile => println!("Sorry, there is no file by that name."),
        OtherDirectory => println!("Please check the other directory."),
    }
}

Korean for “works great.”

Using imports in this way lets you type OtherDirectory instead of FileState:: SimilarFileNameInNextDirectory.

Very handy! Having learned about importing and renaming, let’s move on and take a look at the todo! macro next.

11.2 The todo! macro

Rust users love the todo! macro because it lets you tell the compiler to be quiet for a bit. Sometimes, you want to write the general structure of your code to help you imagine your project’s final form (writing the general structure of your code is called prototyping). For example, imagine a simple project to do something with books. The comments in the code show what you might be thinking as you write it:

// Okay, first I need a book struct.
// Nothing in there yet - will add later
struct Book;
 
// A book can be hardcover or softcover, so add an enum...
enum BookType {
    HardCover,
    SoftCover,
}
 
// should take a &Book and return an Option<String>
fn get_book(book: &Book) -> Option<String> {} 
 
// should take a ref Book and return a Result...
fn delete_book(book: &Book) -> Result<(), String> {} 
                                                    
// TODO: impl block and make these functions methods...
// TODO: make this a proper error
fn check_book_type(book_type: &BookType) {
 
// Let's make sure the match statement works
    match book_type {
        BookType::HardCover => println!("It's hardcover"),
        BookType::SoftCover => println!("It's softcover"),
    }
}
 
fn main() {
    let book_type = BookType::HardCover;
    // Okay, let's check this function!
    check_book_type(&book_type);
}

Unfortunately, Rust is not happy with .get_book() and .delete_book() and won’t even compile the code:

error[E0308]: mismatched types
  --> src\main.rs:32:29
   |
32 | fn get_book(book: &Book) -> Option<String> {}
   |    --------                 ^^^^^^^^^^^^^^ expected enum
   `std::option::Option`, found `()`
   |    |
   |    implicitly returns `()` as its body has no tail or `return`
   expression
   |
   = note:   expected enum `std::option::Option<std::string::String>`
           found unit type `()`
 
error[E0308]: mismatched types
  --> src\main.rs:34:31
   |
34 | fn delete_book(book: Book) -> Result<(), String> {}
   |    -----------                ^^^^^^^^^^^^^^^^^^ expected enum
   `std::result::Result`, found `()`
   |    |
   |    implicitly returns `()` as its body has no tail or `return`
   expression
   |
   = note:   expected enum `std::result::Result<(), std::string::String>`
           found unit type `()`

But maybe you don’t feel like finishing the .get_book() and .delete_book() functions right now because you want to finish the code’s general structure first. This is where you can use todo!(). If you add that to the function, Rust will stop complaining and compile your code:

struct Book;
 
enum BookType {
    HardCover,
    SoftCover,
}
 
fn get_book(book: &Book) -> Option<String> {
    todo!();
} 
 
fn delete_book(book: &Book) -> Result<(), String> {
    todo!();
} 
                                                    
fn check_book_type(book_type: &BookType) {
    match book_type {
        BookType::HardCover => println!("It's hardcover"),
        BookType::SoftCover => println!("It's softcover"),
    }
}
 
fn main() {
    let book_type = BookType::HardCover;
    check_book_type(&book_type);
}

Now the code compiles, and you can see the result of .check_book_type():

It's hardcover

Make sure that you don’t call the functions that have todo! inside. Rust will compile our code and let us use it, but if it comes across a todo!, it will automatically panic.

Also, todo! functions still need signatures that Rust can understand. Code that uses undeclared types, like the next example, won’t work even if you put a todo! inside the code:

struct Book;
 
fn get_book(book: &Book) -> WorldsBestType {
    todo!()
}

It will say

error[E0412]: cannot find type `WorldsBestType` in this scope
  --> src\main.rs:32:29
   |
32 | fn get_book(book: &Book) -> WorldsBestType {
   |                             ^^^^^^^^^^^^^^ not found in this scope

You can use todo! in other places, too, like struct parameters:

struct Book {
    name: String,
    year: u8
}
 
fn make_book() -> Book {
    Book {
        name: todo!(),
        year: todo!()
    }
}
 
fn main() {}

Here, too, the .make_book() function never gets called so the code will compile and run without panicking.

One final note: todo! is the same as another macro called unimplemented!. Rust users originally only had unimplemented! to use, but it was a bit too much to type, so the macro todo! was created, which is shorter.

11.3 Type aliases

A type alias means “giving a new name to another type.” Type aliases are very easy because they don’t change the type at all (just the name). Usually, you use them when you have a long type name that makes your code difficult to read or when you want to describe an existing type in a different way. Here are two examples of type aliases.

First, say you have a type that is not difficult to read, but you want to make your code easier to understand for other people (or for you):

type CharacterVec = Vec<char>;

When you have a type that makes your code difficult to read:

fn returns_some_chars(input: Vec<char>) ->
std::iter::Take<std::iter::Skip<std::vec::IntoIter<char>>> {
    input.into_iter().skip(4).take(5)
}

You can change it to this:

type SkipFourTakeFive = 
std::iter::Take<std::iter::Skip<std::vec::IntoIter<char>>>;
 
fn returns_some_chars(input: Vec<char>) -> SkipFourTakeFive {
    input.into_iter().skip(4).take(5)
}

You could also import items to make the type shorter instead of using a type alias:

use std::iter::{Take, Skip};
use std::vec::IntoIter;
 
fn returns_some_chars(input: Vec<char>) -> Take<Skip<IntoIter<char>>> {
    input.into_iter().skip(4).take(5)
}

You can decide what looks best in your code depending on what you like.

Keep in mind, however, that a type alias doesn’t create an actual new type. It’s just a name to use instead of an existing type. So if you write type File = String;, the compiler just sees a String. So this will print true:

type File = String;
 
fn main() {
    let my_file = File::from("I am file contents");
    let my_string = String::from("I am file contents");
    println!("{}", my_file == my_string);
}

Because a type alias isn’t a new type, it doesn’t violate the orphan rule. You can use them on anybody’s type with no problem because you’re not touching the original type.

11.4 Cow

Cow is a very convenient enum. It means “clone on write” and lets you return a &str if you don’t need an owned String or a String if you do. It can also do the same with any other types that you might want to borrow but also might want to own.

To understand it, let’s look at the signature. It’s a bit complicated, so we’ll first start with a very simplified version:

enum Cow {
    Borrowed,
    Owned
}

Okay, so a Cow offers two choices.

Next is the generic part: Cow is generic over a single type called B (it could have been called anything, but the creators of the standard library chose B). Both Borrowed and Owned have it:

enum Cow<B> {
    Borrowed(B),
    Owned(B),
}

Now, let’s take a look at the real signature, which involves lifetimes:

enum Cow<'a, B>
where
    B: 'a + ToOwned + ?Sized,
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

We already know that 'a means that Cow can hold a reference. The ToOwned trait means that B must be a type that can be turned into an owned type. For example, str is usually a reference (&str), and you can turn it into an owned String.

Next is ?Sized. This means “maybe Sized, but maybe not.” Remember the term dynamically sized? Almost every type in Rust is Sized, but types like str are not. That is why we need an & for a str because the compiler doesn’t know the size. If you want a trait that can use something like a str, you add ?Sized, which means “might be dynamically sized.”

Now, let’s look at the enum’s variants, Borrowed and Owned. Imagine that you have a function that returns Cow<'static, str>. If you tell the function to return "My message".into(), it will look at the type: "My message" is a str. This is a Borrowed type, so it chooses Borrowed(&'a B). It becomes Cow::Borrowed(&'static str).

If you give it a format!("{}", "My message").into(), it will look at the type. This time, it is a String because format!() makes a String. This time it will select "Owned" and return that.

Let’s put together a quick example that shows how Cow might be useful and how to match on a Cow. We’ll have a function called generate_message() that generates messages, which are usually a &'static str. But when an error happens, we can add some more information with a struct called ErrorInfo:

use std::borrow::Cow;
 
#[derive(Debug)]
struct ErrorInfo {                       
    error: LocalError,
    message: String,
}
 
#[derive(Debug)]
enum LocalError {
    TooBig,
    TooSmall,
}
 
fn generate_message(
    message: &'static str, 
    error_info: Option<ErrorInfo>
) -> Cow<'static, str> {                 
    match error_info {
        None => message.into(),
        Some(info) => format!("{message}: {info:?}").into(),
    }
}
 
fn main() {
    let msg1 = generate_message(         
        "Everything is fine",
        None
    );
    let msg2 = generate_message(
        "Got an error",
        Some(ErrorInfo {
            error: LocalError::TooBig,
            message: "It was too big".to_string(),
        }),
    );
 
    for msg in [msg1, msg2] {            
        match msg {
            Cow::Borrowed(msg) => {
                println!("Borrowed, didn't need an allocation:\n  {msg}")
            }
            Cow::Owned(msg) => {
                println!("Owned, because we needed an allocation:\n  {msg}")
            }
        }
    }
}

No surprises in these two structs. LocalError is an enum, and ExtraInfo holds a LocalError and a String.

If we only pass in a &'static str and no extra info, we won’t need an allocation, so the Cow will be a Cow::Borrowed. But if we need the extra info, we will need an allocation, and it will be a Cow::Owned that owns its data.

Now, let’s make two messages: one that won’t need an allocation and one that will.

And since Cow is just a simple enum, we can always match on it to see if it’s a Cow::Borrowed or a Cow::Owned.

Here is the output:

Borrowed message, didn't need an allocation:
  Everything is fine
Owned message because we needed an allocation:
  Got an error: ExtraInfo { error: TooBig, message: "It was too big" }

Cow has some other methods, like into_owned or into_borrowed, so you can change it if you need to.

Cow is a convenient type to place on your structs and enums, too, and is another method to take both &str and String if you want. Imagine you have a User struct that you would like to take either &str or String, but you don’t want to clone or use .to_string() if you don’t have to. You can use a Cow here, too:

use std::borrow::Cow;
 
struct User {
    name: Cow<'static, str>,
}
 
fn main() {
    let user_name = "User1";
    let other_user_name = "User10".to_string();
 
    let user1 = User {
        name: user_name.into(),
    };
 
    let user2 = User {
        name: other_user_name.into(),
    };
 
    for name in [user1.name, user2.name] {
        match name {
            Cow::Borrowed(n) => {
                println!("Borrowed name, didn't need an allocation:\n  {n}")
            }
            Cow::Owned(n) => {
                println!("Owned name because we needed an allocation:\n  {n}")
            }
        }
    }
}

Once again, both borrowed and owned values work just fine:

Borrowed name, didn't need an allocation:
  User1
Owned name because we needed an allocation:
  User10

Both "User1" and "User10".to_string() work! Of course, if you also want to take in a nonstatic &str, you would write User<'a> and name: Cow<'a, str> instead of 'static to let Rust know that the reference will live long enough. So this code would work, too:

use std::borrow::Cow;
 
struct User<'a> {                                   
    name: Cow<'a, str>,
}
 
fn main() {
    let user_name = "User1";
    let other_user_name = &"User10".to_string();    
 
    let user1 = User {
        name: user_name.into(),
    };
 
    let user2 = User {
        name: other_user_name.into(),
    };
 
    for name in [user1.name, user2.name] {
        match name {
            Cow::Borrowed(n) => {
                println!("Borrowed name, didn't need an allocation:\n  {n}")
            }
            Cow::Owned(n) => {
                println!("Owned name because we needed an allocation:\n  {n}")
            }
        }
    }
}

Here we are using the lifetime <'a> instead of 'static . . .

. . . which means that we can now use a reference to a String that doesn’t have a 'static lifetime.

The output shows that both names were used as a Cow::Borrowed because they are both borrowed values:

Borrowed name, didn't need an allocation:
  User1
Borrowed name, didn't need an allocation:
  User10

We’ve had a good look at the Cow type; let’s move on to the next advanced Rust concept, Rc, a useful type that provides some flexibility in a language as strict as Rust.

11.5 Rc

Rc stands for “reference counter” (or “reference counted,” depending on who you ask). Rc is used a lot as a way to get around Rust’s strict rules on ownership—without actually breaking them—by allowing shared ownership and by keeping a careful eye on how long the data is being shared. Let’s learn what makes Rc useful.

11.5.1 Why Rc exists

We know that in Rust, every variable can only have one owner. That is why this doesn’t work:

fn takes_a_string(_unused_string: String) {}
 
fn main() {
    let user_name = String::from("User MacUserson");
    takes_a_string(user_name);
    takes_a_string(user_name);
}

After takes_a_string takes user_name, you can’t use it anymore. For us, this is no problem: you can give it user_name.clone(). However, sometimes, a variable is part of a struct, and maybe you can’t clone the struct. Or maybe the String is really long, and you don’t want to clone it. An Rc gets around this by letting you have more than one owner. An Rc is like a good office worker: it writes down who has ownership and how many of them there are. Once the number of owners goes down to 0, the value can be dropped.

One interesting thing about Rc has to do with garbage collection. Rust doesn’t use garbage collection, which is why you have to think about things like references and lifetimes. But most other languages use garbage collection invisibly in a manner similar to Rc: the language keeps track of where memory is being shared and later cleans it up when nobody is using it anymore. That’s why a lot of new programmers to Rust use Rc a lot because it lets them not worry about references and lifetimes so much.

11.5.2 Using Rc in practice

Here is how you use an Rc. First, imagine two structs: one called City and another called CityData. City has information for one city, and CityData puts all the cities together in Vecs:

#[derive(Debug)]
struct City {
    name: String,
    population: u32,
    city_history: String,
}
 
#[derive(Debug)]
struct CityData {
    names: Vec<String>,
    histories: Vec<String>,
}
 
fn main() {
    let calgary = City {
        name: "Calgary".to_string(),
        population: 1_200_000,       
        city_history: "Calgary began as a fort called Fort Calgary   
        that...".to_string(),
    };
    let canada_cities = CityData {
        names: vec![calgary.name],                                   
        histories: vec![calgary.city_history],                       
    };
    println!("Calgary's history is: {}", calgary.city_history);
}

Pretend that this string is very, very long.

This uses calgary.name, which is short.

But this String is long.

Of course, it doesn’t work because canada_cities owns the data and calgary doesn’t. It says

error[E0382]: borrow of moved value: `calgary.city_history`
  --> src\main.rs:27:42
   |
24 |         histories: vec![calgary.city_history],                 
   |                         -------------------- value moved here
...
27 |     println!("Calgary's history is: {}", calgary.city_history);
   |                                          ^^^^^^^^^^^^^^^^^^^^ value
   borrowed here after move
   |
   = note: move occurs because `calgary.city_history` has type
   `std::string::String`, which does not implement the `Copy` trait

But this String is very long.

You can easily clone the Strings, but you can also wrap everything that you want to share inside an Rc. Here’s how to do it. First, add the use declaration:

use std::rc::Rc;

Then, put Rc around everything we want to share:

use std::rc::Rc;
 
#[derive(Debug)]
struct City {
    name: Rc<String>,
    population: u32,
    city_history: Rc<String>,
}
 
#[derive(Debug)]
struct CityData {
    names: Vec<Rc<String>>,
    histories: Vec<Rc<String>>,
}

To add a new reference, you have to clone the Rc.

But, hold on! Didn’t we want to avoid using .clone()? Not exactly—we didn’t want to clone the whole String. But a clone of an Rc just clones the pointer: it’s basically free. It’s like putting a name sticker on a box of books to show two people own it instead of making a whole new box.

You can clone an Rc called item with Rc::clone(&item) or item.clone(). Usually, Rc::clone(&item) is better because an Rc holds a type that might have its own methods (including .clone()!). Thus, it’s a good way to show that you are cloning the Rc, not the object inside it.

There is also a method for Rc called strong_count() that shows you how many owners there are for a piece of data. We will use this method in the following code, too. How many owners do you think there are for Calgary’s city history?

use std::rc::Rc;
 
#[derive(Debug)]
struct City {
    name: Rc<String>,
    population: u32,
    city_history: Rc<String>,                          
}
 
#[derive(Debug)]
struct CityData {
    names: Vec<Rc<String>>,
    histories: Vec<Rc<String>>,                        
}
 
fn main() {
 
    let calgary_name = Rc::new("Calgary".to_string());
    let calgary_history = Rc::new("Calgary began as a fort called Fort
    Calgary that...".to_string());
 
    let calgary = City {
        name: Rc::clone(&calgary_name),
        population: 1_200_000,
        city_history: Rc::clone(&calgary_history)
    };
 
    let canada_cities = CityData {
        names: vec![Rc::clone(&calgary_name)],         
        histories: vec![Rc::clone(&calgary_history)],
    };
 
    println!("Calgary's history is: {}", calgary.city_history);
    println!("{}", Rc::strong_count(&calgary.city_history));
}

A String inside an Rc

A Vec of Strings inside Rcs

.clone() will increase the count.

The answer is 3, as the output shows:

Calgary's history is: Calgary began as a fort called Fort Calgary that...
3

First, we made a String inside an Rc: one owner. Then, we cloned the Rc, and the City struct is using it: two owners. Finally, we cloned it again, and the CityData struct is using it: three owners.

If there are strong pointers, are there also weak pointers? Yes, there are. Weak pointers are useful because if two Rcs point at each other, they can’t die (to be precise, they can’t drop their values). This is called a reference cycle. If item 1 has an Rc to item 2 and item 2 has an Rc to item 1, they can’t get to 0 and will never be able to drop their values. In this case, you want to use weak references. Weak references still maintain a memory allocation but allow the Rc to drop its value. Rc will count both strong and weak references, but strong references are the only references that keep an Rc from dropping its value. You use Rc::downgrade(&item) instead of Rc::clone(&item) to make weak references. Also, you use Rc::weak_count(&item) to see the weak count.

Remember the quick example with the two functions that each take a String? Solving it with reference counters is easy now that you know how: wrap a String in an Rc and then clone it with Rc::clone(). Change the function signatures from String to Rc<String>, and you’re done! Now it looks like this:

use std::rc::Rc;
 
fn takes_a_string(input: Rc<String>) {
    println!("It is: {input}")
}
 
fn main() {
    let user_name = Rc::new(String::from("User MacUserson"));
 
    takes_a_string(Rc::clone(&user_name));
    takes_a_string(Rc::clone(&user_name));
}

Finally, we have the output we wanted:

It is: User MacUserson
It is: User MacUserson

11.5.3 Avoiding lifetime annotations with Rc

Rc has been interesting so far, but the two examples haven’t really given us an overwhelming reason to use them. If you search online, you’ll see that Rc is really popular with new Rust users. The reason is that using Rc lets you avoid writing lifetimes while not needing to use .clone(). Let’s look at our example from the last chapter with the City struct, which holds a &'a str for its name. But this time, we have added two more structs: a Country struct that holds a Vec<City> and a World struct that holds a Vec<Country>. We write this, but it won’t compile yet:

#[derive(Debug)]
struct City<'a> {
    name: &'a str,
    date_founded: u32,
}
 
#[derive(Debug)]
struct Country {
    cities: Vec<City>
}
 
#[derive(Debug)]
struct World {
    countries: Vec<Country>
}
 
fn main() {
    let city_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()];
    let my_city = City {
        name: &city_names[0],
        date_founded: 1921,
    };
    println!("{} was founded in {}", my_city.name, my_city.date_founded);
}

Here is the error:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:17
  |
9 |     cities: Vec<City>
  |                 ^^^^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
8 ~ struct Country<'a> {
9 ~     cities: Vec<City<'a>>
  |

City has a lifetime of <'a>, and so the compiler wants to know how long Country will live in relation to City. Will they both share the same lifetime?

Fine, we will give them the same lifetime:

#[derive(Debug)]
struct Country<'a> {
    cities: Vec<City<'a>>
}

The compiler gives the same error message again because World holds a Vec<Country>, and Country has the lifetime <'a>. We indicate the lifetime again in the same way and now the code compiles:

#[derive(Debug)]
struct World<'a> {
    countries: Vec<Country<'a>>
}

That works just fine, but it took a lot of typing. It is starting to feel a bit awkward to keep writing this lifetime just because we don’t want to clone a String inside the city_names Vec. Every time a City, Country, or World gets used anywhere, we will need to specify the lifetime again.

Let’s try an Rc instead: we’ll make city_names a Vec<Rc<String>> instead of a Vec<String> and clone the Rc. City will take an Rc<String> for its name instead of a &'a str, and we can get rid of all the lifetime annotations everywhere else. Now the code looks like this:

use std::rc::Rc;
 
#[derive(Debug)]
struct City {
    name: Rc<String>,
    date_founded: u32,
}
 
#[derive(Debug)]
struct Country {
    cities: Vec<City>,
}
 
#[derive(Debug)]
struct World {
    countries: Vec<Country>,
}
 
impl World {}     
 
fn main() {
    let city_names = vec![
        Rc::new("Ichinomiya".to_string()),
        Rc::new("Kurume".to_string()),
    ];
 
    let my_city = City {
        name: Rc::clone(&city_names[0]),
        date_founded: 1921,
    };
 
    println!("{} was founded in {}", my_city.name, my_city.date_founded);
}

If we were still using lifetimes, you would need to write impl World<'_> here.

This compiles fine and prints Ichinomiya was founded in 1921.

Hopefully, you’ll get some good use out of the Rc type. Let’s move on to the final topic in this chapter: multiple threads.

11.6 Multiple threads

Using multiple threads allows us to do many things at the same time. Modern computers have multiple cores and multiple threads, so they can do more than one thing at the same time, and Rust lets you use them. Rust uses threads that are called OS threads, each of which gets its own state stack and local state, making OS threads both efficient and independent. (Some other languages use what are known as green threads, which need a run time and are less powerful.)

11.6.1 Spawning threads

You create threads with std::thread::spawn and a closure to tell it what to do. Threads are interesting because they run at the same time, and it can be fun to run your code to see what happens as they all operate at the same time. What do you think the output of this simple example will be?

fn main() {
    std::thread::spawn(|| {
        println!("I am printing something");
    });
}

In fact, the output will be different every time. Sometimes it will print, and sometimes it won’t (this depends on your computer, too, and usually, it won’t print in the Playground). That is because sometimes main() finishes before the thread finishes, and when main() finishes, the program is over. This is easier to see in a for loop:

fn main() {
    for _ in 0..10 {                               
        std::thread::spawn(|| {
            println!("I am printing something");   
        });
    }    
}                                                  

Sets up 10 threads

Now the threads start.

How many can finish before main() ends here?

The code goes as follows:

Usually, about four threads will print before main() ends, but it is always different. If your computer is faster, it might not print any. Also, sometimes the threads will panic:

thread 'thread 'I am printing something
thread '<unnamed><unnamed>thread '' panicked at '<unnamed>I am printing
something
' panicked at 'thread '<unnamed>cannot access stdout during shutdown'
panicked at '<unnamed>thread 'cannot access stdout during
shutdown

This error occurs when the thread tries to do something just when the program is shutting down.

You could, of course, give the computer something to do after starting the threads so that the program won’t shut down right away:

fn main() {
    for _ in 0..10 {
        std::thread::spawn(|| {
            println!("I am printing something");
        });
    }
    let mut busy_work = vec![];
    for _ in 0..1_000_000 {      
        busy_work.push(9);
        busy_work.pop();
    }
}

Makes the program push 9 into a Vec and then removes it 1 million times. It has to finish this before it can exit the main function.

That will guarantee that all 10 threads have time to print their messages. But that’s a pretty silly way to give the threads time to finish. The better way is to tell the code to stop until the threads are done. The cool thing here is that the spawn() function actually returns something called a JoinHandle that lets us do exactly this. You can see this in the signature for spawn():

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,

Here are two notes on the signature:

Note In Rust 1.63, in August 2022, Rust got a new type of thread that doesn’t need 'static! We’ll look at that in the next chapter.

11.6.2 Using JoinHandles to wait for threads to finish

Okay, back to the JoinHandle returned by the spawn() function. Let’s make a variable to hold a JoinHandle every time a thread is spawned:

fn main() {
    for _ in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("I am printing something");
        });
    }
}

The variable handle is now a JoinHandle, but we aren’t doing anything with it yet, and the main program is still finishing before the threads have had time to print their messages. To use the JoinHandle to tell the program to wait for the threads to finish, we call a method called .join(). This method means “wait until the thread is done” (it waits for the thread to “join” it). Write handle.join(), and it will wait for each of the threads to finish:

fn main() {
    for _ in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("I am printing something");
        });
        handle.join();    
    }
}

Waits for the threads to finish

So now we won’t leave main() until all 10 threads are done. But, actually, we aren’t using threads exactly the way we want yet. We start a thread, do something, and then call .join() to wait—and only then we start a new thread. What we want instead is for main() to start all the threads at the same time, get them to start working, and only then call .join() on the threads.

To solve this, we can create a Vec that will hold all of the JoinHandles. Then we can call .join() on them once all 10 of the threads are up and running:

fn main() {
    let mut join_handles = vec![];           
    for _ in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("I am printing something");
        });
        join_handles.push(handle);           
    }
    for handle in join_handles {             
        handle.join().unwrap();
    }
}

Here is the Vec that will hold each of the JoinHandles. It stands outside the for loop.

We are pushing each JoinHandle into the Vec and haven’t called them to .join() yet. This will let all 10 threads start working without waiting.

Now that all 10 threads are working, we can finally call .join() on each of the threads to make sure that they are done. Now all 10 threads are working, and main() will not finish until they are all done.

The code successfully prints I am printing something 10 times, showing us that main() is indeed waiting for all 10 threads to finish. Success!

Now, let’s make a small change to the code. Every thread is working on its own, so it would be interesting to print out the thread number instead of just printing I am printing something every time. We could do this by making a variable called num inside the for loop and printing that out. Surprisingly, however, the code doesn’t work:

fn main() {
    let mut join_handles = vec![];
    for num in 0..10 {                                  
        let handle = std::thread::spawn(|| {
            println!("Inside thread number: {num}");    
        });
        join_handles.push(handle);
    }
    for handle in join_handles {
        handle.join().unwrap();
    }
}

We write for num instead of for _, so we can print out the thread number . . .

. . . and print it out here.

The error message is pretty long:

error[E0373]: closure may outlive the current function, but it borrows
`num`, which is owned by the current function
 --> src\main.rs:4:41
  |
4 |         let handle = std::thread::spawn(|| {
  |                                         ^^ may outlive borrowed value
  `num`
5 |             println!("Inside thread number: {num}");
  |                                              --- `num` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src\main.rs:4:22
  |
4 |           let handle = std::thread::spawn(|| {
  |  ______________________^
5 | |             println!("Inside thread number: {num}");
6 | |         });
  | |__________^
help: to force the closure to take ownership of `num` (and any other
referenced variables), use the `move` keyword
  |
4 |         let handle = std::thread::spawn(move || {
  |                                         ++++

Wow. That is quite the error message. But at least the end of the error message gives us some advice: add the move keyword. Indeed, adding this keyword fixes the problem, and we can see the thread numbers in random order:

Inside thread number: 0
Inside thread number: 1
Inside thread number: 4
Inside thread number: 2
Inside thread number: 5
Inside thread number: 6
Inside thread number: 7
Inside thread number: 8
Inside thread number: 9
Inside thread number: 3

But what did this keyword do exactly, and why did we need it? To understand this, we need a small detour to learn about the three types of closures. Understanding the behavior of the three types of closures is a big help in understanding how multiple threads work.

11.6.3 Types of closures

We will take a longer look at closures in the next chapter, but here is a quick introduction. Remember the F: FnOnce() -> T part in the spawn() function? FnOnce is the name of one of the three traits implemented by closures. The following three are the traits implemented by closures:

When a closure captures a variable from its environment, it will try to use Fn if it can. But if it needs to change the value, it will use FnMut, and if it needs to take by value, it will use FnOnce. FnOnce is a good name because it explains what it does: it takes the value once, and then it can’t take it again.

Here is an example:

fn main() {
    let my_string = String::from("I will go into the closure");
    let my_closure = || println!("{my_string}");
    my_closure();
    my_closure();
}

The closure my_closure() doesn’t need to change or take by value, so it implements Fn: it takes a reference. Therefore, the code compiles.

If we change my_string, the closure will implement FnMut.

fn main() {
    let mut my_string = String::from("I will be changed in the closure");
    let mut my_closure = || {
        my_string.push_str(" now");
        println!("{my_string}");
    };
    my_closure();
    my_closure();
}

This prints

I will be changed in the closure now
I will be changed in the closure now now

If you take by value, it implements FnOnce:

fn main() {
    let my_vec: Vec<i32> = vec![8, 9, 10];
    let my_closure = || {
        my_vec.into_iter().for_each(|item| println!("{item}"));    
    };
    my_closure();
    // my_closure();                                               
}

into_iter takes ownership, and my_vec is pulled into my_closure.

This won’t work because the closure, FnOnce, took ownership of my_vec, and my_vec is already gone.

We took by value, so we can’t run my_closure() more than once. That is where the name FnOnce comes from.

We will learn a lot more about these three closure types in the next chapter, but these basics are enough to help us solve our problem with the move keyword.

11.6.4 Using the move keyword

Let’s return to threads. Let’s try to use a value from outside:

fn main() {
    let my_string = String::from("Can I go inside the thread?");
    let handle = std::thread::spawn(|| {
        println!("{my_string}");
    });
    handle.join().unwrap();
}

As before, the compiler says that this won’t work:

error[E0373]: closure may outlive the current function, but it borrows
`my_string`, which is owned by the current function
  --> src\main.rs:28:37
   |
28 |     let handle = std::thread::spawn(|| {
   |                                     ^^ may outlive borrowed value
   `my_string`
29 |         println!("{}", my_string);
   |                        --------- `my_string` is borrowed here
   |
note: function requires argument type to outlive `'static`
  --> src\main.rs:28:18
   |
28 |       let handle = std::thread::spawn(|| {
   |  __________________^
29 | |         println!("{}", my_string);
30 | |     });
   | |______^
help: to force the closure to take ownership of `my_string` (and any other
referenced variables), use the `move` keyword
   |
28 |     let handle = std::thread::spawn(move || {
   |                                     ^^^^^^^

This time, we can understand the message and why move will solve the problem. In this case, the closure wants to use Fn because it only wants to use my_string as a reference to print it. As we saw in the last section, a closure will use Fn if it can because it prefers to only use a reference. However, the spawn() method requires an FnOnce to be used, which means to take by value. The move keyword lets us force the closure to take by value instead of reference, and now that it owns the String, there are no lifetime problems anymore.

Now the code works:

fn main() {
    let mut my_string = String::from("Can I go inside the thread?");
    let handle = std::thread::spawn(move || {
        println!("{my_string}");
    });
    handle.join().unwrap();
}

That was the problem with our former code, too. Let’s look at it again:

fn main() {
    let mut join_handles = vec![];
    for num in 0..10 {
        let handle = std::thread::spawn(|| {
            println!("Inside thread number: {num}");
        });
        join_handles.push(handle);
    }
    for handle in join_handles {
        handle.join().unwrap();
    }
}

You can see that num is being declared outside of the thread, and then the println! statement is trying to borrow num. However, the spawn() method holds an FnOnce closure, which needs to take num by value, not by reference. That’s why the move keyword was needed here, too.

That was a lot of work! You can see that the compiler is watching your back when you use multiple threads to make sure that no data is being borrowed in the wrong way. In the meantime, you now have some tools that give you extra flexibility: Rc for multiple ownership, Cow to take in either owned or borrowed data, and more knowledge of how closures work to help you further understand Rust’s rules on borrowing. Chapter 13 is going to be a relaxing chapter, but we have some hard work yet to do in chapter 12. We will continue to learn to understand threads, closures, and related types.

Summary