一句话定义:EBR 是一种为无锁数据结构设计的延迟回收机制。它通过将时间划分为不同的“代 (Epoch)”,批量管理内存的生命周期,是 Rust 高并发库 crossbeam 的核心引擎。


1. 核心痛点:无锁编程的“垃圾回收”

在实现无锁(Lock-Free)的数据结构(如并发 Stack、Queue、Map)时,我们面临一个经典难题:

  1. 写者把一个节点从链表里移除了(unlink)。
  2. 写者想释放这块内存。
  3. 但是,可能有读者在移除动作发生前一毫秒读取了该节点的指针,并且正在读取数据。
  4. 如果写者立刻 drop,读者就会访问野指针 Segfault

EBR 的作用就是告诉写者:“先别扔!把它放进垃圾袋里,等所有人都说不用了,你再扔。”


2. 核心原理:三代同堂

EBR 并不追踪每一个指针(那是 Hazard Pointers 做的事),而是追踪线程的状态。它维护了一个全局计数器 (Global Epoch)

运作机制

系统维护三个逻辑上的“代”:

  • Current Epoch (N): 当前正在发生的时代。
  • Previous Epoch (N-1): 刚刚过去的时代。
  • Next Epoch (N+1): 即将到来的时代。

规则:

  1. 插眼 (Pinning): 当一个线程想要读取数据时,它必须先 pin()。这相当于宣誓:“我进入了当前时代 (N)”。 只要线程处于 pinned 状态,它就阻止了全局 Epoch 向前推进太快。
  2. 退休 (Retire): 当线程删除一个节点时,它不直接释放,而是把节点标记为“属于时代 N 的垃圾”,放入当前线程的本地垃圾袋
  3. 回收 (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(),然后:

  1. 睡着了 (Sleep)。
  2. 死锁了
  3. 执行了极长时间的 CPU 密集任务

后果:该线程一直停留在 Epoch N。 全局 Epoch 无法推进到 N+1。 Epoch N-1 的垃圾永远无法回收。 所有线程产生的垃圾都会堆积在内存里,导致 OOM (内存溢出)

最佳实践:不要在持有 Guard (pin 住) 的时候做耗时操作,更不要休眠。Guard 的作用域越小越好。


5. 总结

  1. EBR 是 Rust 无锁生态的基石crossbeam-deque, flurry (并发HashMap) 等都依赖它。
  2. 核心优势:读操作几乎零开销,非常适合读多写少的高并发场景。
  3. 使用契约:必须快速进出 pin() 区域,严禁在 Guard 存活期间阻塞线程,否则会拖累整个系统的垃圾回收(一人掉线,全队无法清理背包)。

关联知识点

  • ABA 问题 - EBR 通过延迟回收天然解决了内存地址复用的 ABA 问题。
  • Hazard Pointers - 另一种更精准但更慢的回收策略。