并发与异步编程
📚 Rust 课程系列
Rust 的并发安全由类型系统在编译期保证:Send/Sync 标记 trait 让「能否跨线程传递/共享」成为类型的一部分,从而「无畏并发」(fearless concurrency)。本章按主题组织:并发安全基础(Send/Sync)、线程(std::thread、消息通道、scope 线程、线程本地存储)、共享状态与同步原语(Mutex/RwLock/Condvar、Barrier、OnceLock),以及基于 async/.await 的异步编程(Future、运行时、join!/select!)。
并发安全:Send 与 Sync
| trait | 含义 | 自动推导规则 | 反例 |
|---|---|---|---|
Send | T 的所有权可安全跨线程转移 | 所有字段均为 Send | Rc<T>、*const T |
Sync | &T 可安全跨线程共享 | 所有字段均为 Sync(等价于 &T: Send) | Cell<T>、RefCell<T> |
💡 提示:
Send/Sync是标记 trait(marker trait),没有方法,仅作编译器的「通行证」。手动impl Send/impl Sync是 unsafe 的——你在向编译器担保安全性。
🔄 对比:C++ 线程安全靠程序员自觉(
std::mutex配对靠约定),Rust 在编译期拒绝不安全的跨线程访问——Rc不是Send,编译器直接报错,而非运行时崩溃。
线程
线程创建与消息传递
1 | |
💡 提示:
thread::spawn要求闭包'static,因此捕获的局部变量必须move进线程。若需借用栈上数据,见下方 Scope 线程。
💡 提示:
mpsc适合生产者-消费者模式;性能敏感场景可用crossbeam提供更高性能的多生产者通道。
💡 提示:
join返回Result<T, Box<dyn Any + Send>>–子线程 panic 被捕获为Err,不会传染调用线程。这是线程 panic 隔离的体现;panic = "abort"模式下则直接终止进程。
move 闭包与所有权转移
thread::spawn 要求闭包满足 F: Send + 'static。闭包默认按引用捕获变量,而局部变量引用的生命周期受限于当前栈帧——线程可能比调用者活得更久,因此编译器拒绝不带 move 的借用:
1 | |
要点:
move把捕获变量的所有权转移进闭包,闭包随之成为'static,可以安全送往新线程。- 转移后原变量失效;
Copy类型(如i32)不受影响,按位复制即可。 - 多线程共享同一数据时,惯用法是先
Arc::clone再move——克隆的是句柄(引用计数),数据本身仍只有一份:
1 | |
⚠️ 注意:
move只决定闭包的捕获方式,不影响闭包体内的借用规则。若仅需借用栈上数据且能保证线程先于栈帧结束,使用 Scope 线程 可免去move。
🔄 对比:C++ 线程靠 lambda 捕获列表区分
[=]/[&],误用[&]捕获局部变量是悬垂引用的经典来源;Rust 用'static约束在编译期强制你显式move或改用 scope 线程。
线程配置:Builder
1 | |
💡 提示:命名线程配合日志与 panic 信息可快速定位问题;
thread::current().name()返回Option<&str>,thread::current().id()返回ThreadId用于区分工作线程。
⚠️ 注意:
thread::spawn创建失败时 panic;Builder::spawn返回io::Result<JoinHandle>,可优雅处理资源耗尽等错误。默认栈约 2 MiB;工作线程数可参考thread::available_parallelism()返回的可用并行度。
线程让出与阻塞
1 | |
💡 提示:
yield_now仅为「提示」–调度器可能忽略并继续运行当前线程。忙等场景应优先sleep或阻塞原语让出 CPU,而非空转。
park 与 unpark
1 | |
💡 提示:
unpark采用「许可」语义–若目标线程尚未park,许可被保存,下次park立即返回,因而不会丢失唤醒信号;Condvar::notify在无等待者时则信号丢失。
🔄 对比:
park/unpark适合「一对一」唤醒(已知目标Thread句柄);涉及条件判断与多线程协调仍应使用Condvar+Mutex。Java 的LockSupport.park/unpark同为许可模型。
Scope 线程
1 | |
💡 提示:
thread::scope借助作用域保证所有线程在作用域结束时必然join,因此允许借用栈上局部数据,避免把数据强行Arc/move的繁琐。
⚠️ 注意:scope 内借用规则仍然生效——编译器会检查同一作用域内的并发借用是否冲突。上例中两个 spawn 的借用不重叠(一个
&T,一个&mut T),编译器会确保它们不会同时活跃。
线程本地存储(Thread Local Storage)
1 | |
💡 提示:TLS 让每个线程拥有独立副本,无需同步即可修改,常用于缓存、随机数生成器、请求上下文等「每线程一份」的状态。因
thread_local!中的值不是Sync,内部用RefCell而非Mutex。
共享状态与同步原语
Mutex 与 Arc
1 | |
⚠️ 注意:若只是计数,优先使用
AtomicUsize等原子类型,避免锁开销。Mutex适用于保护复杂共享数据。
⚠️ 注意:避免在持锁期间调用外部代码或再加锁——这是死锁的经典来源,尽量缩小锁的持有范围。
RwLock
1 | |
⚠️ 注意:
std::sync::RwLock在写饥饿场景下可能表现不佳;性能敏感场景考虑parking_lot::RwLock(写优先,内存开销更小)。
Condvar
1 | |
💡 提示:
Condvar::wait释放 MutexGuard 并挂起线程,被唤醒后重新获取锁。必须用while循环检查条件(防止虚假唤醒)。
🔄 对比:Condvar 用法与 C++ 的
std::condition_variable、Java 的Object.wait()/notify()几乎一致。
OnceLock 与 OnceCell(Rust 1.70+)
1 | |
| 类型 | 线程安全 | 用途 |
|---|---|---|
OnceLock | 是 | static 声明的全局延迟初始化 |
OnceCell | 否 | 单线程场景的延迟初始化 |
关键 API:get_or_init(首次访问时初始化)、set(尝试设置,返回 Result)、get_or_try_init(支持返回 Result 的初始化)。
💡 提示:
OnceLock取代了过去的lazy_static/once_cell第三方方案,是延迟初始化全局变量的标准做法。
Barrier
1 | |
🔄 对比:Barrier ≈ C++ 的
std::latch/std::barrier(C++20)、Java 的CyclicBarrier。
异步编程
async/.await 基础
1 | |
async fn返回实现Future的匿名类型;.await挂起当前任务,让出线程。- 并发原语:
join!(并发等待多个 Future)、select!(多路复用)、spawn(任务调度)。
💡 提示:Rust 的异步是「协作式」的——
Future只有被驱动(poll)才会推进,.await点是让出执行权的地方。运行时(如 tokio)负责调度任务、在 IO 就绪时唤醒它们。async fn本身不执行任何代码,只有.await或交给运行时spawn后才运行。
🔄 对比:Go 的 goroutine 是抢占式调度(运行时自动切换),Rust 的 async 是协作式。好处是切换开销极低且可预测,代价是须避免在
.await之间执行长时间阻塞操作。
⚠️ 注意:异步块默认单线程调度,CPU 密集任务应使用
spawn_blocking或专用线程池,否则会饿死其他任务。
运行时与生态
- 应用层:使用 tokio 或 async-std 作为运行时。
- 库层:尽量不直接依赖特定运行时以提高可组合性(使用
futures兼容 trait)。 - IO 密集型服务:HTTP、数据库访问、网络爬虫等使用 async 可获得高并发吞吐。
⚠️ 注意:在 async 上下文中调用
std::thread::sleep或std::net::TcpListener::accept等同步阻塞操作会阻塞整个执行线程,导致同线程其他任务全部卡住。务必使用运行时提供的异步版本(如tokio::time::sleep)。
Future trait(进阶)
1 | |
🔬 进阶:
Future::poll是异步的底层机制。返回Poll::Pending时,必须通过cx.waker()安排唤醒,否则任务将永远不会再被 poll。上例省略了 waker 注册,仅作演示——实际手写 Future 必须正确处理 waker。
🔬 进阶:
Pin保证自引用 Future 不会被移动,这是 Rust 异步零成本抽象的关键。编译器生成的 Future 状态机可能包含自引用,Pin在类型层面阻止mem::swap/mem::replace等移动操作。
异步 trait
自 Rust 1.75 起,trait 中直接写 async fn 已稳定:
1 | |
⚠️ 注意:若需把异步 trait 当作
dyn Trait动态分派,原生async fn在 trait 中返回不透明 Future 类型,无法直接Box<dyn Future>。此时仍需借助async-traitcrate(把async fn转为返回Pin<Box<dyn Future>>)。
🔬 进阶:
async fnin trait 的动态分派限制源于 Rust 类型系统——每个async fn返回的匿名 Future 类型大小不同,无法统一放入 trait object 虚表。async-trait通过Box擦除类型绕过,代价是一次堆分配,未来dyn*类型可能原生解决此问题。





















