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.8 | Atomic::null() 在 1.61+ 上是 const fn(#797)。loom 下仍不是(const_fn! 宏,第 340 行),因为 loom 的原子类型不能在 const 里构造 |
| 0.9.19 | 修 fetch_update 的返回值(#1197);fmt::Pointer 对空指针解引用的问题(#1273) |
| 0.9.20 | fmt::Pointer 对无效指针仍会解引用,彻底改成只打印地址(#1276)。现在的实现见第 1656 行 |
| 0.9.21 | Shared::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>]也能用一个字指向:长度存在堆上。