bigywater.tech
Rust · 20

多个不可变借用为什么可以共存

「可以有任意多个 &T」这条规则背后的原因是:只读不会互相干扰。这篇从三个角度验证它:引用本身是 Copy、多线程并发读真的能跑出正确结果、以及一旦引入「共享 + 修改」(Cell),编译器马上用 Sync 把它拦住。

Copy
&T 本身是 Copy,可以随便复制
500000500000
4 个线程共享读一个 Vec 得到的总和(正确)
E0277
&Cell 跨线程:Cell 不是 Sync

多个 &T

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let v = vec![1, 2, 3];
      3 |     let a = &v; let b = &v; let c = a;          // 复制一个引用
      4 |     println!("a、b、c 指向同一个 Vec:{}", std::ptr::eq(a, b) && std::ptr::eq(b, c));
      5 |     println!("len:{} {} {}", a.len(), b.len(), c.len());
      6 |     fn is_copy<T: Copy>(_: &T) {}
      7 |     is_copy(&a);
      8 |     println!("&Vec<i32> 是 Copy(上面的 is_copy 调用编译通过)");
      9 | }
  编译:成功
  运行(退出码 0):
    a、b、c 指向同一个 Vec:true
    len:3 3 3
    &Vec<i32> 是 Copy(上面的 is_copy 调用编译通过)

多线程只读

实测 · 本机执行输出
  源码:
      1 | use std::thread;
      2 | fn main() {
      3 |     let data: Vec<u64> = (1..=1_000_000).collect();
      4 |     let chunks: Vec<&[u64]> = data.chunks(250_000).collect();
      5 |     let total: u64 = thread::scope(|s| {
      6 |         let hs: Vec<_> = chunks.iter().map(|c| s.spawn(move || c.iter().sum::<u64>())).collect();
      7 |         hs.into_iter().map(|h| h.join().unwrap()).sum()
      8 |     });
      9 |     println!("4 个线程各读 25 万个元素,共享同一个 Vec,总和 = {}(期望 {})", total, 1_000_000u64 * 1_000_001 / 2);
     10 | }
  编译:成功
  运行(退出码 0):
    4 个线程各读 25 万个元素,共享同一个 Vec,总和 = 500000500000(期望 500000500000)

Send 与 Sync 的定义见Send 与 Sync。

共享加修改

实测 · 本机执行输出
  源码:
      1 | use std::cell::Cell;
      2 | use std::thread;
      3 | fn main() {
      4 |     let c = Cell::new(0);
      5 |     thread::scope(|s| {
      6 |         s.spawn(|| c.set(c.get() + 1));
      7 |         s.spawn(|| c.set(c.get() + 1));
      8 |     });
      9 | }
  编译:失败
    error[E0277]: `Cell<i32>` cannot be shared between threads safely
       --> <实验目录>/src/u20c.rs:6:17
        |
      6 |         s.spawn(|| c.set(c.get() + 1));
        |           ----- ^^^^^^^^^^^^^^^^^^^^^ `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 `&Cell<i32>` to implement `Send`
    note: required because it's used within this closure
       --> <实验目录>/src/u20c.rs:6:17
        |
      6 |         s.spawn(|| c.set(c.get() + 1));
        |                 ^^
    note: required by a bound in `Scope::<'scope, 'env>::spawn`
       --> <rust 标准库源码>/library/std/src/thread/scoped.rs:203:28
        |
    201 |     pub fn spawn<F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T>
        |            ----- required by a bound in this associated function
    202 |     where
    203 |         F: FnOnce() -> T + Send + 'scope,
        |                            ^^^^ required by this bound in `Scope::<'scope, 'env>::spawn`
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0277`.

需要「共享 + 修改」时用 Mutex、RwLock 或原子类型,见Mutex 与 RwLock。

读者在场时写者被拒绝

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let mut v = vec![1, 2, 3];
      3 |     let r1 = &v; let r2 = &v;
      4 |     v.push(4);
      5 |     println!("{:?} {:?}", r1, r2);
      6 | }
  编译:失败
    error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
     --> <实验目录>/src/u20d.rs:4:5
      |
    3 |     let r1 = &v; let r2 = &v;
      |              -- immutable borrow occurs here
    4 |     v.push(4);
      |     ^^^^^^^^^ mutable borrow occurs here
    5 |     println!("{:?} {:?}", r1, r2);
      |                           -- immutable borrow later used here
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0502`.

分步演示

共享借用
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。多线程求和用 release 构建;没有对比与单线程的耗时
    • 没有验证:并行读的加速比、伪共享(false sharing)对只读共享的影响、Arc 共享所有权(见其它文章)
    京ICP备2026067502号-1京公网安备11010502063487号