Skip to content

共享状态与同步

Mutex/RwLock/Arc 共享数据,原子类型与内存序,Send/Sync 的原理。

共享状态:MutexRwLockArc

消息传递不是万能的:有些状态天然需要多方共享(配置、缓存、连接池)。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 情况很值得记:

类型SendSync原因
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 必须一致,但类型系统并不知道这个约束");
}

balancehistory 必须同步更新,但只有 balance 在锁里。两个线程可以各自 lock() 修改余额、 各自 push 历史——没有数据竞争Vec 的并发写会编译失败,或改用 Mutex<Vec<_>>),但如果把它们放进不同的锁,就可能出现"余额加了但历史没记"的窗口。 结论:把构成同一个不变式的所有字段放进同一把锁。

Arc<Mutex<T>>:完整的并发计数器

Arc<T>(atomically reference counted,原子引用计数)是〈智能指针与闭包〉一章的多线程版 RcArc<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 策略

如果某个线程在持有锁的时候 panicMutex 会被标记为中毒(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 只能靠设计纪律避免:

  1. 固定加锁顺序:给所有需要同时持有的锁定义一个全局顺序(例如按地址、按 ID 排序),任何线程都按同一顺序获取。
  2. try_lock 打破等待Mutex::try_lock() 返回 Result,拿不到就放掉已持有的锁、退避重试。要注意退避与超时,否则会变成活锁。
  3. 缩小临界区:不要在持锁时做 I/O、sleep、调用可能再次加锁的函数("调用外部代码"是死锁的温床)。
  4. 一个锁优于两个锁:把构成同一不变式的数据合并进一把锁,能从根本上消除"多锁顺序"问题。
  5. 不嵌套:能在拿第二把锁之前完成第一把锁的工作,就不要嵌套持有。

原子类型与内存序

原子类型基础

原子类型(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发布数据后置"就绪"标志、放锁
AcqRelAcquire + Release,用于读-改-写操作(如 fetch_addcompare_exchange 成功分支)无锁队列的入队/出队、CAS 成功路径
SeqCstAcqRel 基础上,所有 SeqCst 操作存在全序,所有线程看到的顺序一致需要"全局一致顺序"的推理;不确定时的默认选择

读-改-写操作(fetch_*compare_exchangeswap) 可以接受 Acquire/Release/AcqRel/SeqCst/Relaxed; 纯 loadReleaseAcqRel 会在部分平台上 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 护住。


SendSync 深入

精确定义

两个 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 = 引用能不能跨线程"。

类型SendSync说明
i32StringVec<T>T: Send普通拥有型数据
&TT: Sync共享引用可送,前提是被指数据可共享
&mut TT: 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(还有 UnpinUnwindSafe 等)是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)。实践建议:

  1. 写清楚 // SAFETY: 注释,说明为什么安全,以及为什么在什么前提下才安全;
  2. unsafe impl 限制在最小、最内层的类型上,外层继续靠自动推导;
  3. 优先找现成的证明过的抽象(ArcMutexAtomicXxxcrossbeamrayon);
  4. 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`

读报错的顺序(非常实用,遇到泛型报错都这么读):

  1. 最上面一行:谁不满足哪个 trait —— Rc<RefCell<Vec<i32>>> 不满足 Send
  2. = help: 行的"within ...":说明是哪一层导致的 —— 闭包捕获了 Rc<...>,闭包因此不 Send
  3. note: required by a bound in ...:追到最底层的要求来源 —— spawn 要求 F: Send + 'static
  4. 结论:不是"你的代码写错了",而是"这个类型在设计上就不允许跨线程"。修法是换类型,不是加 move

修法:把 RcArcRefCellMutex

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<_>> 这种组合。


内容以 rustc 1.98.1 · Rust 2024 edition 为基准