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