Rust in Everything
基于 crossbeam-epoch 0.9.21

Atomic、Owned、Shared

主线位置:回收主线里「Atomic::swap 拿到旧的 Shared」这一步用到的类型。这一章不涉及纪元,只讲指针本身:谁拥有、能活多久、低位里藏了什么。

flowchart LR
  owned["Owned<T>:独占,相当于 Box"] -->|"into_shared(&guard)"| shared["Shared<'g, T>:只在 guard 存活期间有效"]
  owned -->|"store / swap / compare_exchange"| atomic["Atomic<T>:共享的原子指针"]
  atomic -->|"load(ord, &guard)"| shared
  shared -->|"unsafe into_owned"| owned
  shared -->|"guard.defer_destroy"| bag["交给 epoch 回收"]

三个类型都在 atomic.rs,1713 行,是 crate 里最大的文件;其中大半是文档和例子,真正的数据只有一个 usize。

三个类型,同一个 usize

// crossbeam-epoch/src/atomic.rs
pub struct Atomic<T: ?Sized + Pointable> {
    data: AtomicUsize,
    _marker: PhantomData<*mut T>,
}

pub struct Owned<T: ?Sized + Pointable> {
    data: usize,
    _marker: PhantomData<Box<T>>,
}

pub struct Shared<'g, T: 'g + ?Sized + Pointable> {
    data: usize,
    _marker: PhantomData<(&'g (), *const T)>,
}

(Atomic 第 293 行、Owned 第 1041 行、Shared 第 1290 行)

三者存的都是「指针 + 低位标签」打包成的一个 usize,区别全在类型上:

类型所有权Drop能否跨线程典型来源
Atomic<T>共享位置,本身不拥有指向的对象不释放指向的对象T: Send + Sync 时 Send + Sync数据结构的字段
Owned<T>独占,相当于 Box<T>释放(T::drop,第 1193 行)随 Box<T>Owned::new(v)
Shared<'g, T>借来的,Copy什么都不做否(含 *const T)atomic.load(ord, &guard)

Atomic 的 Drop 不释放对象,这是有意的:一个 Atomic 被 drop 时,对象可能还被别的线程读着,该不该释放只有数据结构自己知道。所以数据结构的 Drop 里通常要手动 into_owned(),而且那时已经没有并发访问,可以用 unprotected()(pin 与 Guard)。

Shared 的生命周期绑在 Guard 上

从 Atomic 读出来的指针一律是 Shared<'g, T>,'g 来自传进去的 &'g Guard(load 第 375 行):

pub fn load<'g>(&self, ord: Ordering, _: &'g Guard) -> Shared<'g, T> {
    unsafe { Shared::from_usize(self.data.load(ord)) }
}

guard 参数在函数体里根本没用到,它只是为了把 'g 传给返回值。效果是:Shared 不能活得比 guard 长,而 guard 活着就意味着当前线程钉在某个纪元上、读到的对象不会被回收。想把读到的指针带出 pin 的范围,编译器会拒绝。实测(exp-crossbeam/shared-outlives-guard/):

fn leak<'a>(a: &'a Atomic<u64>) -> Shared<'a, u64> {
    let guard = epoch::pin();
    a.load(Ordering::Acquire, &guard)
}
error[E0515]: cannot return value referencing local variable `guard`
 --> src/main.rs:8:3
  |
8 |   a.load(Ordering::Acquire, &guard)
  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^------^
  |   |                         |
  |   |                         `guard` is borrowed here
  |   returns a value referencing data owned by the current function

这是 crossbeam-epoch 唯一由编译器替你检查的安全条件。其余的都还是 unsafe:

  • Shared::deref / as_ref 是 unsafe:编译器能保证 guard 还在,但保证不了这个指针当初是被正确发布的(比如读用的是 Relaxed,而写者用 Relaxed 发布,读到的对象内容可能还没写好)。deref 的文档专门举了这个例子。
  • Shared::into_owned 是 unsafe:要求「指针有效,且没有别人持有对它的引用」(第 1524 行)。
  • Guard::defer_destroy 是 unsafe:要求对象已经对其他线程不可达(下一章和 defer 与回收)。

Owned 变成 Shared 也要经过 guard:Owned::into_shared(self, &'g Guard) -> Shared<'g, T>(第 1156 行),所有权从此交给「数据结构 + epoch」。

写操作:Pointer trait

store、swap、compare_exchange 的新值参数是泛型 P: Pointer<T>,Owned 和 Shared 都实现了它(into_usize / from_usize,第 1022 行)。传 Owned 表示「把这块新内存交出去」,传 Shared 表示「指向一个已经共享的对象」。

compare_exchange(第 478 行)失败时把新值原样还给调用方:

pub struct CompareExchangeError<'g, T: ?Sized + Pointable, P: Pointer<T>> {
    /// The value in the atomic pointer at the time of the failed operation.
    pub current: Shared<'g, T>,

    /// The new value, which the operation failed to store.
    pub new: P,
}

