" fearless concurrency"------无畏并发,是 Rust 官方文档中对并发能力的描述。在 C/C++ 中,写并发代码需要极度谨慎,一个疏忽就可能导致数据竞争、死锁或难以复现的 bug。Rust 通过类型系统在编译期消灭了数据竞争,让并发编程变得"无畏"。本文从线程的创建与生命周期讲起,系统讲解消息传递(Channel)和共享状态(Arc<Mutex>)两种并发范式,深入剖析 Send 和 Sync trait 如何保证编译期并发安全,并通过一个并发 Web 爬虫实战帮你掌握 Rust 并发编程的核心技能。
一、线程基础
1.1 创建线程
Rust 使用 std::thread::spawn 创建线程,参数是一个闭包:
rust
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("子线程: {}", i);
thread::sleep(Duration::from_millis(500));
}
});
for i in 1..=3 {
println!("主线程: {}", i);
thread::sleep(Duration::from_millis(800));
}
// 等待子线程结束
handle.join().unwrap();
println!("所有线程结束");
}
关键点:
spawn 返回 JoinHandle,通过 join() 等待线程结束。
join() 返回 Result<T, Box<dyn Any + Send>>,线程 panic 时返回 Err。
主线程结束时,所有子线程被强制终止------join 确保子线程执行完毕。
1.2 move 闭包:转移所有权到线程
线程的生命周期可能超过创建它的作用域,因此闭包必须获取数据的所有权:
rust
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// ❌ 编译错误:闭包可能比 data 活得更久
// let handle = thread::spawn(|| {
// println!("{:?}", data);
// });
// ✅ 使用 move 转移所有权
let handle = thread::spawn(move || {
println!("线程中的数据: {:?}", data);
});
// data 已被移动,不再可用
handle.join().unwrap();
}
move 的本质:将闭包捕获的变量所有权转移到闭包中,从而转移到线程中。
1.3 线程 panic 处理
rust
use std::thread;
fn main() {
let handle = thread::spawn(|| {
panic!("子线程崩溃了!");
});
match handle.join() {
Ok(_) => println!("线程正常结束"),
Err(e) => {
let msg = e.downcast_ref::<&str>().unwrap_or(&"未知错误");
println!("线程 panic: {}", msg);
}
}
}
二、消息传递:Channel
Rust 的并发哲学之一是:不要通过共享内存来通信,而要通过通信来共享内存。Channel 是实现这一理念的核心工具。
2.1 创建 Channel
rust
use std::sync::mpsc;
use std::thread;
fn main() {
// mpsc = Multiple Producer, Single Consumer
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(String::from("hello")).unwrap();
});
let received = rx.recv().unwrap();
println!("收到: {}", received);
}
mpsc 的含义:多个生产者(Sender),单个消费者(Receiver)。
2.2 发送多个消息
rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let messages = vec!["消息1", "消息2", "消息3"];
for msg in messages {
tx.send(msg).unwrap();
thread::sleep(Duration::from_millis(500));
}
});
// 遍历接收所有消息
for received in rx {
println!("收到: {}", received);
}
// 当 tx 被 drop 后,循环自动结束
}
for received in rx:当所有 Sender 都被 drop 后,Receiver 会收到 Err,循环自动结束。
2.3 多个生产者
rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
for i in 0..3 {
let tx = tx.clone(); // 克隆 Sender
thread::spawn(move || {
tx.send(format!("来自线程 {}", i)).unwrap();
});
}
drop(tx); // 释放原始 Sender
for received in rx {
println!("收到: {}", received);
}
}
⚠️ 注意:必须 drop 原始 tx,否则 Receiver 会一直等待。
2.4 同步 Channel:sync_channel
rust
use std::sync::mpsc;
use std::thread;
fn main() {
// 有界缓冲区大小为 2
let (tx, rx) = mpsc::sync_channel(2);
thread::spawn(move || {
for i in 1..=5 {
println!("发送: {}", i);
tx.send(i).unwrap(); // 缓冲区满时阻塞
println!("已发送: {}", i);
}
});
thread::sleep(std::time::Duration::from_millis(100));
for received in rx {
println!("收到: {}", received);
thread::sleep(std::time::Duration::from_millis(500));
}
}
sync_channel 的价值:提供背压(Backpressure) ------当消费者处理不过来时,生产者会被阻塞,避免内存无限增长。
三、共享状态:Arc<Mutex>
3.1 为什么需要 Mutex?
多个线程共享数据时,需要确保同一时刻只有一个线程能修改数据:
rust
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!("最终计数: {}", *counter.lock().unwrap()); // 10
}
Arc<Mutex> 的解读:
Arc 让多个线程共享所有权。
Mutex 确保同一时刻只有一个线程能访问数据。
3.2 Mutex 的锁守卫
lock() 返回 MutexGuard,它实现了 Deref 和 Drop:
rust
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
} // num 离开作用域,锁自动释放
println!("m = {:?}", m);
}
锁守卫的生命周期就是锁的持有时间------离开作用域时自动解锁。这避免了忘记解锁导致的死锁。
3.3 避免死锁
死锁发生在两个线程互相等待对方持有的锁:
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let a = Arc::new(Mutex::new(1));
let b = Arc::new(Mutex::new(2));
let a1 = Arc::clone(&a);
let b1 = Arc::clone(&b);
let a2 = Arc::clone(&a);
let b2 = Arc::clone(&b);
let t1 = thread::spawn(move || {
let _lock_a = a1.lock().unwrap();
thread::sleep(std::time::Duration::from_millis(10));
let _lock_b = b1.lock().unwrap(); // 等待 b
println!("线程1 完成");
});
let t2 = thread::spawn(move || {
let _lock_b = b2.lock().unwrap();
thread::sleep(std::time::Duration::from_millis(10));
let _lock_a = a2.lock().unwrap(); // 等待 a
println!("线程2 完成");
});
t1.join().unwrap();
t2.join().unwrap();
}
避免死锁的策略:
按固定的顺序获取锁(如总是先锁 a 再锁 b)。
使用 try_lock 避免无限等待。
尽量减少锁的持有时间。
四、Send 与 Sync:编译期并发安全
Rust 的并发安全基石是 Send 和 Sync 两个 marker trait:

