《C++并发编程实战》中为何用引用计数/hazard变量而非std::atomic<std::shared_ptr>?
我正在学习Anthony Williams所著《C++并发编程实战》中的无锁数据结构,书中示例为无锁栈。Williams在第186页给出了pop操作的伪代码:
- 读取head的当前值;
- 读取head->next;
- 将head设置为head->next;
- 返回取出节点的数据;
- 删除取出的节点。
不含步骤5的代码如下:
std::shared_ptr<T> pop() { node* old_head=head.load(); while(old_head && !head.compare_exchange_weak(old_head,old_head->next)); return old_head ? old_head->data : std::shared_ptr<T>(); }
书中指出:若一个线程执行完步骤5时,另一个线程刚执行到步骤2,后者会解引用悬空指针。随后提出了引用计数/垃圾回收等解决方案。我的问题是:为何不能直接用原子智能指针封装节点?它们难道不能自动完成资源清理吗?
核心原因:原子操作的"可见性"与智能指针的局限性冲突
直接用std::atomic<std::shared_ptr<node>>封装节点看似能解决问题,但实际存在几个关键障碍:
普通
std::shared_ptr的引用计数非原子(C++20前)
早期C标准中,std::shared_ptr的引用计数修改默认不具备线程安全性。即便C20引入了std::atomic_shared_ptr,它也只保证指针本身的原子读写,节点内部的next指针依然是普通指针——当线程A原子替换head并让节点引用计数归零销毁时,线程B可能正卡在读取old_head->next的步骤,依然会触发悬空指针解引用。无锁场景下的"延迟销毁"需求无法被智能指针满足
智能指针的自动回收依赖明确的引用关系,但无锁环境中,线程对节点的访问是无同步的瞬时操作:你弹出节点后,引用计数可能立刻降到0,但此时仍有其他线程持有该节点的指针(比如正执行步骤2的线程)。智能指针无法感知这种潜在的访问,直接销毁节点必然导致崩溃。原子智能指针的操作粒度无法覆盖完整访问周期
就算用std::atomic_shared_ptr,读取head和读取head->next是两个独立的原子操作,中间存在时间窗口:线程A读取old_head后,线程B弹出并销毁该节点,线程A再去访问old_head->next时依然会踩悬空指针。原子智能指针只能保证指针本身的原子性,无法保证指针指向的对象在整个访问周期内存活。
总结
智能指针的自动清理需要明确的引用边界,但无锁数据结构中线程间的访问无同步机制,无法确保节点在所有潜在访问结束后再销毁。因此必须借助额外的节点级引用计数、 Hazard Pointer(危险指针)或垃圾回收机制,来实现节点的安全延迟销毁。
内容的提问来源于stack exchange,提问作者teddy

