【问题标题】:c++ - vector of atomics fully thread safe?c++ - 原子向量完全线程安全?
【发布时间】:2017-08-21 09:16:23
【问题描述】:

我有一个std::vector<std::atomic<size_t>> vec。运行vec[index].fetch_add(1, std::memory_order_release) 或存储/加载多个并发线程是否安全?我认为应该是,因为读取是线程安全的,并且由于原子性,不可能同时从多个线程写入一个条目 - 对吗?

【问题讨论】:

  • 如果另一个线程正在或可能正在修改您正在阅读的内容,则阅读不是线程安全的。
  • 是的,但是读取也是原子性的——至少对于变量本身来说是这样。因为它在向量中,有什么不同吗?
  • 您的意思是vec[0].fetch_add(...) 还是类似的?如果是这样,考虑如果另一个线程调用push_back,向量可能重新分配和移动所有元素。这不是原子的,它根本不与您的原子访问在您获取地址后停止存在的元素同步。

标签: c++ vector concurrency atomic


【解决方案1】:

不,一般来说,它不是线程安全的,因为 容器 本身不是原子的。

也就是说,只要您不更改向量中的内容(即做任何使 data() 的返回无效的事情),就可以了。

遗憾的是,您不能诉诸 std::atomic<std::vector<...>>,因为 std::vector 不是可轻易复制的。

【讨论】:

  • 好的,这就是计划 - 我将使用互斥锁同步所有插入/擦除操作,并且在我什么都不做之前读取会很好:) 对吗?
  • 几乎:您还需要考虑迭代器失效的可能性。
  • 元素的原子读写会很好,只要你不通过增长、缩小或擦除向量中的元素来使这些元素无效,是的。
  • 好的,没关系 :) 它用于自定义 LRU 缓存,具有许多并发获取但只有少量插入,所以我想计算一个元素被访问的频率,因为那时我不需要在读取操作中使用一个列表(所以,写入它)(就像在这个 github.com/mohaps/lrucache11/blob/master/LRUCache11.hpp 实现中一样)
  • @dvs23 元素的任何添加/删除都必须与所有读取同步。
【解决方案2】:

vec[X].fetch_add 是安全的,只要您修改每个单独的原子。如果您调用包装 std::vector 的任何非 const(修改)方法,这是未定义的行为,因为 std::vector 本身不是线程安全的。

事实上,您可以在某个线程中初始化向量,将其引用传递给某个异步任务,从那时起,仅对向量的特定元素起作用,而不对向量本身起作用。 vec[X].action(...) 是线程安全的,vec.action(...) 不是。

【讨论】:

    【解决方案3】:

    这取决于其他线程在做什么。

    来自标准:

    17.6.5.9 避免数据竞争 [res.on.data.races]

    ...

    2 C++ 标准库函数不得直接或间接访问线程可访问的对象 (1.10) 除了当前线程,除非通过函数的参数直接或间接访问对象, 包括这个。

    3 C++ 标准库函数不得直接或间接修改线程可访问的对象 (1.10) 除了当前线程之外,除非通过函数的非常量直接或间接访问对象 论据,包括这个。

    和

    23.2.2 容器数据竞赛 [container.requirements.dataraces]

    1 为避免数据竞争 (17.6.5.9),实现应考虑以下功能 常量:开始,结束,rbegin,撕裂,前,后,数据,查找,lower_bound,upper_bound,equal_range,在 并且,除了在关联或无序关联容器中,操作符[]。

    2 尽管有 (17.6.5.9),但需要实现以避免在包含的内容时发生数据竞争 同一容器中不同元素中的对象,除了vector,是同时修改的。

    通过将这两部分与原子规则相结合,我们可以推断出调用vec[index].fetch_add(1, std::memory_order_release) 不会导致其他线程执行相同或其他“const”操作(包括第 23.2.2.1 段中提到的那些)的竞争条件。但是,如果另一个线程在 vec 本身上调用非常量操作(例如 insert、erase、resize 等),那么我们会遇到 1.10 节中指定的未定义行为:

    1.10 多线程执行和数据竞争 [intro.multithread]

    ...

    4 如果其中一个修改内存位置 (1.7) 而另一个访问 或修改相同的内存位置。

    ...

    21 如果程序的执行包含不同线程中的两个冲突操作,则该程序的执行包含数据竞争, 至少其中一个不是原子的,也没有发生在另一个之前。任何此类数据竞争都会导致 未定义的行为。

    【讨论】:

    • +1:仅考虑 23.2.2 实际上非常重要的答案。否则,对容器的非 const 引用多次调用 operator[] 可能会出现竞争,而不管用户对结果做了什么或者容器的内容是什么。
    【解决方案4】:

    您的表达式vec[n].atomic_op(...) 本身不是原子的,而是分解为:

    auto iter = vec.begin();
    iter += n;
    iter->atomic_op(...);
    

    因此,前两个语句容易受到通常的迭代器无效规则的影响。同时更改向量的大小,或擦除第 n 个之前的任何元素,或第 n 个元素本身,都可能破坏它们。

    一旦你拥有一个元素的迭代器,如果那个迭代器在你使用它的时候没有失效,那么不管您对元素本身执行的原子操作将是安全的。

    【讨论】:

      猜你喜欢
      • 2021-11-17
      • 2020-09-15
      • 1970-01-01
      • 2020-06-28
      • 1970-01-01
      • 1970-01-01
      • 2012-04-18
      • 2019-10-02
      • 1970-01-01
      相关资源
      最近更新 更多