std::atomic 不是无锁的:多数标准库实现中其 is_lock_free() 返回 false,底层依赖互斥量或自旋锁,不满足 lock-free 进展保证。

std::atomic> 不是无锁的
直接说结论:std::atomic<shared_ptr>></shared_ptr> 在绝大多数标准库实现中(如 libstdc++、libc++)**不是无锁的**,调用 is_lock_free() 会返回 false。它底层依赖内部互斥量或自旋锁模拟原子操作,而非 CPU 原生的 CAS 指令序列。这意味着高竞争场景下仍有锁开销,不满足真正“无锁”(lock-free)算法对等待自由(wait-free 或 lock-free progress guarantee)的要求。
常见误判现象:代码编译通过、load()/store() 行为符合预期,就以为它是无锁的;实际压测时在多核高频更新下出现明显性能拐点或调度抖动。
- 可用
static_assert(std::atomic<:shared_ptr>>::is_lock_free(), "not lock-free");</:shared_ptr>在编译期验证(但注意:即使断言通过,也不代表所有操作都无锁——比如compare_exchange_weak可能仍退化) - libstdc++ 中该类型本质是
std::atomic<__shared_ptr_impl></__shared_ptr_impl>的封装,而指针本身虽可 CAS,但shared_ptr的引用计数更新仍需同步,无法单靠指针 CAS 完全解耦 - libc++ 同样使用内部
__atomic_flag或pthread_mutex_t保护引用计数读写
为什么不能靠 std::atomic 实现安全的无锁链表或栈
真正无锁数据结构(如 Michael-Scott 队列、Treiber 栈)要求每个修改操作(push/pop)能用单条 CAS 完成状态跃迁,并处理 ABA 问题。而 std::shared_ptr 的语义天然与之冲突:
-
shared_ptr的拷贝/赋值隐含引用计数增减,这些操作无法被包裹进一个原子 CAS 中——你 CAS 成功了,但新节点的use_count可能还没来得及从 1 升到 2,就被其他线程析构了 - 典型错误模式:
head.load()->next后,head被其他线程释放,但你手里的裸指针还在解引用 →use-after-free - 即使你手动管理引用(如用
std::atomic<t></t>+ 自定义 refcount),std::shared_ptr的自动管理反而成为干扰项,掩盖生命周期边界
所以,想写无锁结构,必须把内存生命周期控制权收回来,而不是交给 shared_ptr 自动托管。
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
真无锁 shared_ptr 替代方案:hazard pointer 或 epoch-based reclamation
若坚持用智能指针语义又要求 lock-free,得换内存回收机制。标准库不提供,但可基于 std::atomic<t></t> 手动组合:
- 用
std::atomic<node></node>存裸指针,所有读取侧先登记 hazard pointer(标记“我正在用这个地址”),写入侧只回收那些未被任何 hazard pointer 标记的节点 - 或者采用 RCU-like 的 epoch 机制:每次批量更新后推进 epoch,延迟回收上个 epoch 中所有被解引用过的节点
- 关键点:引用计数不再嵌在对象里,而是由回收器全局维护;
load()不增加计数,store()也不减少——增减只发生在明确的acquire/retire调用中
例如简单 hazard pointer 伪码:
struct HazardPtr {
static thread_local Node* ptr;
static std::atomic<Node*> current;
static void protect(Node* p) { ptr = p; current.store(p, std::memory_order_relaxed); }
};
// 读取时:
Node* old = head.load();
HazardPtr::protect(old);
if (old && old == head.load()) { /* 安全使用 old */ }
std::atomic 的合理使用场景
它适合低频、非核心路径的“原子切换”需求,比如配置热更新、状态快照、观察者指针交换——此时锁开销可接受,且你不需要对指针所指对象做并发修改。
- 典型用法:
std::atomic<:shared_ptr>> g_config;</:shared_ptr>,主线程周期性store(std::make_shared<config>(...))</config>,工作线程只load()一次并缓存使用 - 避免在循环内高频调用
load()或compare_exchange_weak(),尤其不要把它当std::atomic<t></t>用 - 若需高性能原子指针,直接用
std::atomic<t></t>,自己管生命周期;若需共享语义,用外部引用计数 + hazard pointer,别绕回shared_ptr
真正棘手的是跨线程传递所有权的同时保证无锁——这从来不是 std::shared_ptr 的设计目标,强行嫁接只会让问题更隐蔽。


















