【问题标题】:Altering and reading elements of a unordered_map from different threads without lock在没有锁的情况下从不同线程更改和读取 unordered_map 的元素
【发布时间】:2021-03-15 21:46:08
【问题描述】:

我在代码中有一个 unordered_map,它初始化了固定数量的元素 在我的程序启动时

std::unorderd_map<int, bool> flags;
//Initialize all needed values in the map
// Start thread T1
// Start thread T2

所有条目在开始时都初始化为 false。 有两个线程(T1 和 T2)。 T1 可以更改映射中每个条目的值。 T2 只是定期读取条目。在这种情况下,我需要使用锁吗? unordered_map 中的元素总数在程序的整个生命周期中保持不变。

【问题讨论】:

标签: c++ stl mutex unordered-map


【解决方案1】:

是的,这会导致问题。标准中的措辞(§ [intro.races]/21.2)是:

如果程序的执行包含两个潜在的并发冲突操作,则程序的执行包含数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生,除了下面描述的信号处理程序的特殊情况。任何此类数据竞争都会导致未定义的行为。

因此,如果您从多个线程读取,则不会出现“冲突操作”,但即使一个线程写入,您也需要同步访问。

unordered_map的具体情况下,我可以看到同时写入和读取可能会导致非常严重的问题。

unordered_map 使用带有冲突链接的哈希表。这意味着如果两个(或更多)键散列到相同的值,它们都存储在同一个“桶”中,这基本上是值的链表。因此,在 unordered_map 中查找内容可能涉及遍历链表。

经验表明,在典型情况下,我们通常可以预期集合中的某些元素比其他元素更频繁地被访问,通常遵循众所周知的 80:20 规则(20% 的元素将占 80%访问)。

