自动更新整数数组元素C ++

时间:2019-02-05 15:18:24

标签: 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 = ... // 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::atomic<int>的研究中,我不相信构造原子对象 也会构造受保护的成员。 (还有很多答案说明了为什么您无法制作std::vector<std::atomic<int> >

2 个答案:

答案 0 :(得分:3)

一种方法:

def exampleFour(array,num):
    temp = []
    for i in array:
       if i % num == 0:
         temp.append(i)
    return temp

print(exampleFour([3,5,9,6,7,15,24],3))

请注意,// 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); 复制构造函数已删除,因此无法调整向量的大小,需要使用元素的最终计数来初始化向量。

由于丢失了std::atomic<>的调整大小功能,因此您最好使用std::vector,而不是std::vector,例如:

std::unique_ptr<std::atomic<int>[]>

答案 1 :(得分:3)

C ++ 20 / C ++ 2a(或您要调用的任何名称)将添加std::atomic_ref<T>,使您可以对非atomic<T>的对象执行原子操作开始。

作为大多数编译器的标准库的一部分,还不可用 ,但是gcc / clang / ICC /其他具有GNU扩展名的编译器有一个有效的实现。

以前,仅通过某些平台特定的功能(例如Microsoft的LONG InterlockedExchange(LONG volatile *Target, LONG Value);或GNU C / C ++)才能原子访问“普通”数据。 type __atomic_add_fetch (type *ptr, type val, int memorder)(GNU编译器的C ++库用于实现std::atomic<T>的内置函数。)

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0019r8.html包含一些有关动机的介绍性内容。 CPU可以轻松地做到这一点,编译器已经可以做到这一点,而C ++并没有移植性地展示这一功能,这很令人烦恼。

因此,您不必费心C ++即可在构造函数中完成所有非原子分配和初始化,而只需让每次访问都创建要访问的元素的atomic_ref。 (至少在无锁的情况下,可以在任何“常规” C ++实现中将其实例化为本地实例)。

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

尚未针对gcc / clang在libstdc ++或libc ++中实现。

#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
}

对于x86-64,它们完全按照我们希望使用GCC的方式进行编译, 使用C ++工作组提案中链接的示例实现(对于实现GNU扩展的编译器)。 https://github.com/ORNL/cpp-proposals-pub/blob/master/P0019/atomic_ref.hpp

From the Godbolt compiler explorer with g++8.2 -Wall -O3 -std=gnu++2a

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

原子版本是相同的,只是它使用lock前缀使读-修改-写原子by making sure no other core can read or write the cache line while this core is in the middle of atomically modifying it.成为可能,以防万一您好奇原子在asm中如何工作。

大多数非x86 ISA(例如AArch64)当然都需要LL / SC重试循环来实现原子RMW,即使内存顺序宽松。

这里的要点是,构建/销毁atomic_ref不会花费任何费用。其成员指针完全可以优化。因此,它的价格与vector<atomic<int>>一样便宜,但没有让人头疼。

只要您注意不要通过调整向量的大小或访问atomic_ref的元素来创建数据竞赛UB。 (如果std :: vector与另一个索引到其中的线程并行地重新分配内存,那么它在许多实际实现中可能会表现为“无用后使用”,当然,您将自动修改一个过时的副本。)

如果您不仔细考虑std::vector对象本身不是原子的事实,并且编译器不会阻止您对原子进行非原子访问,那么这肯定会让您绞尽脑汁。其他线程已开始使用基础v[idx]之后。