C++11 以原子方式更新整数数组元素C++;

C++11 以原子方式更新整数数组元素C++;,c++11,thread-safety,stdatomic,C++11,Thread Safety,Stdatomic,给定一个整数计数器的共享数组,我想知道线程是否可以在不锁定整个数组的情况下自动获取和添加数组元素 下面是使用互斥锁锁定对整个阵列的访问的工作模型的示例 // thread-shared class members std::mutex count_array_mutex_; std::vector<int> counter_array_( 100ish ); // Thread critical section int counter_index = ... // unpredic

给定一个整数计数器的共享数组,我想知道线程是否可以在不锁定整个数组的情况下自动获取和添加数组元素

下面是使用互斥锁锁定对整个阵列的访问的工作模型的示例

// thread-shared class members
std::mutex count_array_mutex_;
std::vector<int> counter_array_( 100ish );

// Thread critical section
int counter_index = ... // unpredictable index
int current_count;
{
  std::lock_guard<std::mutex> lock(count_array_mutex_);
  current_count = counter_array_[counter_index] ++;
}
// ... do stuff using current_count.
//线程共享类成员
std::mutex count\u array\u mutex\u;
std::向量计数器数组(100ish);
//螺纹临界截面
int计数器_索引=…//不可预测指数
int当前_计数;
{
std::锁\保护锁(计数\数组\互斥锁);
当前计数=计数器数组[计数器索引]+;
}
// ... 使用当前的_计数做一些事情。
我希望多个线程能够同时获取和添加单独的数组元素