既然如此,优化哈希表的一种方法是尝试将最常访问的元素保留在其各自链表的头部。为此,写入值不仅可以修改值本身,还可以将其节点移动到(或朝向)链表的开头。因此,您的读取线程可能会尝试遍历链表,就像写入线程试图在链表中向前移动节点一样,因此链表当时完全损坏(例如,包含一个 next 指针'尚未初始化)。

因此,即使您不执行任何插入删除操作,并将映射中的值从 bool 更改为 std::atomic&lt;bool&gt;,您仍然有潜在的竞争条件。您确实需要使用锁来保护对地图本身的访问。

【讨论】:

  • 说更新是使用 m[k] = v 对现有密钥 k 完成的,这保证不会使迭代器无效 - 这是否意味着该节点上的链表不能重新排序(根据您的答案中假设的优化),因为这样做会影响迭代器的任何后续使用(继续迭代会跳过或重复元素 - 虽然它不会崩溃,但我不确定是否允许不 -无效的迭代器?)。给定T&amp; operator[](const Key&amp;),写入值本身不会写入任何可以在operator[] 调用后重新排序的代理。
  • (不管怎样,如果有人担心这个并想避免锁定,他们可以在更新线程中拥有第二个 unordered_map&lt;Key, std::atomic&lt;Value&gt;*&gt; 并通过它写入,当然两个映射意味着对缓存的更多需求这可能比锁定性能更重要,也可能不重要。)
  • @TonyDelroy:在链表中重新排列节点通常不需要修改任何节点的地址,因此很容易做到这一点,而不会影响指向该节点的现有指针的有效性。该标准并没有真正详细说明是否允许两次看到相同的条目。但它也可以将同步构建为递增迭代器,当/如果您只是从两个单独的线程进行正常查找时不会发生这种情况。
  • 我不知道如何构建同步,所以它只有在有写入线程时才会启动(并减慢操作)。无论如何,[container.requirements.dataraces] (here) 要求 “为了避免数据竞争” 应将 find 视为 const。因此,如果您避免使用operator[] 而使用find,然后从许多线程中修改/读取atomic 元素,那么它必须没有竞争条件。
  • @TonyDelroy:即使没有写作,它可能至少会减慢一点速度,但如果没有写作,您可能会将争用降至最低。虽然没有尝试实现它,而且(至少根据我的经验)在你真正处理它们之前很难确定细节。
【解决方案2】:

回答这个问题的方法有很多。几乎都是“是的,这是个问题”。

如果 T1 从地图中插入或删除元素,则地图在 T2 下方进行查找时可能会发生变化。如果您非常幸运,这将导致持续崩溃。

假设 T1 仅更新映射中的现有值,则 T2 可以读取一个正在更新的值,并且可以为布尔值获取一个不寻常的(例如,非真和非假)值。这可以通过使用unorderd_map&lt;int, atomic&lt;bool&gt;&gt; 来避免。

假设您避免了上述两个问题,您可能仍然会遇到数据竞争。某种锁可以避免这种情况。

[已添加] 实际上,锁可以避免所有这三个问题。

【讨论】:

  • 即使没有插入/删除,我认为 unordered_map&lt;int, atomic&lt;bool&gt;&gt; 仍然可以有竞争条件(在我的回答中描述)。
【解决方案3】:

由于 T1 正在向unordered_map 写入新值,因此您需要保护这些值不被并发访问,是的。是否使用std::mutexstd::atomic 等,由您决定。但是您需要在写入和读取之间建立某种同步机制。否则,T1 可能会在 T2 尝试读取相同值的完全相同的时刻写入一个新值,这将导致竞争问题。 T2 很容易最终收到陈旧或不完整的数据。

【讨论】:

  • @largest_prime_is_463035818 我认为更好的描述是“两个或多个线程访问共享数据并且其中至少一个是写入者”。
  • 他不需要锁。由于数据结构本身没有改变,因此不需要锁定它。然而,每个元素中的值可能会发生变化,因此他需要在那里进行一些同步。像std::unordered_map&lt;int, std::atomic&lt;bool&gt;&gt; 这样的东西可能比胖锁更轻量级。
  • @whitetiger 是的。因为标准是这样说的。
  • @whitetiger "我可以让 T2 读取之前的值" - 这种数据竞争的问题在于数据可能不完整,而不仅仅是陈旧。 bool 是 1 个字节,但 CPU 不能只读/写 1 个字节,它必须一次读/写整个字。 bool 数据可能不显着,但尝试 2+ 字节数据,如 shortint 等,您可以轻松看到在发生读取时仅写入部分字节的情况,因此您结束混合来自不同值的字节。原子/同步访问可以防止这种情况发生。
  • 也不能保证 bool 将是一个字节(这不仅仅是理论上的——有些真正的编译器使用 4 个字节作为布尔值)。只要正确对齐,它就会可能在硬件级别自动发生。另一方面,分配它是相当简单的,所以它跨越了缓存线边界,所以基本上没有办法让它甚至有希望自动发生。
【解决方案4】:

请使用锁。即使 T2 正在读取数据,您也不希望 T2 在 T1 修改/写入地图的同时读取。如果发生这种情况,您可能会遇到具有未定义行为的竞争条件,即 T2 从地图中读取错误的项目。

【讨论】:

  • 您确定行为未定义?
【解决方案5】:
std::unorderd_map<int, bool> flags;

...在这种情况下,我需要使用锁吗?

是的...您需要锁定整个unordered_map。毕竟——即使你刚刚……

bool flag;

...你需要锁定。 Jerry Coffin 在他的回答中引用了相关的诗句——为方便起见,在此转载:§[intro.races]/21.2

如果程序的执行包含两个潜在的并发冲突操作,则程序的执行包含数据竞争,其中至少一个不是原子的,并且两者都不会在另一个之前发生,除了下面描述的信号处理程序的特殊情况。任何此类数据竞争都会导致未定义的行为。


或者,您可以使用:

std::unorderd_map<int, std::atomic_bool> flags;

[container.requirements.dataraces] 保证您可以安全地执行并发 find 操作来查找 atomic&lt;bool&gt; 元素。 std::unordered_map&lt;...&gt;::find 按值返回 iterator,因此每个线程都有一个独立的对象来取消引用——那里没有潜在的竞争。然后从不同的线程读取/写入atomic_bool 显然可以安全地完成——这就是atomic 的重点。

【讨论】:

    【解决方案6】:

    不,如果您只是阅读并且没有更改 T2 中的数据,那么您应该没问题。但是,您可能需要考虑同步线程,以便 T2 在 T1 写入之前不会立即读取。

    【讨论】:

    • "T1 可以更改映射中每个条目的值" - 这意味着 T2 在没有保护的情况下读取值是不安全的
    猜你喜欢
    • 2013-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多