epoch-based reclamation是最实用的C++无锁内存回收方案,因它无需GC、不阻塞写线程、不要求线程持续调用retire,仅靠周期性全局epoch推进即可安全释放节点内存。

为什么 epoch-based reclamation 是 C++ 无锁数据结构里最实用的内存回收方案
因为它是少数能在不依赖垃圾收集器、不阻塞写线程、也不要求所有线程持续调用 retire 的前提下,还能保证安全释放内存的方案。核心在于它把“谁还在用这块内存”压缩成一个整数 epoch,靠周期性全局推进来判断对象是否真正可回收。
它不是万能的:不能替代 std::shared_ptr 做任意生命周期管理,只适用于无锁容器(如 LockFreeStack、ConcurrentQueue)中“被多个线程临时引用”的节点对象。一旦你试图用它管理长期存活或跨模块传递的对象,就会踩坑。
epoch 怎么推进?别自己手写轮询循环
常见错误是让每个线程在每次操作前都去读取并尝试更新全局 current_epoch,结果引发严重缓存行争用。正确做法是:每个线程维护自己的 local_epoch 和 last_sync_time,仅当本地 epoch 落后太多(比如 >2 个 epoch),或距离上次同步超过一定次数(如 100 次操作),才去原子读取全局值并同步。
- 全局
current_epoch只能由一个“主推进线程”(通常是第一个发现 epoch 长期未变的 worker)用compare_exchange_strong更新,避免多线程竞争写 - 线程进入临界区(如开始遍历链表)前,必须先
enter_epoch()获取当前快照;退出时调用leave_epoch()标记自己已离开——这两个动作不能省略,否则回收器无法感知活跃区间 - 推进时机不是定时器驱动,而是基于“回收队列积压量”和“最老未同步 epoch”双条件触发,否则低负载下 epoch 几乎不前进,内存一直悬着
retire_node 放哪儿?千万别直接放进全局链表
错误示范:所有线程把待回收节点直接 push 到一个共享的 std::vector<node></node>,再由回收线程统一处理。这会引入锁或昂贵的无锁链表操作,反而成为瓶颈。
立即学习“C++免费学习笔记(深入)”;
正确做法是每个线程持有一个本地 retired_list(通常用 std::vector 或定长数组),只在以下任一条件满足时才批量提交到全局回收池:
- 本地列表满(如 64 个节点)
- 当前线程的
local_epoch已比全局current_epoch落后 2 个以上 - 该线程即将长时间休眠(如等待条件变量),需主动 flush
全局回收池本身可以是一个简单的无锁单向链表(用 std::atomic<node></node> + CAS 实现),但关键点在于:回收线程只在 current_epoch - 2 对应的所有线程都确认离开后,才真正 delete 那批节点——这个“-2”是安全窗口,确保即使有线程卡在临界区,其 epoch 快照也已过期。
构造/析构函数里调用 retire_node?危险!
无锁结构体的节点常被设计为 POD 类型,但若你在 Node 析构函数里直接调用 retire_node(this),会导致两个问题:
- 析构发生在用户线程上下文,而
retire_node本应由持有该节点引用的线程负责(比如 pop 操作的调用者),责任错位 - 若析构发生在回收线程内部(比如你误把
delete放进回收逻辑),会触发重入,可能 double-retire 或崩溃
标准解法是:节点本身不管理生命周期,由上层容器(如 LockFreeStack)在 pop 成功后,由调用线程立即调用 reclaim->retire(node)。节点类里只放 raw pointer 和原子字段,不带虚函数、不重载 operator delete。
另外注意:C++17 的 [[no_unique_address]] 可以帮你把 epoch 相关字段(如 thread_local_epoch)零成本嵌入节点,但别滥用——它只节省空间,不解决语义问题。











