AtomicCell 不是 lock-free
主线位置:不在三条调用链上,属于内存序横线的「锁回退」一类。机制和实验 E2 在 AtomicCell。
flowchart TB
t["AtomicCell<T>"] --> q{"size 等于某个 AtomicU8..U64,且 align 不小于它?"}
q -->|是| at["原生原子指令"]
q -->|"否:u128、(u32, u16)、Instant ..."| lock["按 addr % 67 选一把全局 seqlock"]
lock --> share["不相干的 cell 可能共用同一把锁"]
现象
AtomicCell<T> 的名字和 API 都像 AtomicU64,于是被用在信号处理函数、实时线程、或者「不能拿锁」的地方:
static LAST_SEEN: AtomicCell<(u32, u16)> = AtomicCell::new((0, 0));
它能编译、能用,但每次 load / store 都在拿一把全局的 seqlock。不会有任何警告。
实测
实验 E2(exp-crossbeam/atomic-cell-lockfree/,M2 Max,跑 3 次相同)打印 size_of、align_of 和 AtomicCell::<T>::is_lock_free(),摘几行:
u64 size=8 align=8 lock_free=true
u128 size=16 align=16 lock_free=false
[u8;4] size=4 align=1 lock_free=false
(u32,u16) size=8 align=4 lock_free=false
align(8){u32,u16} size=8 align=8 lock_free=true
(u32,u32) size=8 align=4 lock_free=false
Instant size=16 align=8 lock_free=false
Option<u64> size=16 align=8 lock_free=false
Option<NonZeroU64> size=8 align=8 lock_free=true
大小对上了也不够:[u8; 4]、(u32, u16)、(u32, u32) 都是对齐不够;把 (u32, u16) 包进 #[repr(align(8))] 的结构体就变成 lock-free。u128 一律走锁(AtomicU128 不稳定)。Instant 在 macOS 和 Linux x86_64 上都是 16 字节,所以 tick channel 的 AtomicCell<Instant> 走锁(at、tick、never)。
代价有多大(E2 吞吐部分,AtomicCell):
- 单线程无竞争:
u64每次fetch_add约 4.0 ns,u128(锁)约 4.9 ns。 - 两线程争同一个 cell:
u128(锁)反而比u64(原子)快 20%–30%,规划时「锁路径慢一个数量级」的预测不成立。 - 两线程各写自己的
u128cell,但两个 cell 恰好映射到同一把锁:从 25 ms 变成 195–227 ms,慢 8 到 9 倍。
所以多数时候问题不在速度,而在两点:语义上它会「拿锁」(自旋等待),不适合不能等待的上下文;以及全进程共享的锁表让不相干的数据互相干扰。
从哪来
is_lock_free 是 const fn,判断只看类型:can_transmute 要求 size_of::<T>() == size_of::<Atomic*>() 且 align_of::<T>() >= align_of::<Atomic*>()。都不满足就落到 lock(addr):按 addr % 67 从 67 把全局 seqlock 里选一把(LEN = 67,每把 CachePadded)。读是乐观的(读版本号、拷贝、再校验),写要拿锁,拿不到就在 Backoff::snooze 里自旋、让出。
怎么改
在编译期断言:需要无锁的地方加一行
#[repr(C, align(8))] #[derive(Clone, Copy)] struct Packed { a: u32, b: u16, } const _: () = assert!(AtomicCell::<Packed>::is_lock_free());类型改了、对齐变了,编译就失败。对
(u32, u16)写同样的断言(exp-crossbeam/pitfalls/src/bin/lockfree_assert.rs,--features bad):error[E0080]: evaluation panicked: assertion failed: AtomicCell::<(u32, u16)>::is_lock_free()调整类型:两个小字段打包成一个
u64(自己移位),或者用#[repr(C, align(8))]的结构体让对齐达到 8;Option<u64>换成Option<NonZeroU64>或用一个哨兵值。本来就要 16 字节:
u128、Instant没有稳定的无锁办法。低竞争时锁路径够用;高竞争时考虑Mutex(语义明确)或者换表示(比如把Instant换成相对某个起点的纳秒数u64)。很多走锁的 cell 被不同线程高频写:这是 67 把锁会互相干扰的情形;改成原生原子或分开加锁。
小结
AtomicCell<T>只有在大小相等且对齐足够时才是原子指令,其余走全局 67 把 seqlock;类型签名上看不出来,只有is_lock_free()知道。- E2:无竞争时锁路径只多约 1 ns,两线程争同一个 cell 时也不比原子慢;不相干的 cell 撞同一把锁时慢 8 到 9 倍。
- 用
const断言把「必须无锁」写进代码。