如何解决OpenMP中的竞态条件?关于STL map插入的锁疑问
问题分析
首先明确:STL标准容器(包括std::map)本身不提供线程安全保证。多个线程同时对std::map执行修改操作(比如insert),或者在有修改操作的同时执行读操作(比如find),都会触发竞态条件,导致未定义行为(比如容器结构损坏、数据读取错误、内存泄漏等)。
你当前代码的核心问题在于**“检查-插入”的竞态窗口**:当多个线程同时调用get函数时,可能都通过了!clusterExists(infoSet)的检查,然后排队进入#pragma omp critical块。第一个线程完成insert后,后续线程再执行insert时,虽然std::map的insert不会覆盖已存在的key,但会额外new一个InfoSet对象,直接导致内存泄漏;而且后续线程进入临界区后执行mInfoMap[infoSet]时,拿到的是第一个线程插入的对象,自己new的对象则完全丢失。
解决方法
1. 双重检查锁(最直接的修复)
在临界区内部再做一次存在性检查,避免重复创建对象:
InfoSet* get(std::string& infoSet, const int& numActs, Env::History& actions) { if (numActs > 0 && mCreateNotExists && !clusterExists(infoSet)) { #pragma omp critical { // 进入临界区后再次检查,避免其他线程已提前插入 if (mInfoMap.find(infoSet) == mInfoMap.end()) { mInfoMap.insert({infoSet, new InfoSet {numActs}}); for (auto& a : actions) { mInfoMap[infoSet]->insertActStr(Env::ActToStr(std::get<1>(a)), std::get<2>(a)); } } } } return mInfoMap[infoSet]; }
这样即使多个线程通过了外部的clusterExists检查,进入临界区后只有第一个线程会执行创建和插入操作,后续线程直接跳过,避免了内存泄漏和无效操作。
2. 缩小临界区范围(提升并行效率)
如果insertActStr的循环耗时较长,会导致临界区占用时间过久,降低并行性能。可以把对象的初始化操作移到临界区外,只在临界区执行find和insert:
InfoSet* get(std::string& infoSet, const int& numActs, Env::History& actions) { if (numActs > 0 && mCreateNotExists && !clusterExists(infoSet)) { // 先在临界区外完成InfoSet的初始化 InfoSet* newInfo = new InfoSet{numActs}; for (auto& a : actions) { newInfo->insertActStr(Env::ActToStr(std::get<1>(a)), std::get<2>(a)); } #pragma omp critical { if (mInfoMap.find(infoSet) == mInfoMap.end()) { mInfoMap.insert({infoSet, newInfo}); } else { // 若已存在,释放提前创建的对象,避免内存泄漏 delete newInfo; } } } return mInfoMap[infoSet]; }
这种方式能减少临界区的执行时间,让更多线程并行处理其他任务。
3. 使用读写锁优化读操作
当前clusterExists函数用critical区保护find操作,虽然安全但会导致所有读操作串行。如果读操作远多于写操作,可以改用OpenMP读写锁,允许多个线程同时读,仅在写操作时互斥:
首先初始化读写锁(通常在程序启动时):
omp_lock_t rw_lock; omp_init_read_write_lock(&rw_lock);
修改clusterExists使用读锁:
bool clusterExists(std::string& infoSet) { bool exists {false}; omp_set_read_lock(&rw_lock); // 获取读锁,允许多线程同时读 exists = mInfoMap.find(infoSet) != mInfoMap.end(); omp_unset_read_lock(&rw_lock); return exists; }
修改get函数的临界区为写锁:
InfoSet* get(std::string& infoSet, const int& numActs, Env::History& actions) { if (numActs > 0 && mCreateNotExists && !clusterExists(infoSet)) { InfoSet* newInfo = new InfoSet{numActs}; for (auto& a : actions) { newInfo->insertActStr(Env::ActToStr(std::get<1>(a)), std::get<2>(a)); } omp_set_write_lock(&rw_lock); // 获取写锁,独占访问 if (mInfoMap.find(infoSet) == mInfoMap.end()) { mInfoMap.insert({infoSet, newInfo}); } else { delete newInfo; } omp_unset_write_lock(&rw_lock); } return mInfoMap[infoSet]; }
记得在程序结束时销毁锁:
omp_destroy_read_write_lock(&rw_lock);
这种方式能大幅提升读操作的并行效率,适合读多写少的场景。
内容的提问来源于stack exchange,提问作者CuriousPanda

