bigywater.tech
Rust · 30

Arc<T> 与跨线程共享所有权

上一篇的 Rc 不能跨线程,Arc 就是它的线程安全版本。这篇验证 Arc 能做什么、不能做什么、对 T 有什么要求,并量一量「原子计数」到底贵不贵 —— 结果是:不争用时几乎不贵,争用时非常贵。

E0594
Arc 里的数据不可变,要修改得配合 Mutex
40000
4 线程 × 10000 次,Arc> 的最终值
≈ 34 倍
4 线程共用一个 Arc 克隆与各用各的相比的慢速倍数

跨线程共享

实测 · 本机执行输出
  源码:
      1 | use std::sync::Arc;
      2 | use std::thread;
      3 | fn main() {
      4 |     let data = Arc::new(vec![1, 2, 3, 4, 5, 6, 7, 8]);
      5 |     let mut hs = vec![];
      6 |     for i in 0..4 {
      7 |         let d = Arc::clone(&data);
      8 |         hs.push(thread::spawn(move || { let s: i32 = d[i * 2..i * 2 + 2].iter().sum(); (i, s) }));
      9 |     }
     10 |     for h in hs { let (i, s) = h.join().unwrap(); println!("线程 {i} 求和 = {s}"); }
     11 |     println!("全部线程结束后 strong_count = {}", Arc::strong_count(&data));
     12 | }
  编译:成功
  运行(退出码 0):
    线程 0 求和 = 3
    线程 1 求和 = 7
    线程 2 求和 = 11
    线程 3 求和 = 15
    全部线程结束后 strong_count = 1

Arc 与 Mutex

实测 · 本机执行输出
  源码:
      1 | use std::sync::Arc;
      2 | fn main() { let a = Arc::new(5); *a += 1; }
  编译:失败
    error[E0594]: cannot assign to data in an `Arc`
     --> <实验目录>/src/u30b.rs:2:34
      |
    2 | fn main() { let a = Arc::new(5); *a += 1; }
      |                                  ^^^^^^^ cannot assign
      |
      = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Arc<i32>`
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0594`.
  源码:
      1 | use std::sync::{Arc, Mutex};
      2 | use std::thread;
      3 | fn main() {
      4 |     let counter = Arc::new(Mutex::new(0u64));
      5 |     let hs: Vec<_> = (0..4).map(|_| { let c = Arc::clone(&counter); thread::spawn(move || { for _ in 0..10_000 { *c.lock().unwrap() += 1; } }) }).collect();
      6 |     for h in hs { h.join().unwrap(); }
      7 |     println!("4 个线程各加 10000 次,counter = {}(期望 40000)", *counter.lock().unwrap());
      8 | }
  编译:成功
  运行(退出码 0):
    4 个线程各加 10000 次,counter = 40000(期望 40000)

Mutex 的细节见Mutex 与 RwLock。

Send + Sync 要求

实测 · 本机执行输出
  源码:
      1 | use std::cell::Cell;
      2 | use std::sync::Arc;
      3 | use std::thread;
      4 | fn main() {
      5 |     let a = Arc::new(Cell::new(1));
      6 |     let b = Arc::clone(&a);
      7 |     thread::spawn(move || b.set(2)).join().unwrap();
      8 | }
  编译:失败
    error[E0277]: `Cell<i32>` cannot be shared between threads safely
       --> <实验目录>/src/u30d.rs:7:19
        |
      7 |     thread::spawn(move || b.set(2)).join().unwrap();
        |     ------------- ^^^^^^^^^^^^^^^^ `Cell<i32>` cannot be shared between threads safely
        |     |
        |     required by a bound introduced by this call
        |
        = help: the trait `Sync` is not implemented for `Cell<i32>`
        = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicI32` instead
        = note: required for `Arc<Cell<i32>>` to implement `Send`
    note: required because it's used within this closure
       --> <实验目录>/src/u30d.rs:7:19
        |
      7 |     thread::spawn(move || b.set(2)).join().unwrap();
        |                   ^^^^^^^
    note: required by a bound in `spawn`
       --> <rust 标准库源码>/library/std/src/thread/functions.rs:128:8
        |
    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`
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0277`.

克隆的代价

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    Rc::clone + drop                          每次约 3.98 ns
    Arc::clone + drop(单线程)                每次约 3.98 ns
    4 个线程各克隆 2000000 次,共用同一个 Arc:    墙钟 272 ms
    4 个线程各克隆 2000000 次,各用自己的 Arc:    墙钟 8 ms
    共用同一个计数器比各用各的慢 34.1 倍(我的推断:同一个计数器所在的缓存行在 4 个核之间来回传递,没有用性能计数器验证)

原子操作与内存顺序见原子类型与内存顺序。

分步演示

Arc<T>
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。计时为 release 构建的单次运行,机器为 Apple 芯片(原子操作的代价在其它 CPU 上可能不同)
    • 没有验证:Arc 在 x86 上无争用的开销、Arc::make_mut / get_mut / try_unwrap、Arc、克隆后立即 drop 是否会被编译器优化掉(使用了 black_box)、争用慢速的硬件原因
    京ICP备2026067502号-1京公网安备11010502063487号