到目前为止,在我对
std::atomic
的研究中,我发现构造原子对象也会构造受保护的成员。(还有很多答案解释了为什么你不能做一个
std::vector

单向:

// Create.
std::vector<std::atomic<int>> v(100);
// Initialize.
for(auto& e : v)
    e.store(0, std::memory_order_relaxed);

// Atomically increment.
auto unpredictable_index = std::rand() % v.size();
int old = v[unpredictable_index].fetch_add(1, std::memory_order_relaxed);
C++20/C++2a(或任何你想称之为的东西)将添加一个功能,允许你对一个不是
原子的对象执行原子操作。

#include <vector>
#include <atomic>

#define Foo std   // this atomic_ref.hpp puts it in namespace Foo, not std.
// current raw url for https://github.com/ORNL/cpp-proposals-pub/blob/master/P0019/atomic_ref.hpp
#include "https://raw.githubusercontent.com/ORNL/cpp-proposals-pub/580934e3b8cf886e09accedbb25e8be2d83304ae/P0019/atomic_ref.hpp"


void inc_element(std::vector<int> &v, size_t idx)
{
    v[idx]++;
}

void atomic_inc_element(std::vector<int> &v, size_t idx)
{
    std::atomic_ref<int> elem(v[idx]);
    static_assert(decltype(elem)::is_always_lock_free,
           "performance is going to suck without lock-free atomic_ref<T>");

    elem.fetch_add(1, std::memory_order_relaxed);  // take your pick of memory order here
}
它是大多数编译器标准库的一部分,目前还不可用,但是对于带有GNU扩展的gcc/clang/ICC/其他编译器,它有一个有效的实现

以前,对“普通”数据的原子访问只能通过一些特定于平台的功能,如Microsoft或GNU C/C++
(GNU编译器C++库使用的相同构建体实现<代码> STD::原子)

包括一些关于动机的介绍。CPU可以很容易地做到这一点,编译器已经可以做到这一点,而恼人的是C++没有公开地展示这个能力。

因此,不必与C++进行角力以在构造函数中完成所有非原子分配和init,就可以让每个Access创建一个ActuixReF到要访问的元素。在任何“正常”C++实现中,它可以实例化为本地,至少在没有锁定时实例化。

这甚至可以让您在确保没有其他线程访问向量元素或控制块本身后,调整
std::vector
的大小。然后,您可以向其他线程发送信号以继续

它还没有在libstdc++或libc++for gcc/clang中实现。

#include <vector>
#include <atomic>

#define Foo std   // this atomic_ref.hpp puts it in namespace Foo, not std.
// current raw url for https://github.com/ORNL/cpp-proposals-pub/blob/master/P0019/atomic_ref.hpp
#include "https://raw.githubusercontent.com/ORNL/cpp-proposals-pub/580934e3b8cf886e09accedbb25e8be2d83304ae/P0019/atomic_ref.hpp"


void inc_element(std::vector<int> &v, size_t idx)
{
    v[idx]++;
}

void atomic_inc_element(std::vector<int> &v, size_t idx)
{
    std::atomic_ref<int> elem(v[idx]);
    static_assert(decltype(elem)::is_always_lock_free,
           "performance is going to suck without lock-free atomic_ref<T>");

    elem.fetch_add(1, std::memory_order_relaxed);  // take your pick of memory order here
}
原子版本是相同的,只是它使用了一个
前缀使读-修改-写原子化,以防您好奇原子在asm中是如何工作的

当然,大多数非x86 ISA(如AARC64)都需要LL/SC重试循环来实现原子RMW,即使在内存顺序宽松的情况下也是如此

这里的要点是,构造/销毁
原子\u ref
不需要任何成本。
它的成员指针完全优化了。因此,这与向量
一样便宜,但不令人头痛

只要小心不要通过调整向量大小或访问元素而不经过
原子\u ref
来创建数据竞争。(如果std::vector与另一个线程并行地重新分配内存并将其索引到内存中,那么它可能会在许多实际实现中表现为释放后使用,当然,您将以原子方式修改过时的副本。)


如果您不认真尊重
std::vector
对象本身不是原子的事实,并且编译器不会阻止您对底层
v[idx]进行非原子访问,那么这无疑会让您陷入困境
在其他线程开始使用它之后。

我认为第二种不使用向量的方法更好,因为resize()被破坏了。无论如何,就原子操作而言,很难理解调整和重新分配原子阵列的大小意味着什么。您可能会启用转换到一个跨度,以允许使用stl容器API?@GemTaylor我不认为可以在同时读取/更新向量的元素时调整向量的大小,因此,调整向量大小时不需要原子性。我猜@MaximEgorushkin的答案似乎可行。非常感谢。这有点令人讨厌,因为我必须在非线程区域的其他任何地方使用原子实例,在这些区域使用简单的整数数组[]元素更为自然。挂起任何其他方法。@NoahR:,它允许您在程序的一部分中执行无锁操作,在程序的其他部分中一次执行一个非原子线程的普通访问。e、 g.在确保没有其他线程访问该向量后,可以调整其大小,然后让其他线程继续,只要它们的所有访问都是通过
std::atomic_ref elem(vec[i])(至少在没有锁的情况下,应该可以自由实例化。)对于数据争用的标准语言而言,单独的数组元素已经是单独的内存位置。(只有在对“同一内存位置”的访问发生冲突时才会发生数据竞争)@BenVoigt我想我理解你的观点。我的用例试图说明多个线程在这个关键部分中具有相同的
counter\u index
值。因此,需要对每个元素进行原子访问。通常,同时匹配
计数器索引
的可能性远小于同时在关键部分中匹配多个线程的可能性。我猜想对元素的原子访问应该比将整个阵列数据结构锁定为单线程访问要快。是的,这是技术和可移植性的交叉点。许多流行的平台通过机器全局内存总线锁来保证原子性。这些不需要原子对象存储任何支持状态。但是,对大于平台原子原语支持的对象的原子访问是在使用锁的软件中实现的。这要求原子数据的所有处理程序都使用
inc_element(std::vector<int, std::allocator<int> >&, unsigned long):
    mov       rax, QWORD PTR [rdi]          # load the pointer member of std::vector
    add       DWORD PTR [rax+rsi*4], 1      # and index it as a memory destination
    ret

atomic_inc_element(std::vector<int, std::allocator<int> >&, unsigned long):
    mov       rax, QWORD PTR [rdi]
    lock add  DWORD PTR [rax+rsi*4], 1     # same but atomic RMW
    ret