Skip to content

29.1 Arc

Arc(Atomic Reference Counted, 原子引用计数)是 Rc 的线程安全版本。注意第一个单词代表的是 atomic 而不是 automatic。它强调的是“原子性”。 它跟 Rc 最大的区别在于,其引用计数用的是原子整数类型。

Arc 使用方法示例如下:

rust
use std::sync::Arc;
use std::thread;

fn main() {
    let numbers: Vec<_> = (0..100u32).collect();
    // 引用计数指针,指向一个 Vec
    let shared_numbers = Arc::new(numbers);

    // 循环创建 10 个线程
    for _ in 0..10 {
    // 复制引用计数指针,所有的 Arc 都指向同一个 Vec
        let child_numbers = shared_numbers.clone();
    // move 修饰闭包,上面这个 Arc 指针被 move 进入了新线程中
        thread::spawn(move || {
    // 我们可以在新线程中使用 Arc,读取共享的那个 Vec
            let local_numbers = &child_numbers[..];
    // 继续使用 Vec 中的数据
        });
    }
}

这段代码可以正常编译通过。

如果我们把上面代码中的 Arc 改为 Rc 类型,就会发生下面的编译错误:


rust
error: the trait `std::marker::Send` is not implemented for the type `std::rc::Rc<std::vec::Vec<u32>>`

因为 Rc 类型内部的引用计数是普通整数类型,如果多个线程中分别同时持有指向同一块内存的 Rc 指针,是线程不安全的。 这个错误是通过 spawn 函数的签名检查出来的。spawn 要求闭包参数类型满足 Send 条件,闭包是没有显式 impl Send 或者 Sync 的,按照 auto trait 的推理规则,编译器会检查这个类型所有的成员是否满足 Send 或者 Sync。

目前这个闭包参数“捕获”了一个 Rc 类型,而 Rc 类型是不满足 Send 条件的,因此编译器推理出来这个闭包类型是不满足 Send 条件的,与 spawn 函数的约束条件发生了冲突。

查看源码我们可以发现,Rc 和 Arc 这两个类型之间的区别,除了引用计数值的类型之外,主要如下:


rust
unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}

impl<T: ?Sized> !marker::Send for Rc<T> {}
impl<T: ?Sized> !marker::Sync for Rc<T> {}

编译器的推理过程为:u32 是 Send,得出Unique<u32>是 Send,接着得出Vec<u32>是 Send,然后得出Arc<Vec<u32>>是 Send,最后得出闭包类型是 Send。它能够符合 spawn 函数的签名约束,可以穿越线程边界。如果把共享变量类型变成Cell<u32>,那么Arc<Cell<u32>>依然是不符合条件的。因为 Cell 类型是不满足 Sync 的。

这就是为什么有底气 Rust 给用户提供了两种“引用计数”智能指针。因为用户不可能用错。如果不小心把 Rc 用在了多线程环境,直接是编译错误,根本不会引发多线程同步的问题。如果不小心把 Arc 用在了单线程环境也没什么问题,不会有 bug 出现,只是引用计数增加或减少的时候效率稍微有一点降低。

Released under the MIT License