首页 / Rust 入门教程 / Arc 与多线程共享

Rust 入门教程

Arc 与多线程共享

本教程共 78 篇 · 第 68 篇 · 更新于 2026-08-08 · 约 7 分钟阅读

RustRust 入门教程Arc引用计数多线程智能指针共享所有权

本节目标:理解 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

Arcstd::sync 模块下(use std::sync::Arc),而 Rcstd::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/ArcCell/RefCell/Mutex 的组合。

Tip

一张速查表:单线程只读共享用 Rc;单线程要改共享用 Rc<RefCell<T>>;多线程只读共享用 Arc;多线程要改共享用 Arc<Mutex<T>>Arc<RwLock<T>>。照着这张表选,基本不会错。

1-8 小结

Arc 是线程安全的引用计数智能指针,让多份引用共享同一份数据,计数原子化保证并发安全。Rc 不能进线程,是因为它的计数不是原子的。记住两点:Arc 本身只读,要改得配 Mutex(或 RwLock);只有真正需要跨线程共享时才用它,单线程用 Rc 更轻量。下一章我们上升到规则层面,看看 Rust 是怎么从类型系统上保证”什么能跨线程”的——这就是 SendSync