c++oding="utf-8" ?>
c++ stl容器默认线程不安全。所有容器如vector、map、stack、queue等,读写共享实例均需手动同步;push_back等操作非原子,涉及多步易致数据竞争;适配器如stack/queue底层仍不安全;shared_mutex适用于读多写少场景;封装线程安全容器需注意迭代器生命周期、算法调用原子性及const接口语义。

不安全。C++标准库中的所有STL容器(如 std::vector、std::map、std::queue、std::stack 等)默认都不是线程安全的——无论读、写、还是读+读并发,只要涉及共享实例,就必须由程序员显式加锁或采用其他同步机制。
为什么 std::vector::push_back 不是线程安全的
看似原子的操作,实际包含多步:检查容量 → 必要时重新分配内存 → 复制/移动元素 → 更新 size。任意一步被其他线程打断,都可能造成内存越界、重复析构、size 与实际元素数不一致等问题。
-
std::vector的迭代器在扩容时会全部失效;若线程 A 正在遍历,线程 B 同时push_back触发扩容,A 的迭代器立刻变成悬垂指针 - 即使只读操作(如
at()或operator[]),若另一线程正在修改容器结构(如clear()),仍会引发未定义行为 - MSVC、libstdc++、libc++ 都不保证任何成员函数内部加锁;这是 C++ 标准明确要求的“零开销抽象”设计
std::stack 和 std::queue 的常见误用陷阱
它们只是适配器(std::stack 默认基于 std::deque,std::queue 同样),底层容器仍不线程安全。直接把 std::stack 对象传给多个线程并调用 push()/pop(),等同于裸用 std::deque —— 没有内置保护。
- 示例中两个线程分别调用
push和pop能“偶尔跑通”,纯属侥幸:未触发竞争条件不代表安全 -
empty()+top()+pop()是典型非原子序列;中间插入其他线程的pop()会导致top()返回已不存在的元素 - 不要依赖
std::stack::size()判断是否可取;它和后续操作之间没有同步语义
std::shared_mutex 适合读多写少的 map/cache 场景
当容器内容变化不频繁,但读取极其频繁(如配置缓存、路由表),用 std::shared_mutex 替代 std::mutex 可显著降低读冲突开销。
-
std::shared_lock<:shared_mutex></:shared_mutex>允许多个线程同时读,互不阻塞 -
std::unique_lock<:shared_mutex></:shared_mutex>写时独占,会阻塞所有读和写 - 注意:C++17 才正式支持
std::shared_mutex;MSVC 从 19.14(VS 2017 15.7)、GCC 8+、Clang 7+ 起可用;旧版本需用平台 API(如 Windows SRWLock)或第三方库 - 不能解决迭代器失效问题:写操作仍可能导致
std::map重平衡,使已有迭代器失效
封装线程安全容器时最容易忽略的三件事
很多人写 thread_safe_vector 类,只封住 push_back、size、at,却漏掉关键约束。
- 不保护迭代器生命周期:
begin()返回的迭代器必须在其整个使用期间持有锁;否则其他线程修改容器会让它立即失效 - 不处理算法调用:
std::find(vec.begin(), vec.end(), x)中的begin()/end()是两个独立调用,中间无锁保护,结果不可靠 - 不区分 const/non-const 接口:
operator[]即使是 const 成员函数,也可能因返回引用而允许写入;真正只读应返回值拷贝(如T get(size_t i) const)
最麻烦的从来不是加锁本身,而是确定“锁的粒度”和“锁的范围”——一个 std::mutex 保护整个容器太粗,保护单个元素又太细;而迭代器、算法、复合操作这些边界,恰恰是 STL 原生不定义的,得你自己划清楚。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











