Arc 与多线程共享
本教程共 78 篇 · 第 68 篇 · 更新于 2026-08-08 · 约 7 分钟阅读
本节目标:理解 Arc 是线程安全的引用计数智能指针,知道它和 Rc 的关系与区别,掌握用 Arc 让多个线程共享同一份数据,并记住”Arc 只读,要改得配 Mutex”这条铁律。
Rust 的所有权规则规定,一个值同一时刻只能有一个所有者。这在大多数情况下没问题,但遇到”多个线程都要用同一份数据”时,就卡住了。第 67 章的预览里,我们正是靠 Arc 才让十个线程共享同一个计数器。这一章把 Arc 彻底讲清楚。
1-1 引用计数解决”多个所有者”
引用计数(reference counting)的思路很直接:给一份数据记个数,记录现在有几个地方正在用它。当这个数降到 0,说明没人用了,数据就可以安全释放。
Rust 提供两个做这件事的智能指针:Rc 用于单线程,Arc 用于多线程。它们的 API 几乎一模一样,区别只在底层的计数方式是不是原子的(atomic)。
use std::sync::Arc;
use std::thread;
fn main() {
let s = Arc::new(String::from("多线程漫游者"));
for _ in 0..10 {
let s = Arc::clone(&s);
let handle = thread::spawn(move || {
println!("{}", s);
});
// 这里省略 join,仅为演示 clone 到线程
}
}
Arc::new 创建智能指针,Arc::clone 克隆出一份——注意这里的 clone 只是复制了指针并让计数加一,底层的字符串数据并没有被复制,所以效率很高。推荐用 Arc::clone(&s) 而不是 s.clone(),可读性更好,一眼能看出这是引用计数克隆。
1-2 为什么 Rc 不能用于多线程
你可能会问:单线程的 Rc 便宜又好用,干嘛还要整个 Arc?答案藏在并发安全里。
如果你把 Rc 丢进线程,编译器会直接报错:Rc<String> cannot be sent between threads safely。表面原因是 Rc 没实现 Send(第 69 章会细讲这个 trait),更深层的原因是:Rc 的计数器不是线程安全的。两个线程同时 clone,计数可能只加了一次,导致释放时机算错,最后内存泄漏甚至重复释放。这种 bug 极难复现。
Arc 的全称是 Atomic Rc,原子化的 Rc。它用原子操作来维护计数,保证多个线程同时增减计数也不会出错。代价是每次计数变动都有一点点性能开销,但在需要线程安全的场景,这点开销完全值得。
Note
Arc在std::sync模块下(use std::sync::Arc),而Rc在std::rc模块下。导入路径不同,别搞混。
1-3 Arc 是只读的
一个关键认知:Arc<T> 指向的数据本身是不可变的。你通过 Arc 能读,但不能直接改。这其实和借用规则一致——要么多个不可变借用,要么一个可变借用。Arc 提供的是多个只读的共享视图。
那要是多个线程都想改同一份数据怎么办?单独用 Arc 做不到,得把它和能提供内部可变性的类型配合,最常见的就是上一章讲的 Mutex:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("最终计数: {}", *counter.lock().unwrap());
}
Arc 负责”多份引用指向同一份数据”,Mutex 负责”同一时刻只放一个线程进去改”。两者分工明确:Arc 管共享,Mutex 管可变。这是 Rust 多线程共享可变状态的标准写法。
1-4 Arc 引用计数的生命周期
初学时容易忽略一点:Arc 的计数在编译期就决定了资源的回收时机。当最后一个 Arc 离开作用域,计数归零,底层数据连同智能指针一起被释放。你不需要手动 free,也不必担心忘记清理。
来看计数的变化过程:
use std::sync::Arc;
fn main() {
let a = Arc::new(String::from("计数演示"));
println!("创建后: {}", Arc::strong_count(&a)); // 1
let b = Arc::clone(&a);
println!("clone 后: {}", Arc::strong_count(&a)); // 2
{
let c = Arc::clone(&a);
println!("再 clone 后: {}", Arc::strong_count(&c)); // 3
} // c 离开作用域,计数回到 2
println!("c 走后: {}", Arc::strong_count(&a)); // 2
}
Arc::strong_count 能随时查看当前计数。变量离开作用域时计数自动减一,全靠 Arc 实现了 Drop。
1-5 什么时候该用 Arc
Arc 不是默认选择。Rust 把”要不要线程安全”的决定权交给你,因为原子操作有开销,而大部分程序其实跑在单线程里,用 Rc 就够了。
只有在以下情况才需要 Arc:
- 多个线程需要共享同一份数据(比如配置、缓存、连接池)。
- 配合
Mutex/RwLock做跨线程的可变共享。 - 配合
channel把数据发给多个消费者,又想避免拷贝大对象。
如果只是把数据”交出去”给一个线程,用第 65 章讲的 move 转移所有权就行,根本用不上 Arc。
Warning
Arc解决的是”共享”,解决不了”同时改”。千万别以为有了Arc就能随意改数据——不加锁地通过Arc改共享状态,依然是数据竞争。记住组合拳:Arc<Mutex<T>>或Arc<RwLock<T>>。
1-6 Arc 与 RwLock 的读多写少场景
前面组合拳用的是 Arc<Mutex<T>>。如果多个线程主要是读、偶尔才写(比如一份全局配置、一张路由表),用 Mutex 会让所有读者也排成一队,白白损失并发。这时该换成 Arc<RwLock<T>>:RwLock 允许多个线程同时拿到读锁,只有写的时候才独占。
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let config = Arc::new(RwLock::new(String::from("debug")));
let mut handles = vec![];
for _ in 0..4 {
let c = Arc::clone(&config);
handles.push(thread::spawn(move || {
let level = c.read().unwrap(); // 多个读者可同时读
println!("{}", level);
}));
}
for h in handles { h.join().unwrap(); }
}
read() 拿到读锁,write() 拿到写锁。读锁之间不互斥,写锁与任何锁互斥。这样读多的场景吞吐更高。RwLock 在底层也依赖 Send/Sync 来保证线程安全,原理和 Mutex 一脉相承。
一句话总结选型:Arc<Mutex<T>> 通用,适合读写都频繁或写多的共享;Arc<RwLock<T>> 适合读多写少。无论哪种,Arc 只解决”共享”,锁解决”可变”,分工不变。
1-7 一个易错点:误以为 Arc 能改数据
初学者常犯一个错:拿到 Arc<T> 就想直接改里面的 T,编译报错后才反应过来”Arc 是只读的”。记住,Arc 只负责共享,不提供可变性。要改,必须有 Mutex/RwLock 这一层。还有个常见错:在单线程场景硬用 Arc,白白付出原子计数的开销——单线程用 Rc 更轻。区分清楚”要不要跨线程”和”要不要改”,就能选对 Rc/Arc 与 Cell/RefCell/Mutex 的组合。
Tip一张速查表:单线程只读共享用
Rc;单线程要改共享用Rc<RefCell<T>>;多线程只读共享用Arc;多线程要改共享用Arc<Mutex<T>>或Arc<RwLock<T>>。照着这张表选,基本不会错。
1-8 小结
Arc 是线程安全的引用计数智能指针,让多份引用共享同一份数据,计数原子化保证并发安全。Rc 不能进线程,是因为它的计数不是原子的。记住两点:Arc 本身只读,要改得配 Mutex(或 RwLock);只有真正需要跨线程共享时才用它,单线程用 Rc 更轻量。下一章我们上升到规则层面,看看 Rust 是怎么从类型系统上保证”什么能跨线程”的——这就是 Send 和 Sync。