4.1 Send 和 Sync 的自动推导
绝大多数类型自动实现 Send 和 Sync。只有少数类型例外:
rust
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// ❌ Rc 不是 Send,不能跨线程转移
let rc = Rc::new(5);
// thread::spawn(move || { println!("{}", rc); });
// ❌ RefCell 不是 Sync,不能被多个线程引用
let refcell = RefCell::new(5);
// thread::spawn(move || { println!("{}", refcell.borrow()); });
}
为什么 Rc 不是 Send? 因为它的引用计数不是原子操作。如果两个线程同时克隆 Rc,可能导致计数错误,最终双重释放。
为什么 RefCell 不是 Sync? 因为它使用非原子的借用计数。两个线程同时 borrow_mut() 会导致数据竞争。
4.2 手动实现 Send 和 Sync
只有当你确定类型是安全的,才能手动实现:
rust
struct MyType {
data: *mut u8, // 裸指针默认不是 Send/Sync
}
// 你承诺:MyType 可以安全地在线程间转移
unsafe impl Send for MyType {}
unsafe impl Sync for MyType {}
⚠️ 警告:手动实现 Send 和 Sync 是 unsafe 操作,必须确保类型确实安全。错误实现会导致未定义行为。
五、线程池与 Rayon:更高层的并发抽象
5.1 标准库没有线程池
Rust 标准库没有内置线程池。实践中常用 rayon 或 threadpool crate:
toml
dependencies
rayon = "1.10"
rust
use rayon::prelude::*;
fn main() {
let numbers: Vec<i64> = (1..=1_000_000).collect();
// 并行求和
let sum: i64 = numbers.par_iter().sum();
println!("并行求和: {}", sum);
// 并行过滤 + 映射
let evens: Vec<i64> = numbers.par_iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect();
println!("偶数个数: {}", evens.len());
}
Rayon 的价值:只需将 iter() 改为 par_iter(),就能把迭代器链并行化。Rayon 自动管理工作线程池和任务调度。
5.2 线程池实现原理
rust
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
type Job = Box<dyn FnOnce() + Send + 'static>;
struct ThreadPool {
workers: Vec<thread::JoinHandle<()>>,
sender: mpsc::Sender<Job>,
}
impl ThreadPool {
fn new(size: usize) -> Self {
let (sender, receiver) = mpsc::channel::<Job>();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for _ in 0..size {
let receiver = Arc::clone(&receiver);
workers.push(thread::spawn(move || loop {
let job = receiver.lock().unwrap().recv();
match job {
Ok(job) => job(),
Err(_) => break, // Channel 关闭,退出
}
}));
}
ThreadPool { workers, sender }
}
fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
self.sender.send(Box::new(f)).unwrap();
}
}
六、实战:并发 Web 爬虫
综合线程、Channel 和共享状态,实现一个并发 Web 爬虫:
rust
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
#[derive(Debug, Clone)]
struct Page {
url: String,
title: String,
links: Vec<String>,
}
/// 模拟抓取页面
fn fetch_page(url: &str) -> Page {
thread::sleep(Duration::from_millis(100)); // 模拟网络延迟
Page {
url: url.to_string(),
title: format!("页面 {}", url),
links: vec![
format!("{}/link1", url),
format!("{}/link2", url),
],
}
}
fn main() {
let seed_urls = vec![
"https://example.com".to_string(),
"https://rust-lang.org".to_string(),
"https://crates.io".to_string(),
];
// 已访问 URL 集合(共享状态)
let visited = Arc::new(Mutex::new(HashSet::new()));
// 任务队列(Channel)
let (tx, rx) = mpsc::channel::<String>();
// 结果收集
let (result_tx, result_rx) = mpsc::channel::<Page>();
let rx = Arc::new(Mutex::new(rx));
// 初始任务入队
for url in seed_urls {
tx.send(url).unwrap();
}
// 启动 4 个工作线程
let mut workers = vec![];
for id in 0..4 {
let rx = Arc::clone(&rx);
let tx = tx.clone();
let visited = Arc::clone(&visited);
let result_tx = result_tx.clone();
workers.push(thread::spawn(move || {
loop {
// 获取任务
let url = {
let rx = rx.lock().unwrap();
match rx.try_recv() {
Ok(url) => url,
Err(_) => break, // 没有更多任务
}
};
// 检查是否已访问
{
let mut visited = visited.lock().unwrap();
if visited.contains(&url) {
continue;
}
visited.insert(url.clone());
}
// 抓取页面
println!("[工作线程 {}] 抓取: {}", id, url);
let page = fetch_page(&url);
// 发送结果
result_tx.send(page.clone()).unwrap();
// 新链接入队
for link in &page.links {
let _ = tx.send(link.clone());
}
}
}));
}
// 关闭主发送者
drop(tx);
drop(result_tx);
// 等待所有工作线程结束
for worker in workers {
worker.join().unwrap();
}
// 统计结果
let mut total_pages = 0;
let mut total_links = 0;
for page in result_rx {
total_pages += 1;
total_links += page.links.len();
}
println!("\n=== 爬取统计 ===");
println!("总页面数: {}", total_pages);
println!("总链接数: {}", total_links);
println!("已访问 URL 数: {}", visited.lock().unwrap().len());
}
核心设计:
Arc<Mutex>:共享已访问 URL 集合,避免重复抓取。
Arc<Mutex>:多个工作线程共享任务队列。
mpsc::channel:任务分发和结果收集。
drop(tx):关闭 Channel,通知所有工作线程退出。
七、小结
线程创建:thread::spawn + move 闭包,join 等待结束。
消息传递:mpsc::channel 实现多生产者单消费者模型,sync_channel 提供背压。
共享状态:Arc<Mutex> 是跨线程共享可变数据的经典模式。
Send 和 Sync :编译期并发安全的基石,Rc 和 RefCell 不满足。
死锁避免:固定加锁顺序、try_lock、缩短锁持有时间。
Rayon:par_iter() 一行代码实现数据并行。
实战模式:工作线程 + 任务队列 + 共享状态,是并发任务的通用架构。