一句话定义:EBR 是一种为无锁数据结构设计的延迟回收机制。它通过将时间划分为不同的“代 (Epoch)”,批量管理内存的生命周期,是 Rust 高并发库 crossbeam 的核心引擎。
1. 核心痛点:无锁编程的“垃圾回收”
在实现无锁(Lock-Free)的数据结构(如并发 Stack、Queue、Map)时,我们面临一个经典难题:
- 写者把一个节点从链表里移除了(
unlink)。 - 写者想释放这块内存。
- 但是,可能有读者在移除动作发生前一毫秒读取了该节点的指针,并且正在读取数据。
- 如果写者立刻
drop,读者就会访问野指针 → Segfault。
EBR 的作用就是告诉写者:“先别扔!把它放进垃圾袋里,等所有人都说不用了,你再扔。”
2. 核心原理:三代同堂
EBR 并不追踪每一个指针(那是 Hazard Pointers 做的事),而是追踪线程的状态。它维护了一个全局计数器 (Global Epoch)。
运作机制
系统维护三个逻辑上的“代”:
- Current Epoch (N): 当前正在发生的时代。
- Previous Epoch (N-1): 刚刚过去的时代。
- Next Epoch (N+1): 即将到来的时代。
规则:
- 插眼 (Pinning):
当一个线程想要读取数据时,它必须先
pin()。这相当于宣誓:“我进入了当前时代 (N)”。 只要线程处于pinned状态,它就阻止了全局 Epoch 向前推进太快。 - 退休 (Retire): 当线程删除一个节点时,它不直接释放,而是把节点标记为“属于时代 N 的垃圾”,放入当前线程的本地垃圾袋。
- 回收 (Collection):
系统会检查所有线程的状态。
- 如果所有线程都已经处于时代 N 甚至更高,说明没有任何人滞留在时代 N-1 了。
- 结论:时代 N-2 (及更早) 产生的所有垃圾,现在绝对安全了,可以销毁。
生活类比:
- Epoch 是“红绿灯”。
- Pin 是“我车还在路口”。
- 规则:只有当路口的所有车(读者)都开走了,清洁工(回收者)才能上去扫地。
3. Crossbeam 中的代码实现
在 crossbeam-epoch 中,这个机制被封装得非常优雅。
3.1 关键角色
Guard: 相当于“令牌”。持有它意味着线程已pin,可以安全地访问无锁数据。Guard销毁时,线程unpin。Atomic<T>: Crossbeam 提供的原子指针。与std::sync::atomic不同,它的load方法必须传入&Guard。
3.2 代码演示
use crossbeam_epoch as epoch;
use std::sync::atomic::Ordering;
struct LockFreeStack<T> {
head: epoch::Atomic<Node<T>>,
}
struct Node<T> {
data: T,
next: epoch::Atomic<Node<T>>,
}
fn main() {
let stack = LockFreeStack { ... };
// 1. PIN: 激活当前线程,进入当前 Epoch
// guard 的生命周期结束前,全局 Epoch 不会推进导致当前数据失效
let guard = &epoch::pin();
// 2. LOAD: 安全读取
// 必须传入 guard,编译器保证你不能在没有 pin 的时候读
let shared_node = stack.head.load(Ordering::Acquire, guard);
if let Some(node_ref) = unsafe { shared_node.as_ref() } {
println!("Data: {}", node_ref.data);
}
// 3. RETIRE (Defer Destroy): 逻辑删除
// 假设我们把 head 移除了
if stack.head.compare_exchange(..., guard).is_ok() {
unsafe {
// 告诉系统:这个 node 哪怕现在还有人读,
// 但等所有处于当前 Epoch 的人都退出了,就可以释放了
guard.defer_destroy(shared_node);
}
}
// 4. Guard 离开作用域,线程 unpin。
}4. EBR vs Hazard Pointers (对比)
这是面试和架构选型的关键点。
| 特性 | EBR (Crossbeam) | Hazard Pointers |
|---|---|---|
| 性能 (读) | 极快 (只读本地变量) | 较慢 (需写全局内存+屏障) |
| 性能 (吞吐) | 高 (批量回收) | 中 (逐个回收) |
| 内存效率 | 波动大 (依赖 Epoch 推进) | 极好 (即时回收) |
| 最大弱点 | 线程阻塞 = 内存泄漏 | 实现极其复杂 |
🚨 EBR 的阿喀琉斯之踵:阻塞问题
如果有一个线程调用了 pin(),然后:
- 睡着了 (Sleep)。
- 死锁了。
- 执行了极长时间的 CPU 密集任务。
后果:该线程一直停留在 Epoch N。 → 全局 Epoch 无法推进到 N+1。 → Epoch N-1 的垃圾永远无法回收。 → 所有线程产生的垃圾都会堆积在内存里,导致 OOM (内存溢出)。
最佳实践:不要在持有
Guard(pin 住) 的时候做耗时操作,更不要休眠。Guard的作用域越小越好。
5. 总结
- EBR 是 Rust 无锁生态的基石:
crossbeam-deque,flurry(并发HashMap) 等都依赖它。 - 核心优势:读操作几乎零开销,非常适合读多写少的高并发场景。
- 使用契约:必须快速进出
pin()区域,严禁在Guard存活期间阻塞线程,否则会拖累整个系统的垃圾回收(一人掉线,全队无法清理背包)。
关联知识点:
- ABA 问题 - EBR 通过延迟回收天然解决了内存地址复用的 ABA 问题。
- Hazard Pointers - 另一种更精准但更慢的回收策略。