C++标准库至今未提供hazard pointer实现;它是一种用户态内存回收协议,需手动实现三要素:thread_local hazard_record、全局hazard_domain和retire接口,且必须精确判断保护点以避免use-after-free。

标准库根本没提供 hazard pointer
截至 C++26 草案(N4971 及之前),std::memory_order、std::atomic 和 std::shared_ptr 都不包含 hazard pointer 的实现或接口。这不是遗漏,而是标准委员会明确拒绝将其纳入——它太重、太依赖具体使用模式,且难以在通用容器中安全复用。
常见错误现象:std::atomic<Node*> head; 配合 load(memory_order_acquire) 后直接解引用,看似“无锁”,实则可能访问已释放内存;有人误以为 std::shared_ptr 的原子操作能替代 hazard pointer,但它只保对象生命周期,不保指针本身被其他线程观察到的“可见窗口”。
- hazard pointer 是一种**用户态内存回收协议**,不是同步原语,标准库只管同步(如原子操作),不管回收
- C++26 引入的
std::atomic_ref和 relaxed atomic reordering 规则,反而让手写 hazard pointer 更安全,但没提供现成封装 - 所有号称“标准库支持 hazard pointer”的文章,基本混淆了
std::atomic的能力边界
自己实现 hazard pointer 的最小可行结构
核心就三块:每个线程一个 hazard_record(存当前正在访问的指针),全局一个 hazard_domain(管理所有记录和待回收链表),外加一个 retire 接口把待释放节点挂到 domain 上。
关键点在于:线程必须显式调用 publish(标记指针为 hazard)和 clear(清除),且 retire 后不能立刻 delete,得等所有线程的 hazard 记录都确认没指向它。
立即学习“C++免费学习笔记(深入)”;
-
hazard_record通常用thread_local存,避免 cache line bouncing;大小建议固定为 4–8 个槽位,够用且易遍历 - 全局
hazard_domain的扫描逻辑必须用memory_order_acquire读所有 thread_local 记录,否则可能漏检 - 不要用
std::vector<Node*>存 hazard 槽位——动态分配会破坏无锁前提;用std::array<Node*, N>+ 原子索引控制
struct hazard_record {
std::array<std::atomic<Node*>, 4> ptrs;
std::atomic<size_t> count{0};
};
<p>thread_local hazard_record current_hazard;</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/xiazai/gongju/2823" title="C++14"><img
src="https://img.php.cn/upload/manual/001/431/639/6ac8b33c327c4749.png" alt="C++14" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/xiazai/gongju/2823" title="C++14">C++14</a>
<p>C++14 对 C++11 的修正与增强版本,适合旧系统维护和较老工具链兼容。</p>
</div>
<a href="/xiazai/gongju/2823" title="C++14" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p>Node<em> protect(Node</em>& ptr) {
auto p = ptr.load(std::memory_order_acquire);
size_t i = current_hazard.count.fetch_add(1, std::memory_order_relaxed) % 4;
current_hazard.ptrs[i].store(p, std::memory_order_release);
return p;
}</p>为什么别直接抄 folly::hazptr 或 moodycamel::ConcurrentQueue 的实现
它们确实可用,但引入方式和行为差异极大:folly 的 hazptr_holder 依赖 folly::Synchronized 管理全局 epoch,moodycamel 版本用的是类似 RCU 的 epoch 批量回收,两者都不满足“纯 C++ 标准库依赖”前提,且默认开启 debug 检查(比如 double-retire 报错),上线前必须关掉。
典型踩坑场景:在 shared library 中用 folly::hazptr,若主程序也链接了另一份 folly(不同版本),hazptr_domain 全局状态会冲突,导致随机 crash;又或者误把 hazptr_holder 当 RAII 容器,在函数返回时自动 clear,却忘了它只 clear 当前 holder 绑定的 slot,不是整个 thread_local 记录。
- folly::hazptr 默认每 100ms 做一次 epoch bump,高吞吐下可能拖慢回收延迟
- moodycamel::ConcurrentQueue 内置的 hazard pointer 仅用于其 queue 实现,未暴露 API,无法复用于自定义结构
- 二者都不兼容
constexpr初始化,没法塞进 static 生命周期对象里
真正该关心的:你是否真的需要 hazard pointer
绝大多数所谓“无锁数据结构”场景,其实用 epoch-based reclamation(EBR)或 defer-delete 就够了。hazard pointer 只在一种情况下不可替代:节点生命周期极短、线程数多(>32)、且要求严格 O(1) 回收延迟(比如高频交易订单簿)。
更常见的问题是:写了 hazard pointer 却没配好 memory_order,比如在 protect() 里用 memory_order_relaxed 写 hazard 槽位,导致其他线程 scan 时读到陈旧值;或者忘了在 retire() 前做 full barrier,让编译器把 delete 提前到 hazard 还生效的位置。
- 先用
std::shared_ptr+std::atomic实现原型,压测看回收是否成瓶颈 - 如果瓶颈真在内存回收,优先试
liburcu(userspace RCU),它比 hand-rolled hazard pointer 更易 audit - C++26 的
std::atomic_wait和std::atomic_notify对 epoch 等待有帮助,但不改变 hazard pointer 的手动管理本质
最常被忽略的一点:hazard pointer 的正确性完全依赖程序员对“哪些指针需要保护”的精确判断。一个漏掉的 protect() 调用,就等于在生产环境埋了个 use-after-free,而且大概率只在特定负载下触发。


















