共享状态与同步
Mutex/RwLock/Arc共享数据,原子类型与内存序,Send/Sync的原理。
共享状态:Mutex、RwLock 与 Arc
消息传递不是万能的:有些状态天然需要多方共享(配置、缓存、连接池)。Rust 的做法是用类型把"共享"这件事显式表达出来。
Mutex<T>:锁保护的是数据
互斥锁(mutex,mutual exclusion) 在 Rust 里的签名非常有特色:
rust
pub fn lock(&self) -> LockResult<MutexGuard<'_, T>>注意两点:lock 接收 &self(不需要可变引用,内部可变性),返回的是守卫(guard)而不是数据本身。你只能通过守卫访问数据:
rust
use std::sync::Mutex;
fn main() {
let counter = Mutex::new(0);
{
// lock 返回 Result:出错只可能是中毒(poisoning),见「毒化(poisoning)与 `unwrap` 策略」
let mut guard: std::sync::MutexGuard<'_, i32> = counter.lock().unwrap();
*guard += 1; // 通过 DerefMut 修改内部数据
println!("临界区内: {}", *guard);
} // guard 在这里 drop,锁自动释放(RAII)
// 锁已释放,可以再次获取
println!("最终: {}", *counter.lock().unwrap());
// into_inner 拿走内部数据,消费掉 Mutex
let v = counter.into_inner().unwrap();
println!("into_inner: {v}");
}text
输出:
临界区内: 1
最终: 1
into_inner: 1🧠 原理:Rust 的
Mutex<T>保护的是"数据",Java 的synchronized保护的是"代码块"。 在 Java 里, 锁与它保护的数据没有类型上的关联,你完全可以在没加锁的情况下读同一个字段——编译器不会拦你。在 Rust 里,数据被物理地装在Mutex里面, 不lock()就拿不到&mut T,所以"忘了加锁"这件事在类型层面不可能发生。这是两种语言在并发安全上最本质的差异。
💡 对照:Java 的
synchronized (obj) { ... }/ReentrantLock+try/finally unlock; Python 的with threading.Lock():。Rust 的守卫等价于 Python 的with块 + Java 的try/finally, 不同的是 Rust 用 RAII 保证万无一失(包括 panic 展开路径),而且锁和数据绑定在一起。
MutexGuard 的能力与"保护范围"边界
MutexGuard<'a, T> 通过 Deref/DerefMut 提供对 T 的访问,并在 Drop 时解锁。它的 auto trait 情况很值得记:
| 类型 | Send | Sync | 原因 |
|---|---|---|---|
MutexGuard<'_, T> | ❌ | ✅(当 T: Sync) | 不能在别的线程解锁(平台相关的解锁语义);但可以在多个线程间共享引用去读 |
Mutex<T> | ✅(当 T: Send) | ✅(当 T: Send) | 锁本身可以跨线程搬移与共享 |
所以把守卫送进另一个线程会直接编译失败:
rust
use std::sync::{Arc, Mutex};
fn main() {
let m = Arc::new(Mutex::new(0));
let m2 = Arc::clone(&m);
std::thread::spawn(move || {
let guard = m2.lock().unwrap();
std::thread::spawn(move || *guard); // 编译错误:MutexGuard 不是 Send
})
.join()
.unwrap();
}text
error[E0277]: `std::sync::MutexGuard<'_, i32>` cannot be sent between threads safely
= help: within `{closure@...}`, the trait `Send` is not implemented
for `std::sync::MutexGuard<'_, i32>`
note: required by a bound in `spawn`真正需要警惕的是不变式(invariant)跨出了锁的保护。看这个反例:
rust
use std::sync::{Arc, Mutex};
#[derive(Debug)]
struct Account {
balance: Mutex<i64>, // 锁只保护余额
history: Vec<i64>, // 但 history 没有锁保护!
}
fn main() {
let acc = Arc::new(Account { balance: Mutex::new(100), history: vec![] });
let _ = acc;
println!("balance 与 history 必须一致,但类型系统并不知道这个约束");
}balance 与 history 必须同步更新,但只有 balance 在锁里。两个线程可以各自 lock() 修改余额、 各自 push 历史——没有数据竞争(Vec 的并发写会编译失败,或改用 Mutex<Vec<_>>),但如果把它们放进不同的锁,就可能出现"余额加了但历史没记"的窗口。 结论:把构成同一个不变式的所有字段放进同一把锁。
Arc<Mutex<T>>:完整的并发计数器
Arc<T>(atomically reference counted,原子引用计数)是〈智能指针与闭包〉一章的多线程版 Rc。 Arc<Mutex<T>> 是 Rust 里最常见的共享可变状态组合。惯例是始终写 Arc::clone(&x) 而不是 x.clone(), 这样一眼就能看出"这是在增加引用计数,不是深拷贝"。
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0usize));
let threads = 8;
let per_thread = 1000;
let handles: Vec<_> = (0..threads)
.map(|_| {
// 惯例写法:Arc::clone(&counter),语义明确
let counter = Arc::clone(&counter);
thread::spawn(move || {
for _ in 0..per_thread {
// 缩小临界区:拿到锁、加一、立刻释放
let mut guard = counter.lock().unwrap();
*guard += 1;
} // guard 在此 drop
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let total = *counter.lock().unwrap();
assert_eq!(total, threads * per_thread);
println!("total = {total}(期望 {})", threads * per_thread);
}text
输出:total = 8000(期望 8000)这段程序有一个可以优化的点值得说明:每个 += 1 都要加锁解锁一次,锁竞争非常激烈。两种改进:
- 每个线程在本地累加,最后只加锁一次("per-thread 累加 + 一次归并"),这在有大量计数时快得多;
- 如果只是一个计数器,直接用
AtomicUsize(「原子类型与内存序」一节),完全不需要锁。
毒化(poisoning)与 unwrap 策略
如果某个线程在持有锁的时候 panic,Mutex 会被标记为中毒(poisoned)。 之后所有 lock() 都返回 Err(PoisonError<MutexGuard<'_, T>>),理由是:panic 可能发生在不变式被破坏到一半的时刻,此时的数据未必自洽。
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let d = Arc::clone(&data);
let _ = thread::spawn(move || {
let mut g = d.lock().unwrap();
g.push(4);
panic!("在持锁时 panic,锁被毒化");
})
.join();
let err = data.lock().unwrap_err(); // Err(PoisonError)
println!("中毒了: {err}");
// 方式一:明确表示"我知道数据可能不一致,但我确认它是好的"
let recovered = err.into_inner();
println!("into_inner 后: {:?}", *recovered);
// 方式二(更常见):按项目策略处理中毒
match data.lock() {
Ok(g) => println!("正常: {:?}", *g),
Err(poisoned) => {
// 注意:即使中毒,get_ref/get_mut 仍然能拿到数据
let g = poisoned.into_inner();
eprintln!("锁中毒,已恢复使用: {:?}", *g);
}
}
}text
输出(含 panic 消息):
thread '<unnamed>' panicked at src/main.rs:10:9:
在持锁时 panic,锁被毒化
中毒了: poisoned lock: another task failed inside
into_inner 后: [1, 2, 3, 4]
正常: [1, 2, 3, 4] ← 这里因为同一线程再次 match 会拿到 Ok(已恢复)三种主流策略:
| 策略 | 写法 | 适用场景 |
|---|---|---|
unwrap() / expect() | m.lock().unwrap() | 教学代码、原型;等价于"中毒即视为致命错误" |
| 恢复继续用 | match m.lock() { Ok(g) => .., Err(e) => e.into_inner() } | 数据是简单计数/日志,中毒不影响正确性 |
| 主动清除 | m.clear_poison()(1.77+) | 明确知道数据完好,希望在日志里留一条"曾中毒" |
⚠️ 陷阱:
Mutex::clear_poison(&self)(最低稳定版本 1.77)清掉中毒标记,但不会修复可能已损坏的数据。 它只是让后续lock()重新返回Ok。用之前请确认不变式仍然成立。
RwLock<T>:读多写少
读写锁(reader-writer lock) 允许多个读者并发、写者独占。适合"配置/缓存"这类读远多于写的场景。
rust
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
fn main() {
let config = Arc::new(RwLock::new(String::from("v1")));
let mut handles = Vec::new();
// 三个读者可以同时进入读锁
for i in 0..3 {
let config = Arc::clone(&config);
handles.push(thread::spawn(move || {
let r = config.read().unwrap(); // 多个线程可同时持有读守卫
println!("reader {i} 看到: {}", *r);
}));
}
// 一个写者,必须独占
{
let config = Arc::clone(&config);
handles.push(thread::spawn(move || {
thread::sleep(Duration::from_millis(10));
let mut w = config.write().unwrap(); // 等到读者全部离开
*w = String::from("v2");
println!("writer 完成");
}));
}
for h in handles {
h.join().unwrap();
}
println!("最终: {}", *config.read().unwrap());
// 读锁未释放时取证:直接再 read() 会让写者无限等待,见「`RwLock` 读锁未释放导致写饥饿」
}text
输出(读者顺序不定):
reader 0 看到: v1
reader 1 看到: v1
reader 2 看到: v1
writer 完成
最终: v2写优先策略与饥饿。 std::sync::RwLock 的具体策略由平台实现决定(Windows 上是 SRWLOCK, Linux 上是 pthread_rwlock),标准库不作保证。在"写者优先"的实现上,一旦有写者在排队,新的读者会被拦住,避免写者饥饿。危险的是反向场景: 如果读者源源不断(例如读锁在一个长循环里反复获取,或者长期不释放),写者可能永远拿不到锁(写者饥饿)。缓解办法:
- 把长读操作拆短,避免"边读边做耗时计算";
- 需要"读到一致快照再做重活"时,先在读锁里克隆出需要的数据,立刻释放锁,然后慢慢算;
- 写多读少时,
Mutex<T>往往比RwLock<T>更快更简单(RwLock的读锁本身也有原子操作开销)。
⚠️ 陷阱:
RwLock的读锁不可重入。在已持有读锁的线程里再次read(),如果此时有写者在等待,就会死锁(递归读锁在有写者排队时会阻塞)。
死锁:成因与避免
Rust 的类型系统不检测死锁。最经典的形态是两个锁的反向获取顺序:
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, b1) = (Arc::clone(&a), Arc::clone(&b));
let t1 = thread::spawn(move || {
let _ga = a1.lock().unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
let _gb = b1.lock().unwrap(); // 等 b,而 b 已被 t2 拿走
println!("t1 完成");
});
let (a2, b2) = (Arc::clone(&a), Arc::clone(&b));
let t2 = thread::spawn(move || {
let _gb = b2.lock().unwrap(); // 反向顺序!
std::thread::sleep(std::time::Duration::from_millis(50));
let _ga = a2.lock().unwrap(); // 等 a,而 a 已被 t1 拿走
println!("t2 完成");
});
// 这个程序会永久挂住(不要在生产代码里运行)
t1.join().unwrap();
t2.join().unwrap();
println!("永远不会打印");
}⚠️ 陷阱:上面这段代码能完美编译,运行时静默挂死。Rust 不会在编译期发现它,运行时也没有"死锁检测"(
parking_lot在 debug 构建下有可选的死锁检测,std没有)。这类 bug 只能靠设计纪律避免:
- 固定加锁顺序:给所有需要同时持有的锁定义一个全局顺序(例如按地址、按 ID 排序),任何线程都按同一顺序获取。
try_lock打破等待:Mutex::try_lock()返回Result,拿不到就放掉已持有的锁、退避重试。要注意退避与超时,否则会变成活锁。- 缩小临界区:不要在持锁时做 I/O、sleep、调用可能再次加锁的函数("调用外部代码"是死锁的温床)。
- 一个锁优于两个锁:把构成同一不变式的数据合并进一把锁,能从根本上消除"多锁顺序"问题。
- 不嵌套:能在拿第二把锁之前完成第一把锁的工作,就不要嵌套持有。
原子类型与内存序
原子类型基础
原子类型(atomic type) 提供不会被线程调度打断的读-改-写操作,是 std 中最底层的同步工具。常用成员:
| 类型 | 说明 |
|---|---|
AtomicBool | 标志位 |
AtomicUsize / AtomicIsize / AtomicU32 / AtomicU64 / … | 计数、索引、状态机 |
AtomicPtr<T> | 指针交换(构造无锁数据结构用) |
AtomicI32 等 | 有符号整数 |
核心方法(以 AtomicUsize 为例):
rust
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
// 1) static 里的原子量不需要 Arc,也不需要 Mutex —— 这是它最大的优势
static HITS: AtomicUsize = AtomicUsize::new(0);
let handles: Vec<_> = (0..4)
.map(|_| {
thread::spawn(|| {
for _ in 0..1000 {
// fetch_add 返回"加之前的旧值",加法本身是原子的
HITS.fetch_add(1, Ordering::Relaxed);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
// load / store 是最基本的读与写
println!("hits = {}", HITS.load(Ordering::Relaxed));
// 2) compare_exchange:CAS(compare-and-swap),无锁算法的基石
let state = AtomicUsize::new(7);
// 参数:期望值、新值、成功后用的 Ordering、失败后用的 Ordering
match state.compare_exchange(7, 9, Ordering::AcqRel, Ordering::Acquire) {
Ok(old) => println!("CAS 成功,旧值 {old}"),
Err(cur) => println!("CAS 失败,当前值 {cur}"),
}
println!("state = {}", state.load(Ordering::SeqCst));
// 3) 无锁标志位:Release/Acquire 配对,保证"数据先写好,标志位后可见"
let ready = Arc::new(AtomicBool::new(false));
let r = Arc::clone(&ready);
let producer = thread::spawn(move || {
// ... 这里假设写好了某个被 ready 保护的数据 ...
r.store(true, Ordering::Release); // Release:之前的写不许排到它后面
});
while !ready.load(Ordering::Acquire) { // Acquire:之后的读不许排到它前面
std::hint::spin_loop(); // 自旋时给 CPU 一点提示,比空转省电
}
producer.join().unwrap();
println!("标志位已置起");
}text
输出:
hits = 4000
CAS 成功,旧值 7
state = 9
标志位已置起Ordering 五个变体
内存序(memory ordering) 描述"一个线程的读写以什么顺序对另一个线程可见"。这是并发编程里最抽象的部分,先记住实用建议:先用 SeqCst, profile 证明有瓶颈、并且你理解了 happens-before 之后,再逐步放松。
| 变体 | 保证 | 典型场景 |
|---|---|---|
Relaxed | 只保证该操作本身原子,不提供任何跨线程顺序 | 纯计数(不需要与其它内存建立关系)、统计指标 |
Acquire | 用于 load:本线程中它之后的读写不能重排到它之前;与配对的 Release 建立 happens-before | 读取"数据已就绪"标志、拿锁的入口 |
Release | 用于 store:本线程中它之前的读写不能重排到它之后;与配对的 Acquire 建立 happens-before | 发布数据后置"就绪"标志、放锁 |
AcqRel | Acquire + Release,用于读-改-写操作(如 fetch_add、compare_exchange 成功分支) | 无锁队列的入队/出队、CAS 成功路径 |
SeqCst | 在 AcqRel 基础上,所有 SeqCst 操作存在全序,所有线程看到的顺序一致 | 需要"全局一致顺序"的推理;不确定时的默认选择 |
读-改-写操作(fetch_*、compare_exchange、swap) 可以接受 Acquire/Release/AcqRel/SeqCst/Relaxed; 纯 load 传 Release 或 AcqRel 会在部分平台上 panic(详见 std::sync::atomic 文档的"panic 规则"), 请用与操作匹配的变体。
🧠 原理:
Relaxed也不是"什么都不做"——它仍然保证同一变量上的修改顺序(modification order),以及"不会读到该变量从未存在过的值"。 它缺少的只是跨变量的顺序约束。这就是为什么纯计数器用Relaxed是安全的,而"先写数据、再置标志位"必须用Release/Acquire。
🚀 进阶:要系统理解内存序,推荐阅读
std::sync::atomic::Ordering官方文档与 The Rustonomicon 的 Atomics 章节(入口见附录 D); 想验证自己的无锁代码,用loom做模型检验(见「并发测试与loom」)。
原子 vs Mutex:如何选择
| 需求 | 选择 | 理由 |
|---|---|---|
| 单个计数器 / 统计指标 | AtomicUsize 等 | 无锁、可在 static 上直接用、零临界区 |
| 一个布尔标志位(就绪/停止) | AtomicBool | 同上,配合 Release/Acquire |
| 单个值的 CAS 状态机 | AtomicU8/AtomicUsize + compare_exchange | 无锁状态迁移 |
| 多个字段必须一起变(复合不变式) | Mutex<T> 或 RwLock<T> | 原子类型无法保护跨字段的不变式 |
| 需要"读-计算-写"整体原子 | Mutex<T> | 原子只能保护单个操作,不能保护一个逻辑事务 |
| 需要跨多个原子量的一致性 | Mutex<T>(或用 SeqCst + 精心设计) | 多个原子量之间没有自动的事务性 |
反例(编译通过但逻辑错):
rust
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let a = Arc::new(AtomicUsize::new(0));
let b = Arc::new(AtomicUsize::new(0));
let handles: Vec<_> = (0..2)
.map(|_| {
let (a, b) = (Arc::clone(&a), Arc::clone(&b));
thread::spawn(move || {
// 想让 a 与 b 始终相等:两个独立的原子操作之间没有事务性
a.fetch_add(1, Ordering::SeqCst);
// 另一个线程可能正好在这中间被观察到
b.fetch_add(1, Ordering::SeqCst);
})
})
.collect();
for h in handles { h.join().unwrap(); }
// 最终 a == b,但中间任意时刻都可能 a != b —— 这就是"原子不等于一致"
println!("a={} b={}", a.load(Ordering::SeqCst), b.load(Ordering::SeqCst));
}⚠️ 陷阱:原子操作是"每个操作各自原子",不是"一组操作整体原子"。需要保持两个量同步时,把它们放进同一个结构体,用一把
Mutex护住。
Send 与 Sync 深入
精确定义
两个 auto trait 的官方定义只有两行:
rust
// 语义定义,标准库中它们是 unsafe auto trait
pub unsafe auto trait Send {} // 类型 T: Send —— 把 T 的**所有权**移到另一个线程是安全的
pub unsafe auto trait Sync {} // 类型 T: Sync —— 在两个线程同时持有 &T 是安全的由此可以推出那条著名等价关系:
T: Sync当且仅当&T: Send。
直觉解释:&T 是"共享引用",把 &T 送到另一个线程,等于让两个线程同时持有 T 的引用——这正是 Sync 的定义。所以不要死记两条规则, 记住"Sync = 引用能不能跨线程"。
| 类型 | Send | Sync | 说明 |
|---|---|---|---|
i32、String、Vec<T>(T: Send) | ✅ | ✅ | 普通拥有型数据 |
&T(T: Sync) | ✅ | ✅ | 共享引用可送,前提是被指数据可共享 |
&mut T(T: Send) | ✅ | ✅ | 独占引用等价于所有权转移 |
Rc<T> | ❌ | ❌ | 引用计数非原子,两个线程同时 clone/drop 会撕裂计数 |
RefCell<T> / Cell<T> | ✅ | ❌ | 借用检查在运行期,无同步时并发借用检查会撕裂 |
Arc<T> | ✅(T: Send + Sync) | ✅(T: Send + Sync) | 原子计数,但要内部数据也可共享 |
Mutex<T> | ✅(T: Send) | ✅(T: Send) | 锁保证串行访问 |
MutexGuard<'_, T> | ❌ | ✅(T: Sync) | 见「MutexGuard 的能力与"保护范围"边界」 |
*const T / *mut T | ❌ | ❌ | 裸指针没有安全保证 |
Rc<RefCell<T>> | ❌ | ❌ | 单线程"共享可变"标准组合,进不了多线程 |
mpsc::Receiver<T> | ✅ | ❌ | 见「Receiver 不是 Sync:多消费者怎么办」 |
JoinHandle<T> | ✅ | ✅ | 句柄可搬移与共享引用 |
auto trait 与 impl !Send
Send / Sync(还有 Unpin、UnwindSafe 等)是auto trait(自动 trait):它们没有方法,由编译器自动推导。 规则是"结构体是 Send,当且仅当它的所有字段都是 Send"。
rust
use std::rc::Rc;
struct MyBox {
inner: Rc<i32>, // 字段不是 Send
}
// 因此 MyBox 自动也不是 Send:没有任何一行代码声明这件事
fn assert_send<T: Send>() {}
fn main() {
assert_send::<i32>(); // OK
assert_send::<Vec<String>>(); // OK
assert_send::<MyBox>(); // 编译错误:MyBox 不是 Send
}text
error[E0277]: `Rc<i32>` cannot be sent between threads safely
--> src/main.rs:13:19
|
13 | assert_send::<MyBox>();
| ^^^^^ within `MyBox`, the trait `Send` is not implemented
| for `Rc<i32>`标准库对某些类型使用负实现(negative impl) impl !Send for ... 显式标记"永不 Send",最典型的是 Rc<T>:
rust
// 标准库源码(简化):Rc 在任何 T 下都不是 Send/Sync
impl<T: ?Sized> !Send for Rc<T> {}
impl<T: ?Sized> !Sync for Rc<T> {}这个负实现比"某字段不是 Send"更强:即使你给 Rc 包装一个全都 Send 的字段,Rc 本身依然不是 Send。
unsafe impl Send/Sync:正确性与风险
当你有充分的理由确信某个类型实际上可以跨线程使用时,可以手动实现:
rust
use std::cell::UnsafeCell;
/// 只在单线程内部使用、但需要在满足条件时可跨线程搬移的包装。
/// 这里用 UnsafeCell 模拟"裸的、无同步的可变数据"。
struct MyCell<T> {
value: UnsafeCell<T>,
}
// 安全性论证(写进 SAFETY 注释是硬性要求):
// 1. MyCell 不提供任何跨线程的共享访问方法;
// 2. 因此只要 T 能跨线程搬移(T: Send),搬移整个 MyCell 就是安全的。
// 注意 Sync 绝对不能这样加:&MyCell 允许多线程同时拿到 value,那才是数据竞争。
unsafe impl<T: Send> Send for MyCell<T> {}
fn main() {
let c = MyCell { value: UnsafeCell::new(1) };
let h = std::thread::spawn(move || unsafe { *c.value.get() = 2 });
h.join().unwrap();
println!("搬移并独占使用:成功");
}text
输出:搬移并独占使用:成功⚠️ 陷阱:
unsafe impl Send/Sync是你向编译器做的承诺,编译器此后不再检查。一旦承诺错误, 得到的就是货真价实的内存不安全(数据竞争在 Rust 里被定义为 UB)。实践建议:
- 写清楚
// SAFETY:注释,说明为什么安全,以及为什么在什么前提下才安全;- 把
unsafe impl限制在最小、最内层的类型上,外层继续靠自动推导;- 优先找现成的证明过的抽象(
Arc、Mutex、AtomicXxx、crossbeam、rayon);- 用
loom或 Miri 做验证。
编译器如何拦住数据竞争:完整报错解读
回到最开始的例子,看完整的错误链条。目标是让两个线程共享一个 Rc<RefCell<Vec<i32>>>:
rust
use std::cell::RefCell;
use std::rc::Rc;
use std::thread;
fn main() {
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
for _ in 0..2 {
let s = Rc::clone(&shared); // 单线程写法:Rc + RefCell
thread::spawn(move || {
s.borrow_mut().push(4); // 两个线程同时改同一个 Vec
});
}
}text
error[E0277]: `Rc<RefCell<Vec<i32>>>` cannot be sent between threads safely
--> src/main.rs:9:19
|
9 | thread::spawn(move || {
| ------------- ^------
| | |
| _______|_____________within this `{closure@src/main.rs:9:19: 9:26}`
| | |
| | required by a bound introduced by this call
10 | | s.borrow_mut().push(4);
11 | | });
| |_________^ `Rc<RefCell<Vec<i32>>>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:9:19: 9:26}`, the trait `Send` is not implemented
for `Rc<RefCell<Vec<i32>>>`
note: required because it's used within this closure
note: required by a bound in `spawn`
|
125 | pub fn spawn<F, T>(f: F) -> JoinHandle<T>
| ----- required by a bound in this function
...
128 | F: Send + 'static,
| ^^^^ required by this bound in `spawn`读报错的顺序(非常实用,遇到泛型报错都这么读):
- 最上面一行:谁不满足哪个 trait ——
Rc<RefCell<Vec<i32>>>不满足Send。 = help:行的"within ...":说明是哪一层导致的 —— 闭包捕获了Rc<...>,闭包因此不Send。note: required by a bound in ...:追到最底层的要求来源 ——spawn要求F: Send + 'static。- 结论:不是"你的代码写错了",而是"这个类型在设计上就不允许跨线程"。修法是换类型,不是加
move。
修法:把 Rc → Arc、RefCell → Mutex:
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let shared = Arc::new(Mutex::new(vec![1, 2, 3]));
let mut handles = Vec::new();
for _ in 0..2 {
let s = Arc::clone(&shared);
handles.push(thread::spawn(move || {
s.lock().unwrap().push(4); // 现在每个 push 都在锁里
}));
}
for h in handles { h.join().unwrap(); }
println!("{:?}", *shared.lock().unwrap()); // 长度一定是 5,不是"大约 5"
}text
输出:[1, 2, 3, 4, 4]💡 对照:Java 里同样的错误会在运行时表现为
HashMap死循环、丢失更新或诡异的ConcurrentModificationException; Python 里list.append因为 GIL 意外地"看起来没问题",但复合操作依然会丢数据。Rust 把这个问题提前到了编译期, 代价是你要显式选择Arc<Mutex<_>>这种组合。