Rust 可变性完整指南 —— 从默认不可变到多线程安全修改

Rust 可变性完整指南 ------ 从默认不可变到多线程安全

一、引言

程序本质上是对数据的操作。而数据操作最基本的两个问题是:

  1. 谁能修改这份数据?

  2. 多个执行流同时访问时,会发生什么?

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 的类型。它的 getset 都只需要 &self

rust 复制代码
use std::cell::Cell;

  


let c = Cell::new(42);

  


println!("{}", c.get()); // 42

c.set(100); // 通过 &Cell<i32> 修改

println!("{}", c.get()); // 100

关键:setget 都不需要 &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> 在内存中就是一个 i32get/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()

  1. 检查当前借用标志

  2. 如果合法,更新标志

  3. 返回 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 允许 !SyncT(如 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>AtomicU32Arc<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------因为 ArcSend 要求 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 修改时线程安全的类型------MutexRwLock 或原子类型。最简单的方案是 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>

  • MutexGuardDrop 时自动解锁------不可能忘记 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------读写锁

当数据的读操作远多于写操作时,RwLockMutex 更高效------它允许多个读者同时访问:

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 TCell vs RefCellSend vs SyncMutex vs Atomic。但回报是:编译器能发现的 bug,就不会在凌晨三点把你叫醒。

相关推荐
hsg778 小时前
简述:Rust、GeoRust、自主研发GIS平台
开发语言·后端·rust
AOwhisky18 小时前
下一代容器来了?Docker 宣布原生支持 WebAssembly
java·运维·docker·容器·rust·wasm
铅笔侠_小龙虾1 天前
Rust 学习(6)-所有权规则、移动语义、Clone 与 Copy
python·学习·rust
程序员爱钓鱼1 天前
Rust 控制流 if 详解:条件判断与 if 表达式
前端·后端·rust
花褪残红青杏小2 天前
Rust图像处理第19节-RGB 色彩矩阵变换:矩阵 × 向量 = 像素变换
rust·webassembly·图形学
white_ant2 天前
15-ASCII 转换工具使用指南
rust·tauri·watools
铅笔侠_小龙虾2 天前
Rust 学习(2)-变量、常量与 shadowing
学习·算法·rust
独孤留白2 天前
从C到Rust:Mutex / RwLock / Atomic 跨线程局部可变性
rust
程序员爱钓鱼2 天前
Rust 注释与文档注释详解:从代码说明到 cargo doc
后端·rust