std::stack 默认不是线程安全的,因其是依赖非原子操作容器的适配器,push/top/pop 等操作无同步机制,多线程并发访问必然引发竞争条件与未定义行为。

为什么 std::stack 默认不是线程安全的
因为 std::stack 本身只是容器适配器,底层依赖 std::deque 或 std::vector,而这些容器的所有成员函数(包括 push()、top()、pop())都不保证原子性。两个线程同时调用 push() 可能导致内存写冲突;一个线程在 top() 后、pop() 前被抢占,另一线程已清空栈,就会触发未定义行为——这不是“偶尔出错”,而是竞争条件必然发生的前提。
常见错误现象:double free、segmentation fault、std::out_of_range 异常在无显式越界访问时抛出、数据静默丢失。
- 不要试图只给
push()和pop()加锁:top()+pop()是典型检查-执行(check-then-act)模式,必须原子化 - 避免用
std::mutex包裹整个栈对象:高争用下会严重串行化,吞吐量骤降 - 别直接继承
std::stack并加锁——它没虚析构函数,且封装设计不支持安全扩展
用 std::shared_mutex 实现读多写少场景下的并发栈
如果栈主要用于生产者入栈、消费者批量取走(如任务分发),top() 调用远多于 pop(),可用 std::shared_mutex 降低读冲突开销。但注意:C++17 才引入该类型,且 Windows 上部分旧 MSVC 版本需开启 /std:c++17 并确认 _HAS_CXX17 宏启用。
关键逻辑是把“查看栈顶但不移除”和“弹出栈顶”拆成两个语义明确的操作,并确保后者独占访问:
template<typename t>
class ConcurrentStack {
std::stack<t> data_;
mutable std::shared_mutex rw_mutex_;
<p>public:
void push(const T& item) {
std::unique_lock lock(rw<em>mutex</em>);
data_.push(item);
}</p>
<pre class="brush:php;toolbar:false;">bool try_pop(T& out) {
std::unique_lock lock(rw_mutex_);
if (data_.empty()) return false;
out = std::move(data_.top());
data_.pop();
return true;
}
// 允许多个线程同时读 top(),但不保证 pop() 时仍存在
std::optional<t> peek() const {
std::shared_lock lock(rw_mutex_);
if (data_.empty()) return std::nullopt;
return data_.top(); // 注意:返回拷贝或 move,不能返回引用
}</t>
};
-
peek()返回std::optional<t></t>而非T&:避免锁释放后引用悬空 -
try_pop()必须用std::unique_lock,不能用std::shared_lock—— 否则多个线程可能同时看到非空状态并尝试pop(),导致二次弹出空栈 - 若 T 类型不可拷贝(如含唯一资源句柄),需重载移动构造/赋值,否则
data_.top()拷贝失败
无锁栈(lock-free stack)的正确实现要点
真正高性能并发栈必须摆脱互斥锁,但手写无锁结构极易出错。核心是使用 std::atomic<node></node> 管理头指针,配合 compare_exchange_weak() 循环重试。不要复用网上简陋的“CAS loop”示例——它们常忽略 ABA 问题、内存序错误、或节点生命周期管理漏洞。
关键约束:
- 必须用
std::atomic<node> head_</node>,而非std::atomic<node></node>:原子操作只能作用于指针或 POD 类型,不能对含构造/析构函数的对象原子化 - 所有 CAS 操作必须指定
std::memory_order_acq_rel(push/pop)和std::memory_order_acquire(读 head):防止编译器/CPU 重排破坏顺序 - 节点分配必须用
new(不能栈分配),且 pop 后的节点不能立即delete——需等待所有可能正在读该节点的线程退出临界区,通常需结合 hazard pointer 或 epoch-based reclamation - 标准库无内置无锁栈,
boost::lockfree::stack是可靠选择,但要求 T 可默认构造、可复制,且内部使用了 hazard pointer
简单示意 push 核心循环:
void push(const T& data) {
Node* node = new Node(data);
Node* expected;
do {
expected = head_.load(std::memory_order_acquire);
node->next = expected;
} while (!head_.compare_exchange_weak(expected, node,
std::memory_order_acq_rel, std::memory_order_acquire));
}
测试竞争条件不能只靠 sleep() 或循环次数
用 std::this_thread::sleep_for() 控制线程节奏看似能复现 bug,实则掩盖本质:真实竞争取决于调度器、缓存一致性协议、CPU 核心数,sleep 只是引入延迟,不是制造竞态。正确方式是用 libfuzzer 配合 -fsanitize=thread(TSan)编译,在多核机器上持续运行数万次压测。
- TSan 会报告
Data race on variable ...,精确定位哪两行代码、哪个线程、哪个内存地址发生冲突 - 测试用例必须覆盖边界:空栈 pop、单元素栈并发 push/pop、大量线程争抢同一栈
- 避免在测试中用
std::cout :IO 流本身有内部锁,会掩盖或干扰目标栈的竞争表现
最易被忽略的是内存可见性问题:即使加了锁,若忘记在构造函数里初始化 std::atomic 成员(如写成 head_(nullptr) 而非 head_{nullptr}),会导致未定义行为——这种 bug 在调试器里几乎无法捕捉,只有 ASan/TSan 能揪出来。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











