C ++ 11 atomic <>:仅可使用提供的方法进行读写?

问题描述 投票:0回答:1

我编写了一些多线程但无锁的代码,这些代码在较早的支持C ++ 11的GCC(7或更旧版本)上进行了编译并显然可以很好地执行。原子场是[CO],依此类推。尽我所能回忆,我在不需要原子性或事件顺序的地方使用了普通的C / C ++操作对其进行操作(int等)。

稍后,我必须执行一些双倍宽度的CAS操作,并像平常一样使用指针和计数器来制作一些结构。我尝试执行相同的常规C / C ++操作,但出现错误,指出该变量没有此类成员。 (这是您对大多数普通模板的期望,但是我半期望a=1;的工作方式有所不同,部分原因是为了支持atomic,尽我所能,对往返的正常分配提供了支持。) 。

所以有两个部分的问题:

  1. 我们应该在all情况下使用原子方法,甚至(说)由一个没有竞争条件的线程完成初始化吗? 1a)因此,一旦声明为原子,就无法以非原子方式访问吗? 1b)我们还必须使用int方法的详细程度吗?

  2. 否则,如果至少对于整数类型,我们可以使用常规的C / C ++操作。但是在这种情况下,这些操作将与atomic<> / load()相同还是仅仅是普通分配?

还有一个半元问题:关于store()变量为何不支持正常的C / C ++操作,是否有任何见解?我不确定C ++ 11语言是否具有编写规范的能力,但是规范肯定会要求编译器执行规范中的语言,但功能不足。

c++ c++11 stdatomic
1个回答
5
投票

您可能正在寻找atomic<>,以使您能够对也可以通过非原子方式访问的对象执行原子操作。确保声明的非原子C++20 std::atomic_ref<T>对象与std::atomic_ref<T>具有足够的对齐方式。例如

T

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

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

相关:atomic<sixteenbyte>是一个讨厌的联合黑客(在GNU C ++中为“安全”),可以有效地访问单个成员,因为编译器不会优化sizeof(atomic<T>) == sizeof(T)来自动加载该成员。相反,GCC和clang将How can I implement ABA counter with c++11 CAS?加载整个指针+计数器对,然后是第一个成员。 C ++ 20 foo.load().ptr应该可以解决这个问题。


访问lock cmpxchg16b的成员:不允许atomic_ref<>的原因之一是它的心智模型错误。如果两个不同的线程存储到同一结构的不同成员,那么该语言如何定义其他线程看到的顺序?另外,如果允许这样的事情,对程序员来说,错误地设计无锁算法可能很容易。

而且,您甚至将如何实现?返回左值引用?它不能是底层的非原子对象。而且,如果代码在调用某些未加载或存储的函数后捕获了该引用并保持使用很长时间,该怎么办?

请记住,ISO C ++的排序模型在与之同步方面起作用,而不是在本地重新排序和单个缓存一致性域(如真实ISA定义其内存模型的方式)方面起作用。 ISO C ++模型始终严格按照读取,写入或RMWing整个原子对象的方式进行。因此,对象的负载始终可以与整个对象的任何存储保持同步。

在实际的ISA上,如果整个对象都位于一个缓存行中,则该硬件实际上仍然可以存储到一个成员,并从另一个成员进行加载。至少我是这样认为的,尽管可能不在某些SMT系统上。 (在大多数ISA上,只有一条缓存行才能对整个对象进行无锁原子访问)。


我们还必须使用atomic <>方法的详细程度来做到这一点?

atomic<struct foo>的成员函数包括所有运算符的重载,包括shared.x = tmp;(存储)并强制转换为atomic<T>(加载)。 [operator=等效于Ta = 1;,并且是设置新值的最慢方法。

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

除了将args传递给a.store(1, std::memory_order_seq_cst)对象的构造函数外,您别无选择。

但是,[[

您可以在对象仍然是线程专用的同时使用atomic<int> a;加载/存储。避免使用任何RMW运算符,例如std::atomic<T>。例如mo_relaxed将与寄存器宽度或更小的非原子对象进行相同的编译。

(除了无法优化并把值保存在寄存器中,所以请使用局部临时变量,而不是实际更新原子对象。

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


原子字段是整数,依此类推。 ...并且显然执行得很好

如果您指的是普通的+=,而不是a.store(a.load(relaxed) + 1, relaxed);,那么它并不便携。

Data-race UB不保证可见的损坏,

具有未定义行为的讨厌的事情是,在您的测试用例中发生的事情是允许发生的事情之一

并且在许多情况下,如果使用纯负载或纯存储,则不会中断,尤其是在订购有序的x86上,除非负载或存储可以从循环中提升或下沉。 int。但是,当编译器设法进行跨文件内联并在编译时对某些操作重新排序时,它最终会咬住您。


为什么原子<>变量不支持正常的C / C ++操作?...但是规范肯定会要求编译器按照规范的语言执行功能不足。

实际上,这是C ++ 11到17的限制。大多数编译器对此都没有问题。例如,gcc / clang的atomic<int>标头的实现使用Why is integer assignment on a naturally aligned variable atomic on x86?

<atomic>的C ++ 20提案是__atomic_ builtins which take a plain T* pointer,引用为动机:

在明确定义的阶段中,非原子性地可能大量使用对象应用程序。强迫此类对象仅是原子的造成不必要的性能损失。

3.2。超大型阵列成员的原子操作

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

所有这些都是__atomic_的问题,证实了您的怀疑,这是C ++ 11的问题。

[他们没有引入对T*进行非原子访问的方法,而是引入了对atomic_ref对象进行原子访问的方法。与此相关的一个问题是p0019可能需要比默认情况下std::atomic<>更高的对齐方式,因此请小心。

与赋予

atomic

std::atomic<T>成员的访问权限不同,您可能有一个T成员函数可以返回对基础对象的左值引用。
© www.soinside.com 2019 - 2024. All rights reserved.