Rust 可变性完整指南 ------ 从默认不可变到多线程安全
一、引言
程序本质上是对数据的操作。而数据操作最基本的两个问题是:
-
谁能修改这份数据?
-
多个执行流同时访问时,会发生什么?
Rust 的回答是:默认不可改,要改必须显式声明。这个看似简单的变化,从根上消除了整类 bug。
本文从最基础的"可变性"概念出发,逐步深入单线程内部可变性、跨线程所有权传递、跨线程可变性,最后呈现一个完整的工具选型指南。全篇文章的逻辑链如下:
rust
默认不可变(let)→ 外部可变性(&mut T)→ 内部可变性(Cell / RefCell)
↓
跨线程安全标记(两个正交的概念):
├── Send:所有权可以跨线程转移
└── Sync:&T 的跨线程修改必须安全
↓
Arc 是连接点 ------ 同时要求 T: Send + Sync
↓
多线程工具:Mutex / RwLock / Atomic
二、基础:默认不可变与外部可变性
2.1 默认不可变
Rust 在变量声明时就决定了数据的可变性:
rust
let x = 42; // 不可变 ------ 编译器保证值永远不会改变
// x = 43; // ❌ 编译错误:不能对不可变变量赋值
let mut y = 42; // 可变 ------ 可以修改
y = 43; // ✅
不可变是默认的,可变需要显式声明。
在 Rust 中,let x = 42 意味着:
markdown
1. 不能直接赋值:x = 43 → 编译错误
2. 不能取 &mut 引用:&mut x → 编译错误
3. 所有通过 &T 的访问都是只读的 → 编译器保证
4. 没有任何方式可以绕过这些规则 → 没有 "const_cast"
Rust 的不可变是真正的不可变。 不存在"通过指针绕过"的可能性。
rust
let x = 42;
// 所有合法的操作都是只读的
let r1 = &x; // ✅ 可以取 &T
let r2 = &x; // ✅ 多个 &T 可以共存
println!("{}", x); // ✅ 可以读
// 所有修改操作都是非法的
// let r3 = &mut x; // ❌ 编译错误:不能对不可变变量取 &mut
// x = 43; // ❌ 编译错误:不能对不可变变量赋值
2.2 mut 是视觉标记
看到 let mut,你就知道"这个值会被修改"。看到 let,你就知道"这个值从创建到销毁不会变"。
rust
fn example() {
let x = compute_value(); // 不变 ------ 只读一次
let mut y = vec![]; // 可变 ------ 后面会 push、pop、clear
let z = &x; // 不变 ------ 只读
// ...
}
在代码审查中,let mut 是高亮标记------它会吸引审查者的注意力:"为什么这里需要可变?"
2.3 外部可变性:&T vs &mut T
如果一个值是可变的(let mut x),它可以通过两种方式被访问:
rust
let mut x = 42;
// 方式 1:通过 &T ------ 只读访问
let r: &i32 = &x; // 其他代码只能读 x
// *r = 43; // ❌ 不能通过 &T 写入
// 方式 2:通过 &mut T ------ 读写访问
let r: &mut i32 = &mut x; // 独占写入权
*r = 43; // ✅
外部可变性的规则是:&T 和 &mut T 不能同时活跃。 这是借用规则------一个值要么有任意多个 &T,要么有一个 &mut T。
三、内部可变性的起源
3.1 外部可变性的局限
外部可变性(&T 只读、&mut T 独占读写)是 Rust 安全的基石。但有些合法模式无法通过这个规则表达。
场景 1:引用计数
rust
use std::rc::Rc;
let r = Rc::new(42); // 通过 &Rc<i32> 访问
let r2 = Rc::clone(&r); // Rc::clone 需要递增引用计数
// ↑ 但 r 是 &Rc<i32>------不可变引用
// Rc 内部必须通过 &self 修改引用计数
场景 2:缓存统计
rust
struct Cache {
data: HashMap<u32, String>,
hits: u32,
}
impl Cache {
fn get(&self, key: u32) -> Option<&String> {
// 希望在这里更新 hits:self.hits += 1
// ❌ self 是 &self,hits 不可变
self.data.get(&key)
}
}
场景 3:测试替身
rust
struct MockService {
call_count: u32,
}
impl MockService {
fn handle(&self, request: Request) -> Response {
// 希望在这里记录调用次数
// ❌ self 是 &self
Response::ok()
}
}
共同点:需要在 &self(共享引用、不可变)下修改内部状态。
3.2 解决方案:内部可变性
-
外部可变性 :
&mut T,编译期检查,零运行时开销 -
内部可变性 :通过
Cell<T>或RefCell<T>在&self下修改,把检查推迟到运行时
| 外部可变性 | 内部可变性 | |
|---|---|---|
| 访问方式 | &mut T |
&T + Cell/RefCell |
| 检查时机 | 编译期 | 运行时 |
| 违反规则的后果 | 编译错误 | panic |
是否需要 &mut self |
是 | 否 |
3.3 核心洞察
Rust 需要内部可变性,正是因为 Rust 有"不能修改"的保证(&T),而有些合法场景需要在共享引用下修改。内部可变性提供了受控的例外 :在保持 &T API 签名不变的前提下,允许特定字段修改。
四、线程内内部可变性:Cell 与 RefCell
4.1 Cell------Copy 类型的零成本内部可变性
Cell<T> 适用于 T: Copy 的类型。它的 get 和 set 都只需要 &self:
rust
use std::cell::Cell;
let c = Cell::new(42);
println!("{}", c.get()); // 42
c.set(100); // 通过 &Cell<i32> 修改
println!("{}", c.get()); // 100
关键:set 和 get 都不需要 &mut self:
rust
impl<T> Cell<T> {
pub fn set(&self, val: T); // ← 只需 &self
pub fn get(&self) -> T where T: Copy; // ← 只需 &self
}
为什么 Cell 是安全的:
sql
1. T 必须是 Copy ------ set 和 get 都是直接取值/存值
2. get() 返回副本,不是内部数据的引用
3. set() 替换整个值,不产生内部引用
4. 不存在"持有内部引用时修改值"的可能性
(因为 Cell 从不返回 &T)
Cell 通过限制为 Copy 类型来保证安全。 如果 T 不是 Copy(如 String),get() 需要返回引用------那就需要 RefCell。
零成本抽象:
rust
use std::mem::size_of;
println!("{}", size_of::<i32>()); // 4
println!("{}", size_of::<Cell<i32>>()); // 4 ------ 无额外开销
Cell<i32> 在内存中就是一个 i32,get/set 编译为普通的读/写指令。
常见用法:计数器
rust
struct Counter {
count: Cell<u32>,
}
impl Counter {
fn increment(&self) { // &self,不是 &mut self
self.count.set(self.count.get() + 1);
}
fn value(&self) -> u32 {
self.count.get()
}
}
let c = Counter::new();
let r1 = &c;
let r2 = &c;
r1.increment();
r2.increment();
assert_eq!(c.value(), 2);
与 Rc 配合共享状态:
rust
use std::rc::Rc;
use std::cell::Cell;
let shared: Rc<Cell<i32>> = Rc::new(Cell::new(0));
let r1 = Rc::clone(&shared);
let r2 = Rc::clone(&shared);
r1.set(10);
r2.set(20);
println!("{}", shared.get()); // 20
4.2 RefCell------运行时借用检查
RefCell<T> 适用于任意类型,通过运行时借用检查来实现内部可变性:
rust
use std::cell::RefCell;
let rc = RefCell::new(String::from("hello"));
// 通过 borrow() 获取不可变引用
let r1 = rc.borrow(); // 返回 Ref<String>
println!("{}", r1); // "hello"
// 通过 borrow_mut() 获取可变引用
let mut r2 = rc.borrow_mut(); // 返回 RefMut<String>
r2.push_str(", world");
println!("{}", r2); // "hello, world"
签名:
rust
impl<T: ?Sized> RefCell<T> {
pub fn borrow(&self) -> Ref<'_, T>;
pub fn borrow_mut(&self) -> RefMut<'_, T>;
}
两者都只需要 &self。
运行时借用规则:
rust
let rc = RefCell::new(String::from("hello"));
let r1 = rc.borrow(); // ✅ 不可变引用(计数=1)
let r2 = rc.borrow(); // ✅ 另一个不可变引用(计数=2)
// let mut r3 = rc.borrow_mut(); // ❌ 运行时 panic!
// 已有不可变引用,不能借可变引用
drop(r1);
drop(r2);
let mut r3 = rc.borrow_mut(); // ✅ 现在可以了
规则与编译期相同:
-
多个
borrow()可以共存 -
borrow()和borrow_mut()不能共存 -
多个
borrow_mut()不能共存
违反规则时------panic,不是未定义行为(UB)。
运行时开销:
rust
// RefCell 的内部结构
struct RefCell<T> {
value: T,
borrow: Cell<BorrowFlag>, // 借用计数:0 / n / -1
}
每次 borrow() / borrow_mut():
-
检查当前借用标志
-
如果合法,更新标志
-
返回
Ref/RefMut(drop 时恢复标志)
开销:一次整数检查 + 一次赋值。
Rc + RefCell:经典组合:
rust
use std::rc::Rc;
use std::cell::RefCell;
let shared: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(vec![]));
let r1 = Rc::clone(&shared);
let r2 = Rc::clone(&shared);
r1.borrow_mut().push(1);
r2.borrow_mut().push(2);
println!("{:?}", shared.borrow()); // [1, 2]
Rc 提供共享所有权,RefCell 提供共享可变性。
try_borrow------panic-free 版本:
rust
match rc.try_borrow_mut() {
Ok(mut r) => r.push_str("ok"),
Err(_) => println!("暂时借不到"),
}
4.3 &mut T vs Cell vs RefCell 对比
| 维度 | &mut T |
Cell<T> |
RefCell<T> |
|---|---|---|---|
| 检查时机 | 编译期 | 无需检查 | 运行时 |
| 违反规则 | 编译错误 | 不存在 | panic |
| T 的要求 | 任意 | Copy |
任意 |
| 获取值 | 直接 *r |
get() 返回副本 |
borrow() 返回引用 |
| 修改方式 | *r = val |
set(val) |
borrow_mut() |
| 运行时开销 | 零 | 零 | 每次 borrow 一次检查 |
能否在 &self 下用 |
❌ | ✅ | ✅ |
| 线程安全 | Send | !Sync | !Sync |
4.4 选型流程
swift
需要内部可变性(在 &self 下修改)?
│
├── T 是 Copy 吗?
│ ├── ✅ → Cell<T>(零运行时开销)
│ └── ❌ → RefCell<T>(运行时检查)
│
├── 还需要共享所有权?
│ └── ✅ → Rc<Cell/RefCell<T>>
│
└── 还需要跨线程?
└── → 见第六章「跨线程可变性」
五、跨线程所有权传递:Send
Cell 和 RefCell 只能在单线程中使用。当我们需要跨线程操作数据时,首先要解决一个问题:数据的所有权能否安全地转移到另一个线程?
5.1 Send 的定义
rust
pub unsafe auto trait Send { }
Send 的含义:"把这个值的所有权转移到另一个线程是安全的。"
rust
// Send 类型:所有权可以跨线程传递
fn spawn<T: Send + 'static>(val: T) -> JoinHandle<T> {
thread::spawn(move || {
// 使用 val------所有权已经转移到这里
val
})
}
Send 是一个 auto trait------编译器自动推导 。如果类型的所有字段都是 Send,类型自动成为 Send。不需要手动实现。
5.2 Send 的自动推导
rust
// 类型的所有字段都是 Send,类型自动是 Send
struct User {
name: String, // String 是 Send
age: i32, // i32 是 Send
}
// User 自动是 Send------可以跨线程传递
// 如果有非 Send 的字段,类型自动不是 Send
use std::rc::Rc;
struct NotSend {
data: Rc<i32>, // Rc<i32> 不是 Send
}
// NotSend 自动不是 Send------不能跨线程传递
5.3 Send 与 thread::spawn
rust
// thread::spawn 的签名:
// pub fn spawn<F, T>(f: F) -> JoinHandle<T>
// where
// F: FnOnce() -> T,
// F: Send + 'static,
// T: Send + 'static,
// 要求闭包是 Send------意味着它捕获的所有值都必须是 Send
// 如果捕获了非 Send 的值------编译错误
let s = String::from("hello");
// ✅ String 是 Send
thread::spawn(move || {
println!("{}", s);
}).join().unwrap();
5.4 Rc 不是 Send,Arc 是 Send
rust
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
// Rc 的引用计数不是原子的------单线程优化
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Rc 不是 Send
// Arc 的引用计数是原子的------多线程安全
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // ✅ Arc 是 Send
}).join().unwrap();
Rc 不是 Send 是有意为之的: 非原子引用计数在多线程中会数据竞争。Arc 用原子操作,所以是 Send。
5.5 unsafe impl Send
极少数情况下,你的类型包含非 Send 的字段,但你知道它在你的使用场景中是线程安全的,这时可以用 unsafe impl Send:
rust
struct MyBox<T>(*mut T); // 裸指针------不是自动 Send
// 但 MyBox 在语义上等同于 Box------我们保证它是安全的
unsafe impl<T> Send for MyBox<T> {}
// ⚠️ unsafe impl Send 意味着:"我保证这个类型确实在线程间安全"
// 如果保证错了------未定义行为
标准库中的例子:Box<T> 包含裸指针,但标准库 unsafe impl Send for Box<T>,因为 Box 在语义上是拥有所有权的唯一持有者。
六、跨线程可变性:Sync
有了 Send,我们可以把数据的所有权转移到新线程。但如果多个线程要同时访问同一份数据 (通过 Arc 共享),还需要另一个保证。
6.1 Sync 的定义
rust
pub unsafe auto trait Sync { }
Sync 的含义:当多个线程同时持有 &T 时,通过 &T 修改数据是线程安全的。
有些类型不需要 &mut 就能修改自身------这就是内部可变性。Cell::set(&self)、Mutex::lock(&self)、AtomicU32::fetch_add(&self) 都只需要共享引用。
Sync 问的是:这些不需要 &mut 就能执行的修改,在多线程同时调用时安全吗?
r
Sync = 通过 &T 的修改是线程安全的
AtomicU32(原子指令)、Mutex<T>(锁)
!Sync = 通过 &T 的修改不是线程安全的
Cell<T>(普通赋值)、RefCell<T>(非原子标志)
6.2 为什么跨线程可变性需要 Sync
跨线程共享数据最常用的方式是 Arc<T>。但 Arc<T> 只给持有者 &T------通过 Deref 拿到的是不可变引用。
rust
Arc<Mutex<i32>> 给你的是 &Mutex<i32>,不是 &mut i32
Arc<Cell<i32>> 给你的是 &Cell<i32>, 不是 &mut i32
通过 Arc 共享后,你拿不到 &mut T------因为 Arc 可能被多个线程同时持有,同时拿到 &mut T 就违反了借用规则。
所以要通过 Arc 跨线程修改数据,只能走内部可变性------通过 &self 修改。 Mutex::lock(&self)、AtomicU32::fetch_add(&self)、Cell::set(&self) 都是这样。
Sync 的意义就来了:这些通过 &self 修改的操作,在多线程同时调用时安全吗?
-
Mutex::lock(&self)--- 锁保护 → 安全 → Sync -
AtomicU32::fetch_add(&self)--- CPU 原子指令 → 安全 → Sync -
Cell::set(&self)--- 普通赋值 → 不安全 → !Sync
6.3 Sync 的自动推导
rust
// 类型的所有字段都是 Sync → 类型自动是 Sync
struct SafeWrapper {
counter: AtomicU32, // Sync
}
// SafeWrapper 自动是 Sync
struct UnsafeWrapper {
counter: Cell<u32>, // !Sync
}
// UnsafeWrapper 自动是 !Sync
只要有一个字段是 !Sync,整个类型就是 !Sync。
6.4 跨线程可访问的数据只有两种
多线程程序中,一份数据能被多个线程同时访问的路径只有两条:
php
路径 1:全局变量(static)
数据在静态存储区。所有线程都能访问它。不需要"传"------它就在那。
路径 2:转移来的值(Send + Arc)
数据的所有权转移到另一个线程。如果被 Arc 包装,
Arc 的每个 clone 都指向同一块堆内存------多个线程操作同一份数据。
Sync 的检查就发生在这两条路径上。
6.5 全局变量(static)要求 T: Sync
rust
use std::sync::atomic::{AtomicU32, Ordering};
// ✅ Sync 类型------所有线程同时操作安全
static COUNTER: AtomicU32 = AtomicU32::new(0);
// ❌ !Sync 类型------编译器拒绝放在静态区
// static BAD: Cell<u32> = Cell::new(0);
// error: `Cell<u32>` cannot be shared between threads safely
// Cell::set 用普通赋值------不是原子的,多线程同时调用 → 数据竞争
rust
// ✅ 编译通过:
static A: AtomicU32 = AtomicU32::new(0); // Sync
static B: Mutex<u32> = Mutex::new(0); // Sync
// ❌ 编译错误:
// static C: Cell<u32> = Cell::new(0); // !Sync
// static D: RefCell<u32> = RefCell::new(0); // !Sync
6.6 转移来的值(Arc)要求 T: Sync
rust
use std::sync::{Arc, Mutex};
use std::thread;
// ✅ Mutex 是 Sync------多个线程通过 Arc 共享安全
let data = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10).map(|_| {
let d = Arc::clone(&data);
thread::spawn(move || {
let mut val = d.lock().unwrap();
*val += 1;
})
}).collect();
rust
use std::sync::Arc;
use std::cell::Cell;
// ❌ Cell 不是 Sync------Arc<Cell<i32>> 不能跨线程传递
let data = Arc::new(Cell::new(0));
// thread::spawn(move || { data.set(100); });
// ❌ Arc<Cell<i32>> 不是 Send------Cell 不是 Sync
6.7 Arc 的源码约束
rust
// Arc 的源码约束(简化)
unsafe impl<T: Sync + Send> Send for Arc<T> {}
unsafe impl<T: Sync + Send> Sync for Arc<T> {}
Arc 自身引用计数是原子的------跨线程安全。但 Arc 内部的数据 T 并没有被保护。如果 Arc 允许 !Sync 的 T(如 RefCell)跨线程共享,那么多个线程就可以通过 Arc 拿到同一个 &T,同时调用内部可变方法------数据竞争。
rust
Arc<RefCell<i32>> 被 clone 两次:
线程 1 → &RefCell<i32> → borrow_mut() ← 同时调用,非原子
线程 2 → &RefCell<i32> → borrow_mut() ← 数据竞争!
Arc 拦截:RefCell 不是 Sync → Arc<RefCell<i32>> 不实现 Send → 编译错误
Arc 的守门规则: "你想让我带着内部数据 T 跨线程传递或共享?先证明 T 既是 Send 又是 Sync。"
6.8 解决方案的本质:在 Arc 内部加锁
rust
Arc<Mutex<i32>> 被 clone 两次:
线程 1 → &Mutex<i32> → lock() → 加锁 → 修改 → 解锁
线程 2 → &Mutex<i32> → lock() → 等待 → 修改 → 解锁
Mutex 是 Sync → Arc 放行 → 编译通过 ✅
即:Arc 负责共享和引用计数,Mutex 负责内部数据的互斥访问。 两者配合,实现了安全的跨线程可变性。
6.9 为什么 thread::spawn 的闭包需要 Sync
thread::spawn 的签名要求闭包是 Send------因为闭包的所有权要转移到新线程。
但如果闭包捕获的是 Arc<T>,所有权转移(Send)就不够了------因为 Arc 的设计就是让多个线程同时持有同一数据的引用:
rust
let data = Arc::new(Cell::new(0));
// 两个 Arc::clone(),两个线程各持一份
let d1 = Arc::clone(&data);
let d2 = Arc::clone(&data);
// 各自交给不同的线程
thread::spawn(move || { d1.set(100); });
thread::spawn(move || { d2.set(200); });
// 两个线程都持有 &Cell<i32>,同时调用 set()------数据竞争
问题在于:Send 只保证'所有权可以转移',但转移后数据可能通过 Arc 被多个线程同时访问。 如果被访问的数据有内部可变性,这个内部可变性必须是线程安全的。
这就是为什么 Arc<T> 的 Send 实现要求 T: Send + Sync:
rust
impl<T: Send + Sync> Send for Arc<T> {}
// ↑
// Arc 额外要求 T: Sync
// 因为 Arc 的 clone 会创建多个指向同一数据的引用
// 如果 T 有内部可变性,这个可变必须线程安全
6.10 标准库中的 Sync/!Sync
r
Sync 的类型(通过 &T 修改是安全的):
AtomicU32 / AtomicBool / AtomicUsize --- CPU 原子指令
Mutex<T> --- 锁保护
RwLock<T> --- 读写锁保护
!Sync 的类型(通过 &T 修改不安全):
Cell<T> --- 内部用普通赋值,无原子操作
RefCell<T> --- 内部用非原子借用计数
UnsafeCell<T> --- 基础内部可变性原语
6.11 Send 与 Sync 是正交的
Send 和 Sync 是两个完全正交的 trait------它们回答的是两个不同的问题:
rust
Send:这个值的所有权可以转移到另一个线程吗?
Sync:这个类型的 &T 引用可以被多个线程同时安全地访问吗?
一个类型可以为 Send+Sync、Send+!Sync、!Send+Sync、或 !Send+!Sync,四种组合都是可能的:
| 组合 | 含义 | 典型类型 |
|---|---|---|
| Send + Sync | 所有权可转移,共享引用也安全 | Mutex<T>、AtomicU32、Arc<T>、String、&T |
| Send + !Sync | 所有权可转移,但 &T 不安全 | Cell<T>、RefCell<T>(单线程内 Send,但跨线程 &T 不安全) |
| !Send + Sync | 所有权不能转移,但 &T 安全 | MutexGuard<'_, T>(&T 安全,但持有锁,所有权不能跨线程) |
| !Send + !Sync | 所有权不能转移,&T 也不安全 | Rc<T>(非原子引用计数,两者都有问题) |
注:Cell<T> 和 RefCell<T> 是 !Sync 但默认是 Send (因为它们的内部数据通过 UnsafeCell 拥有,所有权可以转移,只是不能共享)。
6.12 Arc------Send 与 Sync 的连接点
Arc 是唯一同时要求 Send 和 Sync 的标准库类型:
rust
impl<T: Send + Sync> Send for Arc<T> {}
impl<T: Send + Sync> Sync for Arc<T> {}
为什么 Arc 需要两者同时满足?
r
Arc 的本质行为 = 所有权转移 + 共享引用
Arc::clone() 把所有权转移给新线程 → 需要 T: Send
Arc 的多个 clone 指向同一堆内存 → 多个线程同时持有 &T
如果 T 有内部可变性,修改必须是线程安全的 → 需要 T: Sync
换句话说,Arc 把 Send 和 Sync 这两个正交的概念联系在了一起:
-
没有 Send:所有权无法跨线程转移,Arc 本身就无法跨线程
-
没有 Sync:即使所有权转移了,Arc 创建的共享引用不安全
-
两者缺一不可
这就是为什么当你想在多个线程间共享可变数据时,标准答案是 Arc<Mutex<T>>:
rust
Arc → 负责"所有权可以跨线程转移"(Send)+ "多个线程可以共享引用"(Sync 的容器)
Mutex → 负责"&T 的跨线程修改是安全的"(Sync 的实现者)
Arc<Mutex<T>> ← Send 和 Sync 在此连接
Send 回答:所有权能过去吗?"能"------Arc 说"需要 T: Send"。
Sync 回答:共享引用安全吗?"安全"------Arc 说"需要 T: Sync"。
Arc 要求两个答案都是"是"才能放行。
6.13 实战追踪:从 !Sync 到正确方案的一步步推演
下面以 Cell<i32>(典型的 Send + !Sync 类型)为例,从"不共享"开始,逐步尝试各种共享方式,看编译器如何在每一步引导你走向最终的正确方案。
第 1 步:不共享------所有权直接转移给一个新线程
rust
use std::cell::Cell;
use std::thread;
let c = Cell::new(42);
// ✅ 编译通过!
thread::spawn(move || {
c.set(100); // 新线程拥有 c 的独占所有权
println!("{}", c.get()); // 100
}).join().unwrap();
为什么可以通过?因为不需要 Sync。
rust
Cell<i32> 的所有权从主线程 move 到新线程
→ 新线程是唯一的持有者
→ 只有一个线程能访问 Cell,不存在"多个线程同时 &Cell"
→ thread::spawn 只需要捕获的值是 Send
→ Cell<i32> 的所有字段都是 Send → Cell<i32> 是 Send ✅
→ 编译通过!
这里 Cell<i32> 是 Send + !Sync ,但因为没有任何共享引用,编译器根本不需要检查 Sync。!Sync 在不共享的场景中完全不构成障碍。
第 2 步:尝试共享------用 Rc 包装后分发给多个线程
单线程中,Rc<Cell<i32>> 工作正常------Rc 提供共享所有权,Cell 提供内部可变性:
rust
use std::rc::Rc;
use std::cell::Cell;
let data = Rc::new(Cell::new(0));
let d1 = Rc::clone(&data);
let d2 = Rc::clone(&data);
d1.set(10);
d2.set(20);
println!("{}", data.get()); // 20
// ✅ 单线程 OK
现在尝试把 Rc 跨线程传递:
rust
use std::rc::Rc;
use std::cell::Cell;
use std::thread;
let data = Rc::new(Cell::new(0));
thread::spawn(move || {
data.set(100); // ❌ 编译错误!
});
编译器报错:
swift
error[E0277]: `Rc<Cell<i32>>` cannot be sent between threads safely
= help: the trait `Send` is not implemented for `Rc<Cell<i32>>`
= note: `Rc` is not `Send` because its reference count is not atomic
= help: consider using `Arc` instead ← 编译器直接给了建议!
编译器告诉你:Rc 不是 Send,因为它的引用计数不是原子的。考虑用 Arc 替代。
第 3 步:按照提示,把 Rc 换成 Arc
rust
use std::cell::Cell;
use std::sync::Arc;
use std::thread;
let data = Arc::new(Cell::new(42));
let d1 = Arc::clone(&data);
let d2 = Arc::clone(&data);
// ❌ 编译错误!Arc<Cell<i32>> 不是 Send
thread::spawn(move || { d1.set(100); });
thread::spawn(move || { d2.set(200); });
为什么失败了?因为 Arc 引入了共享引用。
rust
Arc::clone() 创建多个指向同一 Cell 的引用
→ 多个线程可能同时持有 &Cell<i32>
→ 需要通过 &Cell<i32> 修改(内部可变性)
→ 编译器必须检查 &Cell<i32> 的修改是否线程安全
→ 这需要 Cell<i32>: Sync
→ 但 Cell<i32>: !Sync(内部用普通赋值,非原子操作)
→ Arc 放行失败 → 编译错误
逐层追踪 Arc<Cell> 的检查链:
rust
第 1 层:thread::spawn 签名
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static, ← 闭包必须实现 Send
T: Send + 'static
问题:闭包 move || { d1.set(100); } 是 Send 吗?
第 2 层:闭包捕获了 d1: Arc<Cell<i32>>
闭包的 Send 由它捕获的所有值决定------所有捕获值都必须实现 Send。
所以问题变成:Arc<Cell<i32>> 是 Send 吗?
第 3 层:查找 Arc<T> 的 Send 实现
// 标准库源码(简化)
unsafe impl<T: Sync + Send> Send for Arc<T> {}
Arc 的 Send 实现要求 T: Send + Sync ------两者缺一不可。
所以问题变成:Cell<i32> 是 Send 吗?且 Cell<i32> 是 Sync 吗?
第 4 层:检查 Cell<i32>: Send
Cell<i32> 的所有字段(内部 UnsafeCell<i32>)都是 Send → ✅ Send
这一步通过了。
第 5 层:检查 Cell<i32>: Sync(这里才是关键!)
Sync 是 auto trait------编译器检查 Cell<i32> 的所有字段。
Cell 内部包含 UnsafeCell<T>,而 UnsafeCell<T> 是 !Sync。
编译器推导:
UnsafeCell<i32> → !Sync
Cell<i32> 包含 UnsafeCell<i32> → Cell<i32> 也是 !Sync
第 6 层:结论------ Cell<i32> 不是 Sync
Arc 的 Send 实现要求 T: Sync,但 Cell<i32>: !Sync。
所以 Arc 的 Send 实现不适用------Arc<Cell<i32>> 不是 Send。
第 7 层:最终报错
thread::spawn 要求闭包是 Send。
闭包捕获了 Arc<Cell<i32>>,但 Arc<Cell<i32>> 不是 Send。
→ 编译器报错!
编译器实际输出的错误信息:
swift
error[E0277]: `Arc<Cell<i32>>` cannot be sent between threads safely
= help: the trait `Send` is not implemented for `Arc<Cell<i32>>`
= note: `Arc` is `Send` only when `Cell<i32>: Send + Sync`
编译器说:Arc<Cell<i32>> 不是 Send------因为 Arc 的 Send 要求 T: Send + Sync,但 Cell<i32> 不是 Sync。
等等------第 2 步用 Rc 报错,第 3 步用 Arc 也报错,区别在哪?
rust
第 2 步:Rc<Cell<i32>> 不是 Send → Rc 本身就不行(引用计数非原子)
第 3 步:Arc<Cell<i32>> 不是 Send → Arc 本身行,但内部数据 Cell 不行(!Sync)
Rc 报错是因为 Rc:不管里面装什么,Rc 都不是 Send
Arc 报错是因为 Cell:Arc 自己是 Send,但要求内部数据也是 Send + Sync
第 4 步:最终方案------Arc + Mutex
要解决 Cell 不是 Sync 的问题,需要把 Cell 换成一种通过 &self 修改时线程安全的类型------Mutex、RwLock 或原子类型。最简单的方案是 Mutex:
rust
use std::sync::{Arc, Mutex};
use std::thread;
let data = Arc::new(Mutex::new(42));
let d = Arc::clone(&data);
thread::spawn(move || {
*d.lock().unwrap() = 100; // ✅ 编译通过
}).join().unwrap();
为什么这次通过了?
rust
Mutex 是 Sync
→ 它的 lock(&self) 用操作系统锁保证线程安全
→ 所以 Arc<Mutex<i32>> 满足 Arc 的 Send 条件
→ 闭包捕获的 Arc<Mutex<i32>> 是 Send
→ thread::spawn 放行 ✅
完整路径一览:
rust
起点:不共享,直接 move ← 只需要 Send ✅
共享尝试 1:Rc<Cell<i32>>
→ 编译器:"Rc 不是 Send,考虑用 Arc" ← Rc 的引用计数非原子 ❌
共享尝试 2:Arc<Cell<i32>>
→ 编译器:"Arc 的 Send 要求 T: Sync" ← Cell 的 !Sync ❌
→ 7 层检查链追踪到根源:Cell::set 用普通赋值
共享尝试 3:Arc<Mutex<i32>>
→ Mutex 是 Sync → Arc 放行 → spawn 放行 ← ✅ 最终正确方案
每一步的错误信息都精确地告诉你哪一层断了 ------先是 Rc 本身不行,然后是 Cell 的内部可变性不安全。你跟着编译器的提示,一步步从 Rc 走到 Arc,再从 Arc 走到 Mutex。这就是 Rust 的学习曲线------不是"你猜该用哪个",而是"编译器告诉你哪里不对,告诉你下一步往哪走"。
七、跨线程可变性的工具:Mutex、RwLock、Atomic
有了 Send 和 Sync 来检查类型是否线程安全,我们还需要具体的工具来实现线程安全的可变性。
7.1 Mutex------互斥锁保护的数据
rust
use std::sync::Mutex;
let counter = Mutex::new(0);
// 加锁并访问数据
{
let mut data = counter.lock().unwrap();
*data += 1;
// data 离开作用域时自动解锁
}
println!("{}", *counter.lock().unwrap()); // 1
关键点:
-
Mutex::lock()返回MutexGuard<T>------一个智能指针,实现了DerefMut<Target=T> -
MutexGuard在Drop时自动解锁------不可能忘记 unlock -
lock()返回Result<MutexGuard<T>, PoisonError>------如果其他线程在持有锁时 panic,Mutex被标记为"毒化"(poisoned)
多线程共享 Mutex:
rust
use std::sync::{Mutex, Arc};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut data = c.lock().unwrap();
*data += 1;
}); // 线程结束时 MutexGuard 被 drop → 自动解锁
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
println!("结果: {}", *counter.lock().unwrap()); // 10
自动解锁:
rust
fn example() {
let lock = Mutex::new(vec![1, 2, 3]);
{
let mut guard = lock.lock().unwrap();
guard.push(4);
// guard 在这里被 drop → unlock
}
// 另一个线程可以获取锁了
let mut guard = lock.lock().unwrap();
guard.push(5);
// 函数结束时 guard 被 drop → unlock
}
不存在"忘记 unlock"------因为 Drop 保证执行。
中毒(Poison)机制:
如果持有锁的线程 panic 了,Mutex 被标记为 poisoned:
rust
use std::sync::Mutex;
let m = Mutex::new(42);
// 线程 1:持有锁时 panic
let handle = std::thread::spawn(move || {
let _guard = m.lock().unwrap();
panic!("出错了"); // Mutex 被毒化
});
handle.join().is_err(); // true
// 后续尝试 lock() 会返回 Err
// 可以用 poisoned.into_inner() 强制获取内部值
中毒机制防止了"一个线程崩溃后其他线程读取损坏数据"的场景。
7.2 RwLock------读写锁
当数据的读操作远多于写操作时,RwLock 比 Mutex 更高效------它允许多个读者同时访问:
rust
use std::sync::RwLock;
let config = RwLock::new(Config::default());
// 多个线程可以同时读
fn read_config(config: &RwLock<Config>) -> Config {
config.read().unwrap().clone()
}
// 写操作是独占的
fn write_config(config: &RwLock<Config>, new: Config) {
*config.write().unwrap() = new;
}
基本用法:
rust
use std::sync::RwLock;
let data = RwLock::new(vec![1, 2, 3]);
// 读操作------可以多个线程同时
{
let r = data.read().unwrap();
println!("{}", r.len()); // 3
} // r 被 drop,读锁释放
// 写操作------独占
{
let mut w = data.write().unwrap();
w.push(4);
} // w 被 drop,写锁释放
Mutex vs RwLock 选择:
读多写少 → RwLock(多个读者可同时)
读写相当 → Mutex(更简单,开销更低)
写多读少 → Mutex
Mutex 在内部实现上比 RwLock 简单,所以当读写频率差不多时,Mutex 通常更快。只有在读远多于写(如缓存、配置)时才应该使用 RwLock。
7.3 原子类型 ------ AtomicBool / AtomicU32 / AtomicUsize
当你要保护的只是一个简单的整型或布尔值时,Mutex<i32> 的开销太大了。此时使用原子类型------它们直接编译为 CPU 原子指令:
rust
use std::sync::atomic::{AtomicU32, Ordering};
// 原子计数器------比 Mutex<u32> 快得多
let counter = AtomicU32::new(0);
counter.fetch_add(1, Ordering::SeqCst);
基本类型:
| 原子类型 | 对应的普通类型 |
|---|---|
AtomicBool |
bool |
AtomicU8 / AtomicI8 |
u8 / i8 |
AtomicU32 / AtomicI32 |
u32 / i32 |
AtomicU64 / AtomicI64 |
u64 / i64 |
AtomicUsize / AtomicIsize |
usize / isize |
AtomicPtr<T> |
*mut T |
常用方法:
rust
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let counter = Arc::new(AtomicU32::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(std::thread::spawn(move || {
c.fetch_add(1, Ordering::Relaxed); // 原子递增
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", counter.load(Ordering::Relaxed)); // 10
常用方法:
-
load(order):读取值 -
store(val, order):写入值 -
fetch_add(n, order):原子加 -
fetch_sub(n, order):原子减 -
swap(val, order):交换 -
compare_exchange(prev, next, ok_order, fail_order):CAS
Rust 的原子操作特点:
-
原子类型是
Sync的,可以安全地跨线程共享 -
Ordering是枚举,编译期检查,不会拼写错误 -
不允许"普通赋值 + 原子操作"混用------
counter = 5对原子类型是编译错误
什么时候用 Mutex,什么时候用 Atomic:
rust
// Atomic ------ 只需对单个整数/布尔做简单操作
let flag = AtomicBool::new(true);
flag.store(false, Ordering::Release);
// Mutex ------ 需要保护复杂数据或需要一致性读-改-写
let config = Mutex::new(HashMap::new());
let mut map = config.lock().unwrap();
map.insert("key", "value");
| 场景 | 用 |
|---|---|
| 计数器 | AtomicU32 / AtomicU64 |
| 标志位(开关、停止信号) | AtomicBool |
| 指针的原子交换 | AtomicPtr<T> |
| 复杂数据的互斥访问 | Mutex<T> / RwLock<T> |
八、完整全景图
8.1 概念分层
rust
┌─────────────────────────────────────────────────────────┐
│ 可变性全景图 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 基础层:默认不可变 + let mut │
│ └─ 编译器保证:不可变的数据永远不会被修改 │
│ │
│ 2. 外部可变性:&T(只读) vs &mut T(独占读写) │
│ └─ 编译期借用检查,零运行时开销 │
│ │
│ 3. 内部可变性:在 &T 下修改 │
│ ├─ 单线程:Cell<T>(Copy)、RefCell<T>(运行检查) │
│ └─ 多线程:Mutex<T>、RwLock<T>、AtomicT │
│ │
│ 4. 跨线程安全标记(两个正交的 auto trait) │
│ ├─ Send:所有权可以跨线程转移 │
│ ├─ Sync:&T 的跨线程访问是否安全 │
│ └─ 两者独立,四种组合皆有可能 │
│ │
│ 5. 共享所有权(Arc = Send + Sync 的连接点) │
│ ├─ 单线程:Rc<T>(非原子引用计数) │
│ └─ 多线程:Arc<T>(要求 T: Send + Sync 才能放行) │
│ │
└─────────────────────────────────────────────────────────┘
8.2 类型特性速查
| 类型 | 线程域 | 可变性方式 | 检查时机 | T 约束 | 线程安全标记 |
|---|---|---|---|---|---|
&mut T |
单线程 | 外部 | 编译期 | 任意 | Send |
Cell<T> |
单线程 | 内部(取值/赋值) | 无需检查 | Copy | !Sync |
RefCell<T> |
单线程 | 内部(运行时借用) | 运行时 | 任意 | !Sync |
Mutex<T> |
多线程 | 内部(锁保护) | 运行时 | 任意 | Sync |
RwLock<T> |
多线程 | 内部(读写锁) | 运行时 | 任意 | Sync |
| 原子类型 | 多线程 | 内部(原子指令) | 无需检查 | 特定类型 | Sync |
8.3 完整选型流程
swift
需要可变性?
│
├── 编译期可确定?
│ └── ✅ → &mut T(最佳:编译期检查,零开销)
│
└── 需在 &self(共享引用)下修改?
│
├── 单线程 →
│ ├── T: Copy → Cell<T>(零运行时开销)
│ ├── T: !Copy → RefCell<T>(运行时借用检查)
│ └── 需共享所有权?→ Rc<Cell/RefCell<T>>
│
└── 多线程 →
├── 简单整数/布尔 → AtomicU32 / AtomicBool
├── 复杂数据 →
│ ├── 读多写少 → RwLock<T>
│ └── 读写相当 → Mutex<T>
└── 需共享所有权?→ Arc<Mutex/RwLock<T>>
九、总结
一条逻辑链
Rust 的可变性体系可以用一条逻辑链来理解:
rust
默认不可变(let)→ 从根本上消除意外写入
↓
外部可变性(&T / &mut T)→ 编译期借用检查,零开销
↓
内部可变性(Cell / RefCell)→ 在 &self 下受控修改
↓
跨线程安全:两个正交的概念
├── Send(所有权可以跨线程转移)
└── Sync(&T 的跨线程修改必须安全)
↓
Arc 是连接点 ------ 要求 T: Send + Sync
↓
Mutex / RwLock / Atomic → 跨线程安全的可变工具
95% 规则
实际代码中使用频率大致如下:
erlang
95% 的代码:&mut T → 编译期检查,零运行时开销
5% 的代码:Cell → 零运行时开销(仅限 Copy 类型)
<1% 的代码:RefCell / Mutex / Atomic → 有运行时开销
优先使用编译期可检查的方案 (&mut T),只有在确实需要时才退回到运行时方案。
核心思想
Rust 把安全责任交给编译器------你声明你的意图,我来检查你的实现。
这种转变的代价是学习曲线------理解 let mut、&T vs &mut T、Cell vs RefCell、Send vs Sync、Mutex vs Atomic。但回报是:编译器能发现的 bug,就不会在凌晨三点把你叫醒。