Skip to content

Rust - 并发

并发编程(Concurrent programming),程序的不同部分独立执行;以及并行编程(parallel programming),它们同时执行,变得越来越重要。Rust 的所有权和类型系统提供了强大的编译时保证,以防止常见并发 bug,如数据竞争(data races)。这使得编写安全的并发代码显著更容易。

操作系统管理进程,而在一个进程内部,你可以拥有独立的部分,称为线程(threads),它们可以同时运行。你可以使用 thread::spawn 函数创建一个新线程,该函数接受一个包含要运行代码的闭包。

当程序的主线程退出时,新线程将停止,无论它是否完成了工作。为了防止这种情况,thread::spawn 返回一个 JoinHandle。对此 handle 调用 join() 方法将阻塞当前线程,直到 spawned 线程完成。

use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("Hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..=3 {
println!("Hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
// 阻塞主线程,直到 spawned 线程完成。
// unwrap() 会在子线程 panic 时导致 panic。
handle.join().unwrap();
println!("Main thread exiting after spawned thread has finished.");
}
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 spawned thread!
Hi number 5 from the spawned thread!
Main thread exiting after spawned thread has finished.

通常,你会在 spawned 线程中使用来自主线程的数据。为此,你必须使用 move 闭包来强制闭包获取其所用值的所有权。这是一种安全措施,旨在防止主线程在使用 spawned 线程仍在使用数据时使其失效。

use std::thread;
fn main() {
let v = vec![1, 2, 3];
// 'move' 关键字将 'v' 的所有权转移给新线程。
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
// 'v' 已经被移动,因此我们不能再在主线程中使用它。
// drop(v); // 这会导致编译时错误。
handle.join().unwrap();
}

线程之间安全有效的通信方式是通过通道(channels)。这遵循了“不要通过共享内存来通信;相反,通过通信来共享内存”的原则。一个通道有一个发送端 (tx) 和一个接收端 (rx)。

use std::sync::mpsc; // mpsc 代表多发送者,单接收者
use std::thread;
fn main() {
// 创建一个新通道
let (tx, rx) = mpsc::channel();
// 创建一个线程来发送消息
thread::spawn(move || {
let val = String::from("hi");
println!("Thread sending: {}", val);
tx.send(val).unwrap();
// 'val' 现在由通道拥有,不能再在此处使用。
});
// 主线程接收消息。recv() 会阻塞,直到有消息可用。
let received = rx.recv().unwrap();
println!("Main got: {}", received);
}
Thread sending: hi
Main got: hi

3. 使用互斥锁(Mutex)的共享状态并发

Section titled “3. 使用互斥锁(Mutex)的共享状态并发”

对于必须共享内存的情况,Rust 提供了并发原语,例如 Mutex<T>(互斥锁)。Mutex 允许在任何给定时间只有一个线程访问某些数据。为了在多个线程间共享 Mutex 的所有权,我们将其包装在一个 Arc<T>(原子引用计数)中,它是 Rc<T> 的线程安全版本。

use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
// Arc<T> 提供对堆上分配的类型 T 值的共享所有权。
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 || {
// lock() 获取互斥锁,阻塞当前线程直到可用。
// 当 lock() 返回的 MutexGuard 超出作用域时,锁会被释放。
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

使用 async/await 实现现代异步 Rust

Section titled “使用 async/await 实现现代异步 Rust”

对于 I/O 密集型任务(如网络请求或文件系统操作),Rust 通过 async 和 await 语法对异步编程提供了第一类支持。async 操作不会阻塞线程,而是会交出控制权,允许其他任务运行。这使得单个线程能够高效地处理许多并发操作。

要运行 async 代码,你需要一个异步运行时(async runtime),例如 tokio 或 async-std。以下是一个概念性示例:

// 此示例需要将 `tokio` crate 添加到 Cargo.toml 中
// [dependencies]
// tokio = { version = "1", features = ["full"] }
async fn say_hello() {
println!("Hello");
}
#[tokio::main]
async fn main() {
// 调用 async 函数会返回一个 Future。
let future = say_hello();
// `.await` 关键字会暂停执行,直到 Future 完成。
future.await;
println!("Async function finished.");
}

关键要点: Rust 的 Send 和 Sync trait 是标记 trait,编译器使用它们在编译时强制执行线程安全,从而防止了一整类并发错误。