Rust 并发
并发(concurrency),也就是同时运行多项工作,是很多语言里真正危险的地方:两个线程同时操作同一份数据,可能会把它弄坏,而且这种 bug 极难追查。借用规则要防止的正是这类问题,而这层保护也直接延伸到了线程上。
use std::thread;
fn main() { let handle = thread::spawn(|| { for i in 1..=3 { println!("线程:{i}"); } });
for i in 1..=3 { println!("主线程:{i}"); }
handle.join().unwrap(); // 等待线程结束}thread::spawn 接收一个闭包,在一个新线程上运行它,和其他代码并行执行。.join() 会一直阻塞,直到那个线程结束。如果不调用它,main 可能在新线程还没来得及运行时就已经退出了。
把数据移动到线程里
Section titled “把数据移动到线程里”传给 thread::spawn 的闭包必须完全拥有它用到的数据,因为它可能在原来的作用域结束之后才运行。加上 move 关键字就能强制做到这一点:
use std::thread;
fn main() { let message = String::from("来自线程的问候");
let handle = thread::spawn(move || { println!("{message}"); });
handle.join().unwrap();}输出:
来自线程的问候如果不加 move,Rust 不会编译这段代码:它没法保证新线程真正运行时 message 仍然有效。move 把 message 的所有权转移到闭包里,这就是你在所有权一页见过的那种“移动”,只不过这次是交给一个线程,而不是另一个变量。
在线程之间安全地共享数据
Section titled “在线程之间安全地共享数据”普通引用没法跨线程使用(它们不能比创建它们的作用域活得更久),所以要在线程之间共享数据,就要把你已经见过的两样东西组合起来:用 Arc(来自智能指针一页)实现共享所有权,再用 Mutex(互斥器)保证同一时间只有一个线程能碰这份数据:
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![];
for _ in 0..5 { 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!("结果:{}", *counter.lock().unwrap());}输出:
结果:5counter.lock() 会一直等到能安全地独占这个值,完成更新后再自动释放它。五个线程给同一个计数器加 1,有 Mutex 在,它们永远不会互相踩脚:你每次都能稳稳地得到 5,而不是因为某次更新丢失而得到一个更小的数。
给共享数据加锁是线程协作的一种方式。另一种常见方式是干脆不共享,而是让线程之间互相发送消息。这就是信道一页要讲的内容。
Rust 为什么称之为“无畏并发”
Section titled “Rust 为什么称之为“无畏并发””在大多数语言里,没有什么能阻止你忘记给 Mutex 加锁,或者不小心把不该共享的引用跨线程共享出去。Rust 的编译器会真正强制执行这些规则:会在线程之间造成数据竞争的代码,干脆就无法编译。这和你在本教程前面学到的所有权与借用系统是同一套东西,只是从单个作用域用到了线程上。
1. 两个线程,一个总和
Section titled “1. 两个线程,一个总和”用两个线程计算 1 到 100 的和:一个线程算 1 到 50 的和,另一个算 51 到 100 的和,然后由 main 把两个结果加起来。提示:线程的闭包可以返回一个值,handle.join().unwrap() 会把这个值交还给你。
查看答案
use std::thread;
fn main() { let first = thread::spawn(|| { let total: u32 = (1..=50).sum(); total }); let second = thread::spawn(|| { let total: u32 = (51..=100).sum(); total });
let a = first.join().unwrap(); let b = second.join().unwrap(); println!("{a} + {b} = {}", a + b);}输出:
1275 + 3775 = 5050这两个闭包没有用到任何外部变量,所以不需要 move。两个线程同时运行,join() 会等待每个线程结束,并交回它的结果。
2. 每个人都在留言簿上签名
Section titled “2. 每个人都在留言簿上签名”启动三个线程。每个线程往同一个共享的 Vec<String> 里添加一行,比如 "线程 2 来过这里"。等所有线程都结束后,按排序后的顺序打印这些行。提示:像计数器的例子那样使用 Arc<Mutex<Vec<String>>>,最后对一份副本排序,因为线程结束的顺序是不确定的。
查看答案
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let book = Arc::new(Mutex::new(Vec::new())); let mut handles = vec![];
for id in 1..=3 { let book = Arc::clone(&book); handles.push(thread::spawn(move || { book.lock().unwrap().push(format!("线程 {id} 来过这里")); })); }
for handle in handles { handle.join().unwrap(); }
let mut lines = book.lock().unwrap().clone(); lines.sort(); for line in lines { println!("{line}"); }}输出:
线程 1 来过这里线程 2 来过这里线程 3 来过这里把 lines.sort() 这一行删掉,多运行几次:顺序可能会变。排序能保证你每次都得到同样的输出。
thread::spawn(closure)在一个新线程上运行闭包;.join()等待它结束。- 传给线程的闭包通常需要
move,以便完全拥有它用到的数据。 - 用
Arc(共享所有权)加上Mutex(独占访问)在线程之间共享数据:counter.lock()让你安全地、一次一个地访问这个值。 - 会造成数据竞争的代码,编译器在编译时就会拒绝,而不是等到运行时。
小测验
1. 为什么传给 thread::spawn 的闭包通常需要 move 关键字?
2. Arc<Mutex<T>> 是用来做什么的?
3. 如果忘了对线程句柄调用 .join(),可能会出什么问题?
4. 为什么同一个程序每次运行时,“主线程:”和“线程:”这两种行的顺序可能不同?
得分: 0 / 4
- 使用线程同时运行代码 — Rust 程序设计语言:
spawn、join和move闭包,附输出交错的示例。 - 共享状态并发 — Rust 程序设计语言:深入讲解
Mutex和Arc,包括为什么Rc不能发送到另一个线程。 std::thread— 标准库文档(英文):关于线程的一切,包括给线程命名和作用域线程。Mutex— 标准库文档(英文):加锁是怎样工作的,以及线程持有锁时 panic 会造成的“中毒”(poisoning)是什么意思。- 线程 — 通过例子学 Rust:一个简短的可运行示例,创建并等待多个线程。