多线程下如何确保std::unordered_map中对象存在以避免崩溃?
解决std::unordered_map异步遍历与删除的段错误问题
问题场景
维护一个存储DataClass实例的std::unordered_map,存在两个异步运行的定时器:
- 周期1毫秒的定时器,遍历map对
DataClass内的数据执行数学运算 - 一次性超时定时器,超时值由
DataClass的成员变量定义,触发时发送网络消息并删除对应map键值对
问题:遍历过程中若触发超时删除操作,会导致核心转储(段错误11),需在遍历中确保键值对有效性以避免错误。
原始代码
struct DataClass { int id; //Some Other Data unsigned int timeout; }; std::unordered_map<int, DataClass> data_map = { {1, {1, 100}}, {2, {2, 200}}, {3, {3, 300}} }; void run_every_1ms(void) { for (auto &[key, value] : data_map) //How do I check if particular key is detected while executing for loop { std::cout << "ID : " << key << "\t"; std::cout << "Timeout : " << value.timeout << std::endl; } } void timeout(int id) { //Send message external system data_map.erase(id); //Problem if erase is executed during the for loop execution in run_every_1ms }
解决方案
方法1:互斥锁同步访问
通过互斥锁保证同一时间只有一个线程操作map,彻底避免并发修改导致的迭代器失效。
#include <mutex> std::mutex map_mutex; std::unordered_map<int, DataClass> data_map = { {1, {1, 100}}, {2, {2, 200}}, {3, {3, 300}} }; void run_every_1ms(void) { std::lock_guard<std::mutex> lock(map_mutex); // 自动加锁,作用域结束自动释放 for (auto &[key, value] : data_map) { std::cout << "ID : " << key << "\t"; std::cout << "Timeout : " << value.timeout << std::endl; // 执行你的数学运算逻辑 } } void timeout(int id) { // 若网络消息发送耗时,建议将此部分移到锁外,减少锁持有时间 // Send message to external system std::lock_guard<std::mutex> lock(map_mutex); data_map.erase(id); }
说明:这是最稳妥的方案,适合大多数场景。注意将耗时操作(如网络IO)移出锁范围,避免阻塞遍历线程。
方法2:复制键集合+遍历中检查存在性
先复制当前map的所有键到临时容器,遍历临时容器时,每次操作前检查原map中键是否仍存在。此方法能减少锁的竞争时间,适合遍历频繁、删除较少的场景。
#include <mutex> #include <vector> std::mutex map_mutex; std::unordered_map<int, DataClass> data_map = { {1, {1, 100}}, {2, {2, 200}}, {3, {3, 300}} }; void run_every_1ms(void) { // 短时间加锁复制所有键 std::vector<int> keys; { std::lock_guard<std::mutex> lock(map_mutex); for (const auto& pair : data_map) { keys.push_back(pair.first); } } // 遍历复制的键,每次操作前检查键是否存在 for (int key : keys) { std::lock_guard<std::mutex> lock(map_mutex); auto it = data_map.find(key); if (it == data_map.end()) { continue; // 键已被删除,跳过当前项 } // 访问it->second执行数学运算 std::cout << "ID : " << key << "\t"; std::cout << "Timeout : " << it->second.timeout << std::endl; } } void timeout(int id) { // 网络消息发送建议移到锁外 // Send message to external system std::lock_guard<std::mutex> lock(map_mutex); data_map.erase(id); }
说明:复制键的操作仅需短暂加锁,遍历过程中每次加锁时间也很短,能有效降低线程阻塞概率。
方法3:使用并发安全容器(C++17需依赖第三方库)
C++17本身没有内置的并发安全无序map,可使用第三方库如Boost的boost::concurrent_unordered_map,这类容器原生支持并发访问与修改,无需手动加锁。但需注意引入第三方库的依赖成本。
核心原则
std::unordered_map不是线程安全容器,并发修改和遍历必然导致迭代器失效、内存访问错误,必须通过同步机制避免。- 锁的粒度越小越好,尽量减少锁的持有时间,避免影响整体性能。
内容的提问来源于stack exchange,提问作者Dark Sorrow
相关产品推荐
相关产品推荐

