stl容器如std::queue、std::list非线程安全,所有成员函数(含const)并发访问必触发未定义行为;必须为每次访问加锁,且需防范悬挂指针、死锁及隐式资源耦合。

崩溃不是“可能”,而是“只要对象被多个线程并发访问且未同步,就必然触发未定义行为”——哪怕只是读操作。
std::queue、std::list 等 STL 容器一并发就崩,为什么?
因为它们压根没做线程安全设计。std::queue::pop() 会修改底层 std::deque 的头指针;std::list::empty() 虽是 const 成员函数,但内部仍要读取节点计数或遍历链表头尾。一旦一个线程在 push(),另一个线程调用 empty() 或 front(),就可能读到半更新的指针、已释放的内存块,或破坏链表结构。
常见现象包括:
- 程序运行几秒后随机段错误,堆栈指向
std::list::_M_next或std::deque::_M_start -
empty()返回false,但紧接着front()就触发断言失败或空指针解引用 - 数据丢失或重复:
push()成功了,pop()却返回错误值
所有访问都必须加锁,包括 size() 和 empty()
别信“只锁写不锁读”的说法。STL 容器的 const 成员函数不是线程安全的“只读”——它们可能读取非原子字段、遍历内部结构,而这些操作与写入冲突时就是数据竞争。
实操建议:
- 声明一个
std::mutex成员变量,与容器同生命周期(例如作为类成员) - 每次调用
push()、pop()、front()、empty()、size()前,都用std::lock_guard<:mutex></:mutex>自动加锁 - 避免手写
lock()/unlock(),防止异常时漏解锁 - 锁粒度别太粗:比如不要把整个 while 循环包进一个
lock_guard,否则吞吐量会断崖式下跌
this 指针跨线程传递引发悬挂指针崩溃
Lambda 中用 [&] 捕获,会隐式捕获 this。若主线程中对象 A 已析构,而子线程还在调用 A::saySomething(),就会访问已释放内存,崩溃点常出现在成员函数第一行(如虚表访问或成员变量读取)。
正确做法是用弱引用保护生命周期:
- 让类继承
std::enable_from_this<a></a> - 在线程启动前,调用
weak_from_this()获取std::weak_ptr<a></a> - 在线程内用
lock()转成std::shared_ptr<a></a>,检查是否为空再调用成员函数 - 避免用
[=]值捕获整个对象——这会复制一份,导致主线程退出后子线程仍在运行,资源无法释放
死锁风险:多个 mutex 加锁顺序不一致
两个线程分别按不同顺序获取 mtx1 和 mtx2,极易形成循环等待。比如线程 A 先锁 mtx1 再等 mtx2,线程 B 先锁 mtx2 再等 mtx1——双方永久阻塞。
预防手段:
- 统一全局锁顺序:按地址大小强制排序,例如
if (&mtx1 > &mtx2) { mtx1.lock(); mtx2.lock(); } - 用
std::lock(mtx1, mtx2)一次性获取多个锁,它内部已实现无死锁的原子加锁 - 改用
std::unique_lock+try_lock_for()设置超时,避免无限等待 - 调试时启用
-fsanitize=thread,Clang 的 ThreadSanitizer 能直接报出锁竞争和潜在死锁路径
最易被忽略的一点:即使你只用了一个 mutex,只要它和某个全局资源(比如日志文件句柄、静态 map)存在隐式耦合,而其他模块又以不同顺序访问它们,死锁仍可能发生——锁的边界从来不止代码里那几行 lock_guard。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











