一位临时作家,多名 std::map 的常客

One occasional writer, multiple frequent readers for std::map

我遇到了并发问题。我有一个std::map,有一个 偶尔的作者和来自不同线程的多个频繁的读者,作者偶尔会在地图上添加键(键是一个std::string),我不能保证读者到底什么时候开始阅读和停止阅读。我不想为读者加锁,因为读取非常频繁,经常检查锁会损害性能。

如果读者总是通过键访问地图(而不是 map 迭代器),它是否总是线程安全的?如果没有,知道如何设计代码以便读者始终访问有效密钥(或 map 迭代器)吗?

也欢迎使用不同容器解决此问题的其他方法。

注意:从根本上编辑了答案

作为反射,我会加锁。

乍一看,您的情况似乎不需要加锁:

  • 对于insert(),据说"Concurrently accessing existing elements is safe, although iterating ranges in the container is not."
  • 对于 at() ,据说: "Concurrently accessing or modifying other elements is safe."

标准库解决了线程安全方面的问题:

23.2.2. Container data races

1) For purposes of avoiding data races (17.6.5.9), implementations shall consider the following functions to be const: begin, end, rbegin, rend, front, back, data, find, lower_bound, upper_bound, equal_range, at and, except in associative or unordered associative containers, operator[].

2) Notwithstanding (17.6.5.9), implementations are required to avoid data races when the contents of the contained object in different elements in the same sequence, excepting vector,are modified concurrently.

还有其他几个 SO 答案将其解释为线程安全保证,就像我最初所做的那样。

然而,我们知道在插入完成后容器中的迭代范围是不安全的。在以某种方式迭代查找元素之前,需要访问元素。因此,虽然该标准阐明了在您已经拥有不同元素的地址时并发访问不同元素的安全性,但措辞使潜在的容器并发问题悬而未决。

我在MSVC上尝试了一个多读单写的模拟场景,从来没有失败过。但这还不足以说明问题:允许实现避免比标准中 foressen 更多的数据竞争(参见 17.5.6.9)(或者也许我只是幸运了很多次)。

最后,我发现了两个严肃的 (post C++11) 参考资料,明确指出需要用户锁才能安全:

  • GNU document on concurrency in the standard library: "标准对库提出了要求,以确保没有数据竞争是由库本身引起的 (...) 用户代码必须防止在一个或多个访问修改状态时访问任何特定库对象状态的并发函数调用。"

  • GotW #95 Solution: Thread Safety and Synchronization, by Herb Sutter : "代码是否正确同步 (...)?否。代码有一个线程从 [=72 读取(通过 const 操作) =],以及写入同一个变量的第二个线程。如果这些线程可以同时执行,那就是一场竞赛,并且是未定义行为领域的直接不间断门票。"

基于这两个几乎权威的解释,我修改了我的第一个答案并回到我最初的反应:你必须锁定你的并发访问。

或者,您可以使用非标准库并发执行映射,例如 Microsoft's concurrent_unordered_map from the Parallel Pattern Library or Intel's concurrent_unordered_map from the Threading Building Blocks (TBB) or lock-free library as described in this SO answer

我不同意之前的回答。当他们谈论 "concurrently accessing existing elements" 时(当谈论 insert() 时),这假定您已经拥有现有元素的 pointer/reference/iterator。这基本上是承认地图不会在插入后在内存中移动元素。它还承认在插入期间迭代地图是不安全的。

因此,一旦插入,尝试在同一个容器(同时)上执行 at() 就是一场数据竞争。在插入期间,映射必须更改某种内部状态(可能是指向树节点的指针)。如果 at() 在该操作期间捕获容器,则指针可能不处于一致状态。

一旦有可能 insert()at()(或 operator[]) 同时发生。