【问题标题】:Data race with std::unordered_map, despite locking insertions with mutex与 std::unordered_map 的数据竞争,尽管使用互斥锁锁定插入
【发布时间】:2016-10-29 21:17:30
【问题描述】:

我有一个 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的并发保证的理解不正确吗?

【问题讨论】:

  • 听起来你需要一个shared_mutex,因为你有一个作家,所有的读写调用都需要同步。
  • 链接线程中的答案说:“A.多个线程同时读取, B.一个线程同时写作”。注意
  • 您误解了规范;它允许多个阅读器,但无论单个作者如何,都不允许多个阅读器。您需要像 NathanOliver 的评论中提到的 shared_mutex 一样的 Multiple Reader/Single Writer 锁
  • 谢谢大家。这一切都说得通。回复:shared_mutex,这听起来确实是正确的解决方案。但是,我尝试了它,但我遇到了麻烦。我现在正在尝试使用stackoverflow.com/questions/19915152/… 描述的方法,并使用 -std=c++14 进行编译。然而,虽然它能够找到 标头(同样,我正在使用 gcc 5.4 运行),但我得到了 error: ‘shared_mutex’ in namespace ‘std’ does not name a type。似乎 shared_mutex 可能直到 c++17 才可用?有关如何使其正常工作的任何快速指示?
  • 标准线程安全规则适用于std::unordered_map,就像它们适用于大多数类一样。标准线程安全意味着一个线程不能访问一个对象,而另一个线程正在或可能正在修改它。

标签: c++ concurrency stl containers unordered-map


【解决方案1】:

您仍然需要一个mutex 让您的读者将作者拒之门外,但您需要一个共享C++14 有一个 std::shared_timed_mutex 可以与作用域锁 std::unique_lockstd::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';
}

【讨论】:

    【解决方案2】:

    这种方法存在几个问题。

    首先,std::unordered_map 有两个重载 find - 一个是 const,另一个不是。
    我敢说我不相信那个非常量版本的find 将改变映射,但对于从多个线程调用非 const 方法的编译器来说仍然是数据竞争,并且一些编译器实际上使用未定义的行为进行讨厌的优化。
    所以第一件事 - 您需要确保当多个线程调用 std::unordered_map::find 时,它们使用 const 版本来执行此操作。这可以通过使用 const 引用来引用地图,然后从那里调用 find 来实现。

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

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

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

    另一种可能性是使用无锁哈希映射,例如英特尔的线程构建块库提供的哈希映射,或者在 MSVC 并发运行时使用concurrent_map。实现本身使用无锁算法,确保访问始终是线程安全的且同时快速。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-11-08
      • 1970-01-01
      • 2012-10-27
      • 2016-11-13
      • 1970-01-01
      • 2010-09-16
      • 2012-08-10
      相关资源
      最近更新 更多