std::unordered_map 的数据竞争,尽管使用互斥体锁定插入

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

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

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

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

但是,我的 find() 调用没有互斥量,因为从我的阅读来看,似乎许多线程应该能够同时读取。但是,我偶尔会遇到数据竞争(由 TSAN 检测到),这会在 SEGV 中表现出来。数据竞争显然指向我上面提到的 insert()find() 调用。

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

为什么会这样?我对 std::unordered_map 的并发保证的理解不正确吗?

您仍然需要一个 mutex 让您的读者将作者拒之门外,但您需要一个 共享C++14 有一个 std::shared_timed_mutex that you can use along with scoped locks std::unique_lock and 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_map 有两个 find 重载 - 一个是 const,一个不是。
我敢说我不要相信 find 的非 const 版本会改变映射,但对于编译器来说,从多线程调用非 const 方法仍然是一场数据竞争,一些编译器实际上使用未定义的行为进行讨厌的优化。
所以第一件事 - 你需要确保当多个线程调用 std::unordered_map::find 时,它们使用 const 版本。这可以通过使用 const 引用引用地图然后从那里调用 find 来实现。

其次,您错过了很多线程可能会在您的地图上调用 const find,但其他线程无法在对象上调用非 const 方法的部分!我完全可以想象许多线程同时调用 find 和一些调用 insert,从而导致数据竞争。想象一下,例如,insert 使地图的内部缓冲区重新分配,而其他一些线程对其进行迭代以找到想要的对。

一个解决方案是使用具有 exclusive/shared 锁定模式的 C++14 shared_mutex。当线程调用 find 时,它在共享模式下锁定锁,当线程调用 insert 时,它在独占锁上锁定它。

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

另一种可能性是使用无锁散列图,例如 Intel 的线程构建块库提供的散列图,或者 concurrent_map 在 MSVC 并发运行时上。实现本身使用无锁算法,确保访问始终是线程安全的,同时又是快速的。