std::deque本身不是线程安全的,必须加锁;其所有成员函数(如push_front、pop_back、size)均非原子操作,多线程并发读写会引发数据竞争,导致崩溃或未定义行为。

std::deque 本身不是线程安全的,必须加锁
标准库的 std::deque 所有成员函数(包括 push_front、pop_back、size)都不保证原子性。多个线程同时读写同一实例时,哪怕只是“一个 push + 一个 pop”,也可能触发数据竞争,导致崩溃或未定义行为。
常见错误现象包括:程序随机 segfault、std::length_error 或 std::bad_alloc 抛出、返回错误的 size() 值、迭代器失效后仍被解引用。
- 不要假设只读操作(如
size()、empty())是安全的——它们内部可能访问共享计数器或指针,且不带内存屏障 - 不要用
const成员函数自我安慰:const不等于线程安全 - 避免在锁外调用
front()/back()后再进锁pop_*——中间可能已被其他线程修改
用 std::mutex 封装 deque 是最直接的做法
把 std::deque 和 std::mutex 组合成一个类,所有公有接口在临界区内执行。这是多数场景下最清晰、可控性最强的方式。
示例关键结构:
template<typename t>
class thread_safe_deque {
private:
std::deque<t> data_;
mutable std::mutex mtx_;
public:
void push_front(const T& item) {
std::lock_guard<:mutex> lk(mtx_);
data_.push_front(item);
}
bool try_pop_back(T& item) {
std::lock_guard<:mutex> lk(mtx_);
if (data_.empty()) return false;
item = std::move(data_.back());
data_.pop_back();
return true;
}
size_t size() const {
std::lock_guard<:mutex> lk(mtx_);
return data_.size();
}
};</:mutex></:mutex></:mutex></t></typename>
-
try_pop_back返回bool而非抛异常,避免锁内抛异常导致死锁风险 -
size()声明为const,但内部仍需加锁——所以mutable std::mutex - 移动语义(
std::move)减少拷贝开销,尤其对大对象重要
慎用 std::shared_mutex(C++17)替代 std::mutex
如果读多写少(比如大量线程频繁调用 size() 或 empty(),但增删不频繁),可用 std::shared_mutex 提升并发读性能。
但要注意:
-
std::shared_mutex在 Windows 上(MSVC)直到 VS2019 16.6 才完整支持;GCC 8+、Clang 7+ 支持较好 - 写锁(
std::unique_lock)会阻塞所有读锁,而读锁之间不互斥——这没错,但若写操作稍长,可能导致读线程饥饿 -
empty()和size()仍不能完全无锁:你无法在不锁的情况下安全判断“是否为空”,因为另一线程可能正处在push的中途
因此,除非压测确认读瓶颈明显,否则优先用简单 std::mutex ——它更易推理、调试和移植。
避免 lock-free deque:除非你真需要且懂内存模型
目前没有被广泛验证、跨平台稳定的 lock-free 双端队列实现进入标准库或主流开源容器库。自行实现涉及 ABA 问题、内存重排序、hazard pointer 或 RCU 管理等复杂细节。
真实项目中踩过的坑包括:
- 用
std::atomic<t></t>模拟栈容易,但双端队列需同时维护头尾指针与节点引用计数,正确性极难保障 - 即使基于成熟论文(如 “CAS-based lock-free deque”),在不同 CPU 架构(x86 vs ARM)上因内存序差异表现不一
- 调试困难:数据竞争往往不立即 crash,而是数小时后才暴露为逻辑错误
除非业务对延迟有亚微秒级要求,且团队有资深并发编程经验,否则不要碰 lock-free deque。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











