并发与异步编程
📚 Rust 课程系列
Rust 的并发安全由类型系统在编译期保证:Send/Sync 标记 trait 让「能否跨线程传递/共享」成为类型的一部分,从而「无畏并发」(fearless concurrency)。本章覆盖基于线程的并发(std::thread、通道、Mutex/RwLock/Condvar、线程本地存储、scope 线程)与基于 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提供更高性能的多生产者通道。
共享状态: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()几乎一致——同样的 while 循环防虚假唤醒模式。
线程本地存储与同步原语
线程本地存储(Thread Local Storage)
1 | |
💡 提示:TLS 让每个线程拥有独立副本,无需同步即可修改。常用于缓存、随机数生成器、请求上下文等「每线程一份」的状态。因为
thread_local!中的值不是Sync的,所以内部用RefCell而非Mutex。
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。
Scope 线程
1 | |
💡 提示:普通
thread::spawn要求闭包'static(不能借用局部变量);thread::scope借助作用域保证所有线程在作用域结束时必然join,因此允许借用栈上的局部数据,避免了把数据强行Arc/move的繁琐。
⚠️ 注意:scope 内的借用规则仍然生效——编译器会检查同一作用域内的并发借用是否冲突。上例中两个 spawn 的借用不重叠(一个
&T,一个&mut T),编译器会确保它们不会同时活跃。
异步编程
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点让出)。好处是切换开销极低且可预测,代价是必须避免在.await之间执行长时间阻塞操作。
⚠️ 注意:异步块默认单线程调度,CPU 密集任务应使用
spawn_blocking或专用线程池,否则会饿死其他任务。
运行时与生态
- 应用层:使用 tokio 或 async-std 作为运行时。
- 库层:尽量不直接依赖特定运行时以提高可组合性(使用
futures兼容 trait)。 - IO 密集型服务:HTTP、数据库访问、网络爬虫等使用 async 可获得高并发吞吐。
- 阻塞操作:避免在 async 任务中直接执行阻塞 IO 或 CPU 密集任务,使用
spawn_blocking或单独线程池。
⚠️ 注意:在 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*类型可能原生解决此问题。

