多线程下std::unordered_map元素修改与插入的线程安全性问询
问题描述
我有容器std::unordered_map<std::string, std::vector<int>> myMap;和互斥量std::mutex myMutex,初始访问myMap的代码如下:
void insert(const std::string& Key) { std::lock_guard<std::mutex> lock(myMutex); if(!myMap.contains(Key)) { auto& v = myMap[Key]; // 对v做一些操作使其非空 } }
std::vector<int>& get(const std::string& Key) { std::lock_guard<std::mutex> lock(myMutex); static std::vector<int> empty; // 此容器仅用于只读 if(myMap.contains(Key)) { auto& v = myMap[Key]; return v; } return empty; }
现在想确认:当一个线程执行如下代码修改容器(修改apples时不用myMutex,但用其他互斥量同步同key的写入),同时另一个线程调用insert("AnotherApple");,这种场景是否线程安全?
std::vector<int>& apples = get("Apple"); if(!apples.empty()) // 检查是否存在 { // 修改apples }
我的场景中,insert和get已通过myMutex同步,但修改get返回的指定key引用值时,并未与insert/get同步。查阅资料后仍有疑问,补充完整代码如下:
class myManager { public: myManager() = default; void insert(const std::string& id) { std::lock_guard<std::mutex> lock(myMutex); if (!myMap.contains(id)) { myMap[id];// 这里如果抛出异常,会不会有问题? try { mySeparateMutex[id]; } catch (...) { myMap.erase(myMap.find(id));// 需要从myMap中删除id对应的元素,保证与mySeparateMutex一致 throw; } } } std::tuple<std::vector<int>&, std::mutex&> get(const std::string& id, bool& ok) { std::lock_guard<std::mutex> lock(myMutex); static std::vector<int> Empty; static std::mutex noMutex; ok = false; if (myMap.contains(id)) { ok = true; return std::tie(myMap[id], mySeparateMutex[id]);// 我认为operator[]不会抛出异常 } return std::tie(Empty, noMutex); } private: // 假设只能通过上面的公共成员函数直接访问这些私有成员 std::mutex myMutex; std::unordered_map<std::string, std::vector<int>> myMap; std::unordered_map<std::string, std::mutex> mySeparateMutex; }; myManager instance; void fun0(const std::string& id) { bool isContain = false; auto [a, b] = instance.get(id, isContain); if (isContain) { std::lock_guard<std::mutex> lock(b);// 此互斥量用于防止同key的容器读写冲突 // 做一些修改 a.push_back(1); a.push_back(2); } } void fun1(const std::string& id) { instance.insert(id); } int fun2(const std::string& id, int index) { bool isContain = false; auto [a, b] = instance.get(id, isContain); if (isContain) { std::lock_guard<std::mutex> lock(b); if (index >= 0 && index < a.size()) { // 做一些读取操作 return a[index]; } } return -1; } int main() { fun1("apple"); std::thread a([]() { fun0("apple"); }); std::thread b([]() { fun1("apple"); }); std::thread c([]() { fun2("apple", 0); }); std::thread d([]() { fun0("apple"); }); // 还有其他线程用"banana"等id调用fun*函数 a.join(); b.join(); c.join(); d.join(); // 等待其他线程结束 return 0; }
结论:你的实现是线程安全的
核心安全逻辑说明
全局互斥量
myMutex的作用
它保护的是myMap和mySeparateMutex两个哈希表的结构完整性:插入新键、查找键、删除键等操作都在myMutex保护下完成,避免多线程同时修改哈希表结构导致的未定义行为。
当线程调用insert("AnotherApple")时,只会修改哈希表中与AnotherApple相关的条目,完全不会影响已存在的Apple对应的vector<int>和其专属互斥量,两者操作的是哈希表中独立的位置,无冲突风险。专属互斥量
mySeparateMutex的作用
每个key对应的vector<int>都有独立的互斥量,确保同一时间只有一个线程能修改或读取该vector,避免了同一vector的并发读写/写写冲突。
从get函数获取vector引用后,所有操作都在对应专属互斥量的保护下,符合C++容器并发访问规则:只要不同线程操作容器中独立的元素,且元素自身同步做好,就是安全的。
补充细节验证
insert函数的异常安全:你在insert中的异常处理是正确的。如果mySeparateMutex[id]抛出异常(比如内存不足),会删除已插入myMap的对应id条目,保证myMap和mySeparateMutex的一致性,不会出现“有vector但无对应互斥量”的情况。get函数的返回值安全:当key存在时,myMutex保证哈希表结构不会被修改,myMap[id]和mySeparateMutex[id]能稳定获取到引用,不会出现迭代器失效或引用悬空的问题。- 跨key操作的独立性:比如线程修改
Apple的vector,同时另一个线程插入AnotherApple,两个操作完全不交叉,myMutex保护哈希表结构,专属互斥量保护vector,各司其职,无安全风险。
内容的提问来源于stack exchange,提问作者0x314159