如果新值是 Owned,CAS 失败后调用方拿回 Owned,可以下一轮重试,不会泄漏也不会重复分配。Treiber 栈的 push 就是这么写的:Err(e) => n = e.new。

swap 拿到的旧值是 Shared,不是 Owned:被换下来的对象可能还有读者,所以不能直接拿到所有权释放,只能交给 defer_destroy。回收主线的第三步就在这里。

低位标签

对象按 align_of::<T>() 对齐,地址的最低几位一定是 0,crossbeam 把这几位当标签用(第 104–130 行):

fn low_bits<T: ?Sized + Pointable>() -> usize {
    (1 << T::ALIGN.trailing_zeros()) - 1
}

fn compose_tag<T: ?Sized + Pointable>(data: usize, tag: usize) -> usize {
    (data & !low_bits::<T>()) | (tag & low_bits::<T>())
}

可用位数完全由对齐决定:u64(对齐 8)有 3 位,u32 有 2 位,u8 一位都没有。超出的部分被静默截掉,不报错。实测(exp-crossbeam/epoch-rss/ 的 tags):

Atomic<u64>: with_tag(9) -> tag 1 value 7
Atomic<u8>: with_tag(1) -> tag 0

9 的二进制是 1001,只留下低 3 位 001。所以用标签前要确认 T 的对齐够用;crate 内部用标签的地方,指向的类型都是至少按指针对齐的结构体。

标签主要用来在同一个原子字里同时表达「指向谁」和「一个状态」,一次 CAS 同时检查两者:

  • epoch 内部的参与者链表用 next 指针的第 0 位表示「本节点已删除」,删除就是 self.next.fetch_or(1, Release, guard)(内部的无锁链表和队列)。
  • skiplist 用同样的办法给塔上的每一层打删除标记(并发跳表)。

fetch_and / fetch_or / fetch_xor(第 792–838 行)的参数会先 & low_bits,只作用在标签位上,不会改坏地址。

Pointable:一个字指向一个变长数组

原子操作只能作用于一个字,所以能放进 Atomic 的类型必须「用一个字就能指向」。Box<[T]> 是胖指针(地址 + 长度两个字),不行。Pointable 把这件事抽象出来:

成员作用
const ALIGN指针的对齐,决定标签位数
type Init初始化参数:对 T 是值本身,对数组是长度
unsafe fn init / deref / deref_mut / drop用一个 usize 完成分配、访问、释放

两个实现:

  • 任意 T:就是 Box::into_raw / Box::from_raw(第 194 行)。
  • [MaybeUninit<T>]:分配一块「len + 元素」连在一起的内存,指针指向开头,长度存在堆上而不是指针里(Array<T>,第 236 行起)。Owned::<[MaybeUninit<u32>]>::init(10) 得到一个长度 10 的切片,实测 len() 为 10。

这项支持来自 2020 年的 #452(「Support dynamically sized types」),PR 说明强调它「没有引入另一个 Atomic 类型」,只是把约束从 T: Sized 换成 T: Pointable。

load_consume

Atomic::load_consume(第 401 行)调的是 crossbeam-utils 的 AtomicConsume。在 ARM 和 AArch64 上(且不在 Miri、loom、TSan 下)它是 load(Relaxed) 加 compiler_fence(Acquire),靠硬件的地址依赖保证顺序,省掉一条屏障指令;其他平台退回 load(Acquire)。本机 M2 是 AArch64,走前一种。为什么只在这两种架构上省、为什么 loom 和 TSan 下要退回,见 本库里的内存序。

版本里的小变化

版本变化
0.9.8Atomic::null() 在 1.61+ 上是 const fn(#797)。loom 下仍不是(const_fn! 宏,第 340 行),因为 loom 的原子类型不能在 const 里构造
0.9.19修 fetch_update 的返回值(#1197);fmt::Pointer 对空指针解引用的问题(#1273)
0.9.20fmt::Pointer 对无效指针仍会解引用,彻底改成只打印地址(#1276)。现在的实现见第 1656 行
0.9.21Shared::null() 改成 const fn(#1227)

compare_and_set / compare_and_set_weak 和 CompareAndSetOrdering 已经标为 deprecated,留着只为兼容,新代码用 compare_exchange(两个 Ordering 参数,和标准库一致)。

小结

  • Atomic、Owned、Shared 都只存一个「指针 + 低位标签」的 usize,区别在类型:Owned 独占、drop 时释放;Atomic 共享、drop 时不释放;Shared 是 Copy 的借用。
  • Shared<'g, T> 的 'g 来自 &'g Guard,编译器保证它不会活过 pin 的范围(实测 E0515)。解引用、取回所有权、延迟销毁仍然是 unsafe,条件要调用方自己保证。
  • compare_exchange 失败会把 Owned 新值还回来;swap 换下的旧值是 Shared,只能交给 defer_destroy。
  • 标签位数由 T 的对齐决定,超出的位被静默截掉(with_tag(9) 在 u64 上变成 1)。
  • Pointable 让 [MaybeUninit<T>] 也能用一个字指向:长度存在堆上。

💬 关联讨论

发起讨论
加载中...