不能直接用 std::stack 做非阻塞栈,因其无原子操作、不支持立即失败返回;需基于 std::atomic 和 CAS 实现链式结构,确保 push/pop 立即返回、无锁无等待,并严格处理内存序、异常安全与资源释放。

为什么不能直接用 std::stack 做非阻塞栈
std::stack 本身不提供原子操作,其底层容器(如 std::deque 或 std::vector)所有修改操作都是非原子的。在多线程环境下哪怕只用一个线程“模拟并发调用”,只要存在信号中断、协程切换或与异步 I/O 交叉,就可能破坏内部状态。真正非阻塞(lock-free)的前提是:所有操作必须能用原子指令完成,且失败时可重试——而 std::stack 完全不具备这个能力。
单线程非阻塞栈的关键不是“避免锁”,而是“不等待、不挂起、不依赖调度”;它要求每个 push/pop 调用都立即返回结果(成功/失败),且不引入任何系统调用或条件变量。
用 std::atomic + 指针实现链式非阻塞栈
最轻量、最易验证的单线程 lock-free 栈是基于 CAS(Compare-And-Swap)的单向链表。核心是用 std::atomic<node></node> 管理栈顶指针,所有操作围绕 compare_exchange_weak 展开。
- 每个节点需对齐到指针大小(通常
alignas(std::atomic<void>)</void>不必要,但alignas(8)在 64 位系统上更稳妥) -
push必须先设置新节点的next,再 CAS 更新栈顶——顺序反了会丢节点 -
pop需要两阶段:先读当前栈顶,再 CAS 尝试更新为top->next;失败说明栈被其他逻辑(比如另一个伪并发上下文)抢先改过,需重试 - 单线程下不会真竞争,但代码结构必须和多线程版一致,否则移植或测试时行为突变
示例关键片段:
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
struct Node {
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
};
<p>class LockFreeStack {
std::atomic<Node<em>> head_{nullptr};
public:
void push(int x) {
Node</em> node = new Node(x);
Node* expected;
do {
expected = head<em>.load();
node->next = expected;
} while (!head</em>.compare_exchange_weak(expected, node));
}</p><pre class="brush:php;toolbar:false;">bool pop(int& out) {
Node* expected;
Node* desired;
do {
expected = head_.load();
if (!expected) return false;
desired = expected->next;
} while (!head_.compare_exchange_weak(expected, desired));
out = expected->data;
delete expected;
return true;
}};
单线程下要不要做内存序控制
即使只有单线程,std::atomic 的内存序也不能全用 memory_order_relaxed。因为编译器和 CPU 仍可能重排指令,导致 node->next 写入晚于 head_ 更新,从而让其他(哪怕是稍后执行的)逻辑看到不一致的中间态。
-
push中node->next = expected必须在 CAS 前完成,且不能被重排到 CAS 后 → 用memory_order_relaxed对写node->next足够,但 CAS 本身至少用memory_order_acquire(读)和memory_order_release(写) - 更安全的做法是:CAS 统一用
memory_order_acq_rel,简单且无性能损失(单线程无同步开销) - 完全去掉原子性(比如用裸指针+
volatile)是错的:volatile不阻止编译器优化掉“看似无用”的赋值,也不提供原子读写保证
实际使用时最容易漏掉的资源管理问题
这个栈自己 new 节点、自己 delete 节点,但没考虑异常安全。如果 Node 构造函数抛异常(比如内部 std::string 分配失败),push 中的 new Node(x) 就会泄漏内存,且 CAS 还没开始,无法回滚。
- 解决方法:把节点分配和初始化拆开,或用
std::make_unique+release(),但注意unique_ptr的默认删除器不是noexcept,仍可能抛异常 - 更务实的做法:在单线程嵌入式或性能敏感场景,禁止节点内含抛异常的成员;构造函数全部标记
noexcept - 还有一点常被忽略:
pop中delete expected是普通析构,如果节点里有需要同步释放的资源(如文件描述符),这里就变成“伪非阻塞”——析构本身可能阻塞。所以非阻塞栈只适合管理纯数据节点
非阻塞不是万能胶,它把复杂性从“等待”转移到了“重试逻辑”和“内存生命周期控制”上。单线程下省去了竞争处理,但原子操作、内存序、异常边界这三块,一个都不能少。

















