RustBrock/Simultaneous Code Running.md
darkicewolf50 54cc434db5
Some checks failed
Test Gitea Actions / first (push) Successful in 13s
Test Gitea Actions / check-code (push) Failing after 15s
Test Gitea Actions / test (push) Has been skipped
Test Gitea Actions / documentation-check (push) Has been skipped
finished ch16.1
2025-03-11 15:07:15 -06:00

13 KiB

Using Threads to Run Code Simultaneously

In most modern operating systems, an executed program's program's code is run in a process, and the operating system will manage multiple process at once.

Within a progam you can also have independent components that run simultaneously.

The features that run these independent parts are called threads.

An example of this is a we server could have multiple threads so that it could respond to more than one request at the same time.

Splitting this computation in yor program into multiple threads to run multiple tasks at the same time can improve performance.

This comes with the drawback of added complexity.

Due to threads being able to run simultaneously, there is no ingerent guarantee about the order in which parts of your code on different threads will run.

This can lead to problems like:

  • Race conditions - where threads are accessing data or resources in an inconsistent order
  • Deadlock - where two threads are waiting for each other, preventing both threads from continuing
  • Bugs that happen only in specific cases and are hard to reproduce and fix reliably

Rust attempts to minimize the negative effects of using threads, but programming in a mutlithreaded context still takes a lot of careful thought and reuires a code structure that is different from that in programs running in a single threaded manner.

Rrogramming languages often implement threads in a few different ways, and amny operating systmes provide an API the language can call for creating new threads

The Rust std library uses a 1:1 model of thread implementation, whereby a program uses one operating system thread per on language thread.

There are crates that implement other models of threading that make different tradeoffs to the 1:1 model

(Rust's aync system provides another approach to concurrency as well, we will see this in th nex section.)

Creating a New Thread with spawn

To create a new thread, we use the thread::spawn function and pass it a closure (this was discussed previously here).

This contains the code we want to run in the new thread.

Here is an example where this code prints some text from a main thread and other text from a new thread

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

Note that when the main thread of Rust program completes, all spawned threads are shut down, whether or not they have finished running.

The output form this program might be a little different every time.

Overall it will look similar to this output

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 call to thread::sleep force a thread to stop its execution for a short duration.

This allws for a different thread to run.

The threads will probably take turns, this is not guaranteed.

This is entirely dependant on how your operating system schedules the threads.

Here in this run, the main thread is printed first, even though the print statement from the spawned thread appears first.

And even though we told the spawned thread to print unti i is 9, it only got to 5 before the main thread shut down.

If you run this code and only see the output form the main thread, or dont see any overlap. You can try increasing the numbers in the ranges to create mroe opportunities for the operating system to switch between the threads.

Waiting for All Threads to Finishe Using join Handles

THe code from the example before not only stops the spwaned thread prematurely most of the time due to the main thread ending.

There is no guarantee on the order in which the threads run, we also can't guarantee that the spawned thread will get to run at all.

We can fix the problem of the spawned thread not running o ending prematurely by saving value of thread::spawn in a variable.

The return type of thread::spawn is JoinHandle.

A JoinHandle is an owned value that, wqhen we call the join method on it, will wait for its thread to finish.

This next example shows how to use the JoinHandle of the thread we created in and call join to make sure the spawned thread finishes before main exits:

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

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

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

Blocking a thread means that the thread is prevented form performing work or exiting.

Due to us putting the join call after the main thread's for loop.

Running this now the output should look 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 will alternate btween the two, but the main thread waits because of the call to handle.join().

It does not end until the spawned thread is finished.

Lets see what happens when we move handle.join() to before the for loop in main

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

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}

Now the main thread will wait for the spawned thread to finish and then run its for loop.

Now the output will not be interleaved anymore, like this

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!

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

We need .unwrap to consume the closures otherwise it will not run.

Using move Closures with Threads

We often use the move keyword with closures passed to thread::spawn.

This is due the closure taking ownership of the values it uses from the environment, and ths transferring ownership of those values form one thread to another.

Remember the Capturing References or Moving Ownership section, where we discussed move in the context of clousres.

Here we will focus on the interaction between move and thread::spawn.

Note that in the first example that the closure we pass to thread::spawn takes no args.

There we are not using any data from the main thread in the spawned thread's code.

To use data from the main thread within the spawned thread, we need the spawned thread's closure to capture the values it needs.

In this example shows an attempt to create a vector in the main thread a nd use it in the spawned thread.

This will not comile, it will be exampled afterwards.

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}

This closures uses v, so it will capture v and make it part of the closures environment.

Due to thread::spawn runs this closure in a new thread.

Here we should be able to access v inside that new thread.

But when we compile this we get this compiler error

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

Rust here will infer how to capture v and because println! only needs a reference to v, the closure tires to borrow v.

Here the problem is that Rust can't tell how long the spawned thread will run so it doesn't know if the reference to v will always be valid.

This next example provides a scenario that is more likely to habve a referncfe to v that will not be valid

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

If Rust allwed this code to run, there is a possibility that the spawned thread would be immediately put in the backgrounf without running at all.

The spawned reference to v inside, but the main thread immediately drops v usng the drop function.

Then when the spawned thread starts to execute, v is no longer valid, so a reference to it is also invalid.

Here is what the compiler suggests to fix this, it is located in the compiler error message

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

By adding the move keyword before the closure. This forces the closure to take ownership of the values it is using rather than allowing Rust to unfer that it should borrow the values.

Here is the modification to the bfore exampl that will compile and run as intended

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

We may be tempted to try the sam ething to dix the code in the previous example where the main thread called drop by using a move closure.

This fix will not work because od what the example is attempting to do is disallowed for a different reason.

If we were to add move to the closure, we would move v into the closure's environment and we could no longer call drop on it in the main thread.

We would get this compiler error instead

$ 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

For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error

The Rust ownership rules save us again.

The we get this error becuase Rust was being conservative and only borrowing v for the thread.

This means that the thread could theoretically invaildate, we are guaranteeing Rust that the main thread won't use v anymore.

If we change the example form before in the same way then we would voilate the ownership rules rules when we try to use v in the main thread.

The move keyword overrides Rust's conservative default of borrowing; it doesnt let us violate the ownership rules.

Now with a basic understanding of threads and the thread API, we will look at what we can do with threads

Next Section Here