Rust’s standard library provides support for native threads with its std::thread
module, allowing you to run code in parallel. When programming in Rust, you have a high degree of control over threads, but this comes with an added layer of complexity.
Here’s an example of how to create a new thread in Rust:
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
In this example, we use the thread::spawn
function to create a new thread. The code to be run in the new thread is placed inside a closure.
use std::thread;
use std::time::Duration;
fn main() {
/*
创建线程 std::thread::spawn();
*/
// Case1: 主线程结束,即使子线程没有执行完毕也要结束
thread::spawn(|| {
for i in 1..10 {
println!("子线程{}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("主线程{}", i);
thread::sleep(Duration::from_millis(1));
}
// thread::sleep()会让线程睡眠一段时间,某个线程睡眠的时候,让出CPU,
// 可以让不同的线程交替执行,这要看操作系统如何调度线程
// Case2:使用Join方法,等待子线程全部执行完毕再结束
let handler = thread::spawn(|| {
for i in 1..10 {
println!("子线程{}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("主线程{}", i);
thread::sleep(Duration::from_millis(1));
}
handler.join().unwrap();
}
One major aspect of multi-threading in Rust is communication between threads. Rust provides several ways for threads to communicate and synchronize with each other, such as:
Channels: Channels are a way of sending data between threads. They are provided by the std::sync::mpsc
module (the mpsc
stands for “multiple producer, single consumer”).
Mutexes: Mutexes allow us to control access to shared data by ensuring that only one thread can access the data at any one time.
Arcs: The Arc
type, which stands for “atomic reference count”, is a type of smart pointer that allows multiple threads to safely share ownership of some data.
Finally, Rust’s ownership system extends to its handling of threads, ensuring that data races cannot occur. Rust enforces these rules at compile time, which eliminates a whole class of potential bugs related to thread safety.
Channels in Rust allow two or more threads to communicate with each other by transmitting a certain type of data from one thread to another. Rust provides the std::sync::mpsc
module for this purpose. Here’s an example:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hello");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received); // Got: hello
}
In this example, we’re creating a new channel with mpsc::channel()
, then moving the transmitter end into a new thread and sending the string “hello” down the channel. The receiver end stays in the main thread and waits to receive the message.
Mutexes provide a mechanism to protect shared data. Here’s an example:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); // Result: 10
}
In this example, we’re creating a counter inside a Mutex
to protect access to this shared state. We’re then creating ten threads, each of which will get a lock on the Mutex
and increment the counter. Let’s analyze this program in detail:
Arc::new(Mutex::new(0))
creates a new atomic reference count (Arc) around a mutex that guards an integer (i32
). The Arc
is used so that the counter can be shared safely among multiple threads.
A for loop is used to spawn 10 threads. For each iteration:
A clone of the Arc
containing the counter is created. Cloning an Arc
increases the reference count.
The cloned Arc
is moved into the new thread.
The thread acquires a lock to the mutex (with counter.lock().unwrap()
), which returns a mutable reference to the value inside the mutex (i.e., the counter).
The thread then increments the value the mutable reference points to (*num += 1
), which is the value of the counter.
The thread then ends, the lock is released, and the Arc
’s reference count is decreased.
handles.push(handle);
is used to store the join handle for each thread in a vector, so that we can later wait for all threads to finish execution.
After the threads have been spawned, the main thread waits for all threads to finish by calling handle.join().unwrap();
for each handle in the vector.
Finally, the main thread acquires a lock on the counter one more time to print the final value. Because each thread incremented the counter exactly once, and we waited for all threads to finish before printing, the output is “Result: 10”.
So the counter ends up being 10 because the number was incremented once in each of the 10 threads.
Arc stands for Atomic Reference Counting. It’s a thread-safe reference-counting pointer, used when you need to share data between multiple threads. In the previous Mutex example, we used an Arc
to share the Mutex between multiple threads.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
for _ in 0..3 {
let data = Arc::clone(&data);
thread::spawn(move || {
let mut data = data.lock().unwrap();
data[0] += 1;
});
}
}
In this example, we’re sharing a Mutex
among multiple threads. Without the Arc
, Rust’s ownership rules would prevent us from moving the Mutex
into more than one thread.