不能直接用 std::atomic 存储节点指针,因其仅保证指针原子性,不保障所指对象生命周期安全,且不支持原子的“比较并交换+释放旧节点”操作,无法正确实现无锁插入/删除,易导致 ABA 问题、内存泄漏或 use-after-free。

为什么不能直接用 std::atomic<t></t> 存储节点指针
因为 std::atomic 对裸指针的特化(std::atomic<t></t>)只保证指针本身的原子读写,不保证指针所指向内存的生命周期安全。更关键的是:它不支持原子的“比较并交换+释放旧节点”组合操作——而无锁链表插入/删除必须依赖 compare_exchange_weak 或 compare_exchange_strong 来避免 ABA 问题和内存泄漏。
常见错误是写成:
std::atomic<Node*> head{nullptr};然后在插入时直接 head.exchange(new_node) —— 这会丢掉原有链表,不是真正的 push_front。
- 正确做法是用
std::atomic<:uintptr_t></:uintptr_t>或std::atomic<uintptr_t></uintptr_t>手动管理指针+tag(用于 ABA 防御),但 C++20 起推荐用std::atomic<:shared_ptr>></:shared_ptr>(需注意其原子操作仅对指针本身,不自动管理对象生命周期) - 更稳妥且广泛使用的方案:用
std::atomic<node></node>+std::atomic_flag或 hazard pointer 等外部机制配合,但初学者易踩坑 - 实际项目中,除非性能压测明确瓶颈在此,否则优先用
std::mutex包裹普通链表——无锁 ≠ 更快,反而更容易出错
如何安全实现无锁 push_front(使用 std::atomic<node></node> + CAS)
核心是循环尝试 CAS,直到成功更新头指针。必须确保新节点的 next 字段在 CAS 前已正确设置,且旧头节点未被其他线程释放。
示例代码片段(简化版,忽略内存序和 ABA):
struct Node {
int data;
Node* next;
};
<p>class LockFreeStack {
std::atomic<Node<em>> head{nullptr};
public:
void push(int val) {
Node</em> node = new Node{val, nullptr};
Node* old_head = head.load();
do {
node->next = old_head;
} while (!head.compare_exchange_weak(old_head, node));
}
};
-
compare_exchange_weak可能虚假失败,必须用 do-while 循环重试 - 必须先读
head.load(),再设置node->next,顺序颠倒会导致链表断裂 - 这里没处理内存释放问题:pop 时 delete 节点可能触发 use-after-free —— 实际必须搭配 hazard pointer、RCU 或 epoch-based reclamation
- 默认内存序是
memory_order_seq_cst,性能差;高频场景可降为memory_order_release(store)和memory_order_acquire(load),但需严格验证顺序依赖
pop 操作为何比 push 更危险
push 只修改头指针,pop 却要读头指针、解引用获取 next、再更新头指针——三步之间存在竞态窗口。最典型问题是:线程 A 读到非空 head,线程 B pop 掉该节点并 delete,线程 A 继续解引用 old_head->next 就是野指针。
立即学习“C++免费学习笔记(深入)”;
错误写法:
Node* pop() {
Node* old_head = head.load();
if (old_head == nullptr) return nullptr;
head.store(old_head->next); // ⚠️ old_head 可能已被 delete
return old_head;
}
- 即使加了 CAS,也不能解决“读到指针→解引用→CAS”之间的释放竞争
- 标准解法不是靠原子指令,而是靠内存回收机制:hazard pointer 记录当前正在访问哪些节点;或者用
std::shared_ptr自动管理生命周期(但需注意std::atomic<:shared_ptr></:shared_ptr>的compare_exchange是对整个 shared_ptr 对象原子操作,开销大) - C++20 引入
std::atomic<:shared_ptr>>::compare_exchange_weak</:shared_ptr>,可用,但要注意shared_ptr构造/拷贝非原子,仍需 careful placement
真正可用的最小可行方案(C++17+,带基础内存安全)
放弃完全无锁,改用 std::atomic<:shared_ptr>></:shared_ptr> + 构造时捕获 next,避免运行时解引用裸指针:
struct Node {
int data;
std::shared_ptr<Node> next;
Node(int d, std::shared_ptr<Node> n) : data(d), next(std::move(n)) {}
};
<p>class SimpleLockFreeStack {
std::atomic<std::shared_ptr<Node>> head{nullptr};
public:
void push(int val) {
auto node = std::make_shared<Node>(val, head.load());
while (!head.compare_exchange_weak(node->next, node)) {
// node->next 已更新为最新 head,继续重试
}
}</p><pre class="brush:php;toolbar:false;">std::shared_ptr<Node> pop() {
auto old_head = head.load();
while (old_head && !head.compare_exchange_weak(old_head, old_head->next)) {
// old_head 更新为当前 head,继续重试
}
return old_head;
}};
- 所有节点通过
shared_ptr管理,无需手动 delete,天然规避 use-after-free - push 中
node->next初始化为head.load(),避免后续解引用裸指针 - pop 返回
shared_ptr,调用方持有所有权,不会因 head 更新就失效 - 性能比纯裸指针略低(引用计数原子操作),但正确性高得多;若追求极致性能,必须引入专业 RCU 库如
libcds
无锁链表的复杂性不在原子操作本身,而在内存生命周期管理。写错一行 CAS 可能跑一年才 crash,而漏掉一个 hazard pointer 记录,就会在高并发下随机崩。动手前先问自己:真的需要无锁?还是只是想学原子操作?


















