外观
第 2 节:Send 与 Sync 报错——编译器拦下"不能进线程"的东西
症状
把 Rc 之类的变量搬进线程,编译器当场拦下:
rust
use std::rc::Rc;
fn main() {
let data = Rc::new(String::from("共享数据"));
let handle = std::thread::spawn(move || {
println!("{}", data);
});
handle.join().unwrap();
}编译输出
text
error[E0277]: `Rc<String>` cannot be sent between threads safely
--> src/main.rs:6:37
|
6 | let handle = std::thread::spawn(move || {
| ^ `Rc<String>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:6:37: 6:44}`, the trait `Send` is not implemented for `Rc<String>`诊断
第 15 章的 Send/Sync 合格证在起作用:Rc 的计数器是"单线程"设计的(非原子计数,第 14 章),搬进多线程,两个线程同时改计数器,计数就乱了(数据竞争)。所以 Rc 没有 Send 证——不能送进线程。
两个证的区别:
Send:能搬进别的线程(所有权转移过去)Sync:能共享给别的线程(&T能跨线程用)
| 类型 | Send | Sync | 为什么 |
|---|---|---|---|
Rc<T> | ❌ | ❌ | 计数器非原子 |
Arc<T> | ✅ | ✅ | 计数器原子(第 15 章) |
Mutex<T> | ✅ | ✅ | 内部锁保证安全 |
RefCell<T> | ❌ | ❌ | 借用检查不是原子的 |
*const T 裸指针 | ❌ | ❌ | 编译器啥都不保证 |
编译器不猜,只查证:闭包/类型链上任何一个环节缺证,spawn 就拒绝编译——这就是"线程安全的错误在编译时被拦下"(第 15 章)的现场。
解药
解药一:Rc → Arc(单线程共享 → 多线程共享):
rust
use std::sync::Arc;
let data = Arc::new(String::from("共享数据"));
// 其余代码不用改,spawn 通过!解药二:RefCell → Mutex(单线程"能改" → 多线程"能改"):
rust
// 第 14 章:Rc<RefCell<T>>
// 多线程版:Arc<Mutex<T>>
let counter = Arc::new(Mutex::new(0));解药三:数据真不需要共享?clone 进去:
rust
let data = String::from("数据");
let copy = data.clone(); // 每个线程一份
thread::spawn(move || println!("{}", copy));预防
Rc/RefCell进线程 → 换成Arc/Mutex:口诀"Rc 变 Arc,RefCell 变 Mutex,再加个 Arc 包外面"(Arc<Mutex<T>>,第 14 章收尾)- 裸指针/
Cell进线程:都是编译错误——这是好事,说明你的数据模型需要重新设计 - 给自定义类型"发证":
unsafe impl Send for ...存在,但那是"我自己负责"的签名(第 19 章 unsafe 精神)——先想清楚为什么编译器不给证,再决定是换类型还是冒险发证 - 报错里找"哪一环没证":
within {closure...}告诉你缺证的是谁——顺着报错找类型,通常就是Rc/RefCell/裸指针