直接用 std::atomic 操作节点指针会出错,因其仅保证指针值原子性,不保障所指内存生命周期,易因 ABA 和内存重用导致悬垂指针;正确做法是封装指针+版本号或用 std::atomic 并确保无锁支持。

为什么直接用 std::atomic<t></t> 操作节点指针会出错
因为 std::atomic 对裸指针的特化(std::atomic<t></t>)只保证指针值本身的原子读写,不保证其所指向内存的生命周期安全。在无锁队列中,你可能刚读到一个非空 next 指针,它就被另一个线程释放了——这不是原子性问题,是 ABA 和内存重用共同导致的悬垂指针。
正确做法是:所有参与 CAS 的指针(如 head、tail、next)必须封装进自定义结构体,并用 std::atomic 包装该结构体(而非裸指针)。主流实现(如 Michael-Scott 队列)都采用「带版本号的指针」或「双字 CAS(double-word CAS)」来规避 ABA。x86-64 上可借助 std::atomic<uint64_t></uint64_t> 手动拼接指针+计数器;更稳妥的是用 std::atomic<:shared_ptr>></:shared_ptr>(但注意:C++17 起 std::shared_ptr 的原子操作才是无锁且标准保障的)。
- 别写
std::atomic<node> head;</node>—— 它无法防止 ABA,也无法安全释放节点 - 若用
std::atomic<:shared_ptr>></:shared_ptr>,确保编译器支持__atomic_is_lock_free对shared_ptr的无锁实现(GCC 9+/Clang 8+ 默认启用) - 手动拼接版本号时,指针至少占低 48 位(x86-64 用户空间地址),高 16 位放 counter;CAS 前必须先 load-current,拆解后再构造新值
compare_exchange_weak 在入队/出队循环中为什么总要配合 while 循环
因为 compare_exchange_weak 允许伪失败(spurious failure):即使当前值匹配预期,也可能返回 false。这在高竞争场景下频发,不重试会导致逻辑跳过、数据丢失。
典型错误是只调用一次 CAS 就放弃:
// ❌ 错误:单次尝试,失败即退出
if (!head->compare_exchange_weak(expected, desired)) {
return false; // 这里不该直接 return
}
正确模式是「读-改-比-换」闭环,直到成功或发现结构性变化(如 tail 已推进):
// ✅ 正确:循环重试,直到 CAS 成功或条件失效
Node* old_tail;
Node* next;
do {
old_tail = tail.load();
next = old_tail->next.load();
if (old_tail == tail.load() && next == nullptr) {
// 尝试推进 tail
if (old_tail->next.compare_exchange_weak(next, p)) {
break; // 成功
}
} else {
// tail 已被其他线程更新,或 next 非空 → 帮忙推进 tail
tail.compare_exchange_strong(old_tail, next);
}
} while (true);
-
compare_exchange_weak比strong更轻量,适合循环内使用;但必须配 while,不能用于“仅试一次”的语义 - 循环体内必须重新 load 当前状态(如再次读
tail.load()),否则可能基于过期值计算 - 当发现
next != nullptr时,应主动执行 tail 推进(helping),这是 Michael-Scott 队列的关键设计,避免 tail 滞后阻塞后续入队
出队操作中如何安全释放已弹出节点
无锁结构里,不能在 CAS 成功后立即 delete 节点——因为其他线程可能正持有该节点指针(比如正在遍历 next 链),此时释放会触发 UAF(use-after-free)。
可行方案只有两种:
- 用
std::shared_ptr<node></node>管理节点生命周期:入队时std::atomic<:shared_ptr>></:shared_ptr>存指针,出队时exchange(nullptr)并让引用计数自然归零;缺点是每次原子操作有额外开销 - 采用 Hazard Pointer 或 RCU 等内存回收机制:每个线程注册 hazard pointer 指向当前正在访问的节点,回收线程只释放那些未被任何 hazard pointer 引用的节点;实现复杂但零引用计数开销
简单测试可用 std::shared_ptr,但生产环境若对延迟敏感,必须上 Hazard Pointer。注意:不要用 std::unique_ptr —— 它的移动语义与原子操作不兼容,std::atomic 不支持 unique_ptr 特化。
完整可运行的最小无锁队列骨架长什么样
以下是一个基于 std::atomic<:shared_ptr></:shared_ptr> 的极简 Michael-Scott 队列核心(省略构造/析构/empty 等辅助):
struct Node {
int data;
std::atomic<:shared_ptr>> next{nullptr};
Node(int d) : data(d) {}
};
<p>class LockFreeQueue {
std::atomic<:shared_ptr>> head{std::make_shared<node>(0)};
std::atomic<:shared_ptr>> tail{head.load()};</:shared_ptr></node></:shared_ptr></p>
<p>public:
void enqueue(int data) {
auto node = std::make_shared<node>(data);
std::shared_ptr<node> t;
do {
t = tail.load();
} while (!tail.compare_exchange_weak(t, node));
t->next.store(node);
}</node></node></p>
<pre class="brush:php;toolbar:false;">bool dequeue(int& out) {
std::shared_ptr<node> h, t, n;
do {
h = head.load();
t = tail.load();
n = h->next.load();
if (h == head.load()) {
if (!n) return false;
if (head.compare_exchange_weak(h, n)) {
out = n->data;
return true;
}
}
} while (true);
}</node>
};
这段代码能跑通,但注意:它没处理 tail 滞后问题(即 t != h->next 时未帮 tail 推进),实际使用中会在高并发下显著降低吞吐。真正可靠的实现必须在 dequeue 中检测并修正 tail,这点极易被忽略,也是多数教学代码和博客示例的硬伤。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











