向量插入是否需用Mutex?有序/无序容器线程安全问题问询
多线程容器操作线程安全问题解答
已知在多线程环境下对std::vector执行erase操作时需要使用mutex,编写测试代码验证时发现:为erase操作添加mutex后程序看似正常,但约10分钟后foo1函数遍历vector时出现nullptr异常;若erase操作不使用mutex会立即触发段错误。针对此场景提出以下问题:
- Q1:
push_back仅在vector尾部插入元素,为何会在中间索引(如vector长度为521时,索引129)出现空指针错误? - Q2:使用
vector、deque等有序容器时,插入操作是否需要使用mutex? - Q3:
unordered_map等无序容器的插入操作无需mutex为何能正常运行约20分钟?
测试代码
class Test { public: Test(int idx) : m_index(idx) {} int m_index = { -1 }; int m_count = { 0 }; }; std::vector<std::unique_ptr<Test>> m_vec; std::mutex m_mutex; void foo1() // print element data { while (true) { std::unique_lock ulock(m_mutex); for (auto& e : m_vec) { e->m_count++; printf("%d : Count : %d\n", e->m_index, e->m_count); } ulock.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } } void foo2() // Only insert element { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<int> dis(0, 99); while (true) { int t = dis(gen); if (t >= 0 && t < 10) { //std::unique_lock ulock(m_mutex); m_vec.push_back(std::make_unique<Test>(m_vec.size())); } std::this_thread::sleep_for(std::chrono::milliseconds(t)); } } void foo3() // Only remove element { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<int> dis(0, 99); while (true) { int t = dis(gen); if (t >= 0 && t < 10) { std::unique_lock ulock(m_mutex); if (m_vec.empty() == false) m_vec.erase(m_vec.begin()); } std::this_thread::sleep_for(std::chrono::milliseconds(t)); } } int main() { m_vec.push_back(std::make_unique<Test>(1)); m_vec.push_back(std::make_unique<Test>(2)); m_vec.push_back(std::make_unique<Test>(3)); std::thread t1(foo1); std::thread t2(foo2); std::thread t3(foo3); t1.join(); t2.join(); t3.join(); return 0; }
问题解答
Q1 解析
vector的push_back并非只会操作尾部元素:当容器当前容量不足时,会触发内存重分配——申请一块更大的新内存,将原数组中所有元素拷贝/移动到新内存,随后释放旧内存。
你的测试代码中foo2的push_back未加锁,和加锁的foo1遍历、foo3的erase操作并发执行时,会出现致命竞态:
- 若
foo1正遍历旧内存中的元素,foo2的push_back完成了内存重分配,旧内存被释放,此时foo1访问的是已失效的内存区域,里面的unique_ptr会变成野指针,表现为nullptr或随机值,即使是中间索引的元素也会受影响(因为整个底层数组都被替换了)。 - 同时
foo3在头部执行erase会导致后续元素向前移动,若此时和未加锁的push_back并发,会破坏vector的内部状态(如size、capacity计数不同步),进一步引发非法内存访问。
Q2 解析
必须使用mutex。vector、deque这类标准容器本身不具备线程安全性,即使是尾部插入操作也不例外:
vector的push_back可能触发内存重分配,涉及整个底层数组的替换,过程中容器的内部控制数据(size、capacity、数据指针)会被修改,并发操作会导致这些状态不一致,引发内存访问错误。deque底层是分段数组结构,插入操作可能涉及分段的添加或合并,同样会修改容器的内部控制结构,并发下必然出现竞态条件。
任何对容器的修改操作(插入、删除、元素修改),以及与修改操作并发的读取操作,都必须通过锁机制同步,否则会触发未定义行为。
Q3 解析
这只是未定义行为的偶然表现,不代表unordered_map的插入操作是线程安全的:
unordered_map底层是哈希表,插入时需要计算哈希值、查找桶位置、修改桶内的链表/红黑树结构(不同实现细节有差异),并发插入会直接破坏哈希表的内部结构(如桶链表断裂、哈希冲突处理逻辑出错)。- 能运行20分钟只是因为竞态条件没有立刻触发致命错误,但随着时间推移,哈希表的内部结构会逐渐损坏,最终必然会出现崩溃、数据错乱、死循环等问题。
标准库中仅少数容器的特定操作是线程安全的(比如std::vector的const成员函数在无并发修改时安全),所有修改操作(包括插入)都必须通过同步机制保护。
内容的提问来源于stack exchange,提问作者mystes
相关产品推荐
相关产品推荐

