多线程下共享容器迭代器是未定义行为,因其是“活指针”而非数据快照,容器修改(如扩容、rehash)会导致迭代器立即失效,解引用或递增将随机崩溃;安全做法是每线程独立获取并使用迭代器,或改用索引、分片视图、线程安全容器。

多线程环境下直接共享并使用同一个容器的迭代器(比如 std::vector::iterator 或 std::unordered_map::iterator)是未定义行为,不是“加个锁就能用”的问题——迭代器本身可能在任意时刻因容器修改而失效,且失效后解引用或递增都会崩溃。
为什么不能把迭代器传给多个线程一起用
迭代器不是数据快照,而是指向容器内部状态的“活指针”。哪怕你用 std::mutex 保护了容器的读写,只要有一个线程调用了 push_back、erase、rehash 等操作,其他线程手里拿着的迭代器就可能立刻失效:
-
std::vector扩容时所有迭代器全部失效,旧地址内存被释放 -
std::unordered_map重哈希后所有迭代器失效,桶数组被重建 -
std::list虽然单个节点删除只让对应迭代器失效,但若另一线程正在遍历,而你在线程A中erase(it)后没及时更新线程B的迭代器,B继续++it就越界
更隐蔽的是:这些失效不抛异常,也不报错,只是随机崩溃或读到垃圾值。
安全做法:每个线程自己获取、自己用、用完即弃
核心原则是「迭代器生命周期必须严格限定在单一线程内,且不跨容器修改边界」。常见可行路径有:
立即学习“C++免费学习笔记(深入)”;
- 用索引代替迭代器(仅限
std::vector、std::deque等支持随机访问的容器):for (size_t i = start; i —— 索引不会失效,只要确保 <code>start/end范围在遍历期间不被其他线程修改即可 - 把容器切片后分发:主线程先调用
vec.size(),按线程数均分下标区间,每个线程拿到begin + offset和长度,自行构造局部迭代器范围,全程不共享原迭代器对象 - 对
std::unordered_map这类无序容器,改用键集合驱动:先用锁保护一次std::vector<key> keys; for (const auto& p : map) keys.push_back(p.first);</key>,然后把keys分片分给线程,各线程用自己的map.find(key)安全访问 —— 避开迭代器,也避开遍历时底层 rehash 的风险
如果非要用迭代器遍历,必须配合容器级互斥锁 + 单次原子快照
这不是推荐做法,但在某些低频、短生命周期场景下可临时兜底。关键点是:锁的粒度必须覆盖「从获取 begin()/end() 到遍历结束」整个过程,且期间禁止任何线程修改容器:
- 不能只锁
++it或*it,必须锁住整个循环体 - 不能用
auto it = map.begin()后解锁,再循环 —— 中间一旦有别的线程修改,it就废了 - 正确写法示例(仅作说明,性能差):
std::lock_guard<std::mutex> lk(mtx); for (auto it = map.begin(); it != map.end(); ++it) { process(it->second); }—— 这段代码会阻塞所有其他线程对map的读写,实际并发度为零
这种写法本质是把并发退化成串行,只适合调试或初始化阶段一次性扫表。
真正 scalable 的方案:封装带分片能力的只读视图
如果你需要高频、长期、多线程遍历一个容器,别反复手搓迭代器逻辑。应该封装一层抽象,例如:
- 对
std::vector:提供get_chunk(size_t thread_id, size_t total_threads),返回std::span(C++20)或std::pair<iterator iterator></iterator>,且保证该范围在返回时不被修改 - 对
std::unordered_map:预计算桶分布,暴露bucket_begin(size_t n)和bucket_end(size_t n),让线程按桶并行处理(需确认当前实现是否稳定,如 libc++ 保证桶不随插入变化,但 libstdc++ 不保证) - 更进一步:用
absl::flat_hash_map或tbb::concurrent_hash_map替代裸std::unordered_map,它们原生支持并发遍历接口,迭代器语义明确、线程安全
最易被忽略的一点:即使你封装了线程安全的遍历,也要检查容器本身的修改频率。如果写操作占比超过 5%,分片遍历仍可能看到过期数据——这时得考虑用读写锁(std::shared_mutex)或版本号机制,而不是死磕迭代器。


















