不能直接用 std::atomic 包裹指针实现无锁链表,因其仅能原子操作指针本身,而插入/删除需同时更新多个内存位置,且无法避免 ABA 问题导致段错误、节点丢失或循环。

为什么不能直接用 std::atomic 包裹指针来实现无锁链表
因为 std::atomic<t></t> 只能原子地读写指针本身,但插入或删除节点时需同时更新多个内存位置(如新节点的 next 字段、前驱的 next 字段),而这些操作无法靠单个原子指针完成。更关键的是:**ABA 问题会直接破坏链表结构**——比如一个节点 A 被弹出、释放、又恰好被重新分配为新节点 A’,此时其他线程看到的“还是 A”,但语义已完全不同。
常见错误现象:load() 返回非空指针,但紧接着访问 node->next 触发段错误;或插入后链表跳过节点、循环、丢失数据。
- 必须用
compare_exchange_weak配合版本号(如std::atomic<uint64_t></uint64_t>拆高位存 tag)或std::atomic<:shared_ptr></:shared_ptr>(但后者有引用计数开销,且 C++17 前不保证 lock-free) - 若仅需单生产者单消费者(SPSC),可用
std::atomic<t></t>+ 内存序memory_order_relaxed配合严格顺序约束,但这不属于通用无锁链表 - 所有节点必须动态分配且生命周期由链表逻辑管理(不能提前
delete)
如何用 std::atomic<:shared_ptr>></:shared_ptr> 实现基础 push/pop
这是最易上手、避免手动内存管理错误的方案,适用于中低频场景。核心是利用 shared_ptr 的原子性与自动生命周期管理,绕过裸指针 ABA 和提前释放问题。
使用场景:日志队列、事件缓冲、非高性能实时系统。
立即学习“C++免费学习笔记(深入)”;
struct Node {
int data;
std::shared_ptr<Node> next;
Node(int d) : data(d) {}
};
<p>class LockFreeStack {
std::atomic<std::shared_ptr<Node>> head{nullptr};
public:
void push(int data) {
auto node = std::make_shared<Node>(data);
std::shared_ptr<Node> expected;
do {
expected = head.load();
node->next = expected;
} while (!head.compare_exchange_weak(expected, node));
}</p><pre class="brush:php;toolbar:false;">std::shared_ptr<Node> pop() {
std::shared_ptr<Node> expected, desired;
do {
expected = head.load();
if (!expected) return nullptr;
desired = expected->next;
} while (!head.compare_exchange_weak(expected, desired));
return expected;
}};
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
-
compare_exchange_weak必须在循环中重试,失败可能因竞争或伪失败(spurious failure) -
pop()返回的是原栈顶节点,其next已被安全读出,但调用方需注意:该节点的next指针指向的仍是链表中可能已被其他线程修改的部分 - 性能影响:每次
push/pop触发一次shared_ptr引用计数增减(通常原子操作),在高争用下比裸指针方案慢 2–5 倍
真正 lock-free 且避免 ABA 的最小可行方案:tagged pointer
将指针和一个递增 tag 组合成 64 位整数(x86-64 下指针 48 位,剩余 16 位足够),用 std::atomic<uint64_t></uint64_t> 存储。tag 在每次 CAS 前自增,确保即使指针值重复,整体值也不同。
参数差异:uintptr_t 用于指针转整数,reinterpret_cast 回指针;tag 掩码常用 0xFFFFULL,指针掩码用 ~0xFFFFULL。
struct TaggedPtr {
std::atomic<uint64_t> val{0};
static constexpr uint64_t TAG_MASK = 0xFFFFULL;
static constexpr uint64_t PTR_MASK = ~TAG_MASK;
<pre class="brush:php;toolbar:false;">void store(Node* ptr, uint16_t tag) {
val.store((reinterpret_cast<uint64_t>(ptr) & PTR_MASK) | tag,
std::memory_order_relaxed);
}
std::pair<Node*, uint16_t> load() const {
uint64_t v = val.load(std::memory_order_acquire);
return {reinterpret_cast<Node*>(v & PTR_MASK),
static_cast<uint16_t>(v & TAG_MASK)};
}
bool compare_exchange(Node*& expected_ptr, uint16_t& expected_tag,
Node* desired_ptr, uint16_t desired_tag) {
uint64_t expected = (reinterpret_cast<uint64_t>(expected_ptr) & PTR_MASK) | expected_tag;
uint64_t desired = (reinterpret_cast<uint64_t>(desired_ptr) & PTR_MASK) | desired_tag;
return val.compare_exchange_strong(expected, desired,
std::memory_order_acq_rel,
std::memory_order_acquire);
}};
- 必须用
compare_exchange_strong(或循环 weak),否则 tag 更新可能失败而不重试 - 内存序选
acq_rel是因为 push/pop 同时含读和写语义;acquire保证后续读节点字段不被重排到 CAS 前 - 容易踩的坑:未对齐的指针导致
reinterpret_cast结果不可移植;tag 溢出(16 位最多 65535 次修改,需结合节点复用或更大 tag)
实际部署前必须检查的三件事
无锁结构一旦出错,往往表现为偶发崩溃、数据丢失或无限循环,调试成本极高。
- 确认目标平台
std::atomic<t></t>对对应类型是否 lock-free:std::atomic<:shared_ptr>>::is_lock_free()</:shared_ptr>或std::atomic<uint64_t>::is_lock_free()</uint64_t>,返回false则底层用了互斥量,不是真无锁 - 所有节点分配必须用
new(而非栈变量或静态存储),且禁止在任何线程中显式delete—— 生命周期由链表逻辑或shared_ptr控制 - 测试不能只跑正确路径:用
std::thread启动 4+ 线程反复 push/pop 10 万次,再遍历验证节点数、sum 值、无环;加入随机 sleep 模拟调度干扰
真正难的不是写通代码,而是证明它在所有内存模型(尤其是 relaxed 序下的重排边界)和所有硬件平台(x86 vs ARM)下都满足线性一致性。别跳过 ASan/TSan 检测,它们能抓到 90% 的误用。

















