使用std::unordered_map进行数据竞争,尽管使用互斥锁插入

Data race with std::unordered_map, despite locking insertions with mutex

本文关键字:插入 竞争 unordered std map 数据 使用      更新时间:2023-10-16

我有一个c++ 11程序,它执行一些计算并使用std::unordered_map来缓存这些计算的结果。该程序使用多个线程,它们使用一个共享的unordered_map来存储和共享计算结果。

根据我对unordered_map和STL容器规范的阅读,以及unordered_map线程安全,似乎由多个线程共享的unordered_map可以一次处理一个线程写入,但一次处理许多读者。

因此,我使用std::mutex来包装我的insert()调用映射,这样一次最多只有一个线程插入。

然而,我的find()调用没有互斥锁,从我的阅读,似乎许多线程应该能够一次读取。然而,我偶尔会得到数据竞争(如TSAN检测到的),在SEGV中表现出来。数据竞争清楚地指向我上面提到的insert()find()调用。

当我将find()调用包装在互斥锁中时,问题就消失了。然而,我不想序列化并发读取,因为我试图使这个程序尽可能快。(仅供参考:我正在使用gcc 5.4运行)

为什么会发生这种情况?我对std::unordered_map的并发性保证的理解是否不正确?

您仍然需要一个mutex供您的读者将作者拒之门外,但是您需要一个共享的C++14有一个std::shared_timed_mutex,您可以像这样与作用域锁std::unique_lock和std::shared_lock一起使用:

using mutex_type = std::shared_timed_mutex;
using read_only_lock  = std::shared_lock<mutex_type>;
using updatable_lock = std::unique_lock<mutex_type>;
mutex_type mtx;
std::unordered_map<int, std::string> m;
// code to update map
{
    updatable_lock lock(mtx);
    m[1] = "one";
}
// code to read from map
{
    read_only_lock lock(mtx);
    std::cout << m[1] << 'n';
}

这种方法有几个问题。

首先,std::unordered_mapfind有两个重载——一个是const,一个不是。
我敢说,我不相信find的非const版本会改变映射,但对于编译器来说,从多线程调用非const方法仍然是一种数据竞争,一些编译器实际上使用未定义的行为来进行糟糕的优化。
因此,第一件事—您需要确保当多个线程调用std::unordered_map::find时,它们使用const版本。这可以通过使用const引用引用map,然后从那里调用find来实现。

第二,你忽略了许多线程可以调用你的map上的const find,但其他线程不能调用对象上的非const方法的部分!我完全可以想象许多线程同时调用find和一些线程同时调用insert,从而导致数据竞争。想象一下,例如,insert使map的内部缓冲区重新分配,而其他线程迭代它以找到想要的对。

解决方案是使用c++ 14 shared_mutex,它具有独占/共享锁定模式。当线程调用find时,它将锁锁定在共享模式,当线程调用insert时,它将锁锁定在独占模式。

如果你的编译器不支持shared_mutex,你可以使用平台特定的同步对象,如Linux上的pthread_rwlock_t和Windows上的SRWLock

另一种可能性是使用无锁哈希映射,就像Intel的线程构建块库或MSVC并发运行时上的concurrent_map提供的那样。实现本身使用无锁算法,确保访问始终是线程安全的,同时也是快速的。