C++11 atomic<>:只能用提供的方法读/写?

Swi*_*ank 0 c++ c++11 stdatomic

我编写了一些多线程但无锁的代码,这些代码在较早的支持 C++11 的 GCC(7 或更早版本)上编译并显然执行得很好。原子场是ints 等等。据我所知,我使用普通的 C/C++ 操作a=1;在原子性或事件排序不是问题的地方对它们(等)进行操作。

后来我不得不做一些双宽 CAS 操作,并像常见的那样制作了一个带有指针和计数器的小结构。我尝试执行相同的普通 C/C++ 操作,但出现了变量没有此类成员的错误。(这是您对大多数普通模板的期望,但我半期望atomic以不同的方式工作,部分原因是支持正常的往返分配,据我所知,对于ints。)。

所以两部分问题:

  1. 我们是否应该在所有情况下使用原子方法,甚至(比如)由一个没有竞争条件的线程完成的初始化?1a) 所以一旦声明为 atomic 就无法非原子地访问?1b)我们还必须使用atomic<>方法的冗长冗长来做到这一点?

  2. 否则,如果至少对于整数类型,我们可以使用普通的 C/C++ 操作。但在这种情况下,这些操作是否与load()/相同,store()或者它们只是正常的分配?

还有一个半元问题:是否有任何关于为什么atomic<>变量不支持普通 C/C++ 操作的见解?我不确定规范中的 C++11 语言是否有能力编写那样的代码,但规范肯定会要求编译器做一些事情,规范中的语言不够强大.

Pet*_*des 6

您可能正在寻找C++20std::atomic_ref<T>来使您能够对也可以非原子访问的对象执行原子操作。确保您的非原子T对象声明为atomic<T>. 例如

alignas(atomic<long long>)  long long  sometimes_shared_var;
Run Code Online (Sandbox Code Playgroud)

但这需要 C++20,在 C++17 或更早版本中没有任何等效可用。一旦构造了原子对象,我认为除了原子成员函数之外,没有任何保证安全的方法来修改它。

标准不保证它的内部对象表示,因此即使没有其他线程引用它,标准也不能保证有效地memcpystruct sixteenbyte对象中取出对象atomic<sixteenbyte>是安全的。您必须知道特定实现如何存储它。不过,检查sizeof(atomic<T>) == sizeof(T)是一个好兆头。

相关:如何使用 c++11 CAS 实现 ABA 计数器?对于一个讨厌的联合黑客(GNU C++ 中的“安全”)来提供对单个成员的有效访问,因为编译器不会优化foo.load().ptr以原子方式加载该成员。相反,GCC 和 clang 将lock cmpxchg16b加载整个指针+计数器对,然后只加载第一个成员。C++20atomic_ref<>应该可以解决这个问题。


访问成员atomic<struct foo>: 不允许的一个原因shared.x = tmp;是它是错误的心智模型。如果两个不同的线程存储到同一个结构的不同成员,该语言如何定义其他线程看到的任何顺序?此外,如果允许这样的事情,程序员可能会认为错误地设计他们的无锁算法太容易了。

另外,您将如何实施?返回左值引用?它不能是底层的非原子对象。如果代码捕获该引用并在调用某个不是加载或存储的函数后长时间继续使用它会怎样?

请记住,ISO C++ 的排序模型在同步方面工作,而不是在本地重新排序和单个缓存一致域方面工作,就像真正的 ISA 定义其内存模型的方式一样。ISO C++ 模型在读取、写入或 RMWing 整个原子对象方面始终严格。因此,对象的加载始终可以与整个对象的任何存储同步。

在现实世界的 ISA 上,如果整个对象位于一个缓存行中,那么实际上仍然可以用于存储到一个成员并从另一个成员加载的硬件中。至少我是这么认为的,尽管在某些 SMT 系统上可能不是。(为了在大多数 ISA 上实现对整个对象的无锁原子访问,需要在一个缓存行中。)


我们还必须使用 atomic<> 方法的冗长冗长来做到这一点?

的成员函数atomic<T>包括所有运算符的重载,包括operator=(store) 和 cast back to T(load)。 a = 1;相当于a.store(1, std::memory_order_seq_cst)foratomic<int> a;并且是设置新值的最慢方法。

我们是否应该在所有情况下都使用原子方法,甚至(比如)由一个没有竞争条件的线程完成的初始化?

除了将 args 传递给std::atomic<T>对象的构造函数之外,您别无选择。

不过,您可以mo_relaxed在对象仍为线程私有时使用加载/存储。避免使用任何 RMW 运算符,例如+=. 例如,a.store(a.load(relaxed) + 1, relaxed);将编译与寄存器宽度或更小的非原子对象大致相同。

(除了它无法优化并将值保留在寄存器中,因此使用本地临时对象而不是实际更新原子对象)。

但是对于太大而不能无锁的原子对象,除了首先用正确的值构造它们之外,实际上没有什么可以有效地做的。


原子字段是整数等。...
显然执行得很好

如果你的意思是 plain intatomic<int>那么它不是便携式安全的。

数据竞争 UB 不保证可见的损坏,具有未定义行为的令人讨厌的事情是,在您的测试用例中发生的事情是允许发生的事情之一

在许多纯负载或纯存储的情况下,它不会中断,尤其是在强有序的 x86 上,除非负载或存储可以提升或沉入循环之外。 为什么在 x86 上自然对齐的变量原子上的整数赋值是原子的?. 但是,当编译器在编译时设法进行跨文件内联并重新排序某些操作时,它最终会咬你。


为什么原子<> 变量不支持普通的 C/C++ 操作?
...但规范肯定会要求编译器做一些语言作为规范不够强大的事情。

这实际上是 C++11 到 17 的限制。大多数编译器都没有问题。例如<atomic>,gcc/clang 的头文件的实现使用采用__atomic_普通T*指针的内置函数

C++20 的提议atomic_refp0019,它引用作为动机:

对象可以在应用程序的明确定义的阶段中以非原子方式大量使用。强制这些对象完全是原子的会导致不必要的性能损失。

3.2. 对超大数组成员的原子操作

高性能计算 (HPC) 应用程序使用非常大的阵列。对这些数组的计算通常具有不同的阶段,用于分配和初始化数组成员、更新数组成员和读取数组成员。分配成员值时,用于初始化(例如,零填充)的并行算法具有非冲突访问。用于更新的并行算法对必须由原子操作保护的成员的访问存在冲突。具有只读访问权限的并行算法需要性能最佳的流式读取访问、随机读取访问、矢量化或其他有保证的非冲突 HPC 模式。

所有这些都是 的问题std::atomic<>,证实了您怀疑这是 C++11 的问题。

他们没有引入对 进行非原子访问的方法,而是引入了std::atomic<T>T对象进行原子访问的方法。这样做的一个问题是atomic<T>可能需要比T默认情况下更多的对齐,所以要小心。

与对 的成员进行原子访问不同T,您可能有一个.non_atomic()成员函数返回对底层对象的左值引用。