宽松的原子商店在发布前是否重新排序? (类似于加载/获取)
Are relaxed atomic store reordered themselves before the release? (similar with load /acquire)
我阅读了 en.cppreference.com specifications 宽松的原子操作:
"[...]only guarantee atomicity and modification order
consistency."
所以,我在问自己,当你处理相同或不同的原子变量时,这样的 'modification order' 是否有效。
在我的代码中,我有一个原子树,其中一个低优先级、基于事件的消息线程填充应该更新的节点,使用 memory_order_relaxed
在红色“1”原子上存储一些数据(见图)。然后它继续使用 fetch_or 在其 parent 中写入以了解哪个 child 原子已更新。每个原子最多支持64位,所以我在红色操作'2'中填充位1。它连续继续,直到根原子也使用 fetch_or 标记,但这次使用 memory_order_release
.
然后一个快速、实时、不可阻塞的线程加载控制原子(memory_order_acquire
)并读取启用它的位。然后它用 memory_order_relaxed
递归地更新 childs 原子。这就是我将数据与高优先级线程的每个周期同步的方式。
由于此线程正在更新,因此可以 child 在其 parent 之前存储原子。问题是在我填充child信息之前,它存储了一个parent(填充children的位来更新)。
换句话说,就像标题说的那样,relaxed store 是在 release 之前重新排序的吗?我不介意 non-atomic 变量被重新排序。 Pseudo-code,假设 [x, y, z, control] 是原子的并且初始值为 0:
Event thread:
z = 1; // relaxed
y = 1; // relaxed
x = 1; // relaxed;
control = 0; // release
Real time thread (loop):
load control; // acquire
load x; // relaxed
load y; // relaxed
load z; // relaxed
我想知道在实时线程中是否总是这样:x <= y <=z。检查我是否写了这个小程序:
#define _ENABLE_ATOMIC_ALIGNMENT_FIX 1
#include <atomic>
#include <iostream>
#include <thread>
#include <assert.h>
#include <array>
using namespace std;
constexpr int numTries = 10000;
constexpr int arraySize = 10000;
array<atomic<int>, arraySize> tat;
atomic<int> tsync {0};
void writeArray()
{
// Stores atomics in reverse order
for (int j=0; j!=numTries; ++j)
{
for (int i=arraySize-1; i>=0; --i)
{
tat[i].store(j, memory_order_relaxed);
}
tsync.store(0, memory_order_release);
}
}
void readArray()
{
// Loads atomics in normal order
for (int j=0; j!=numTries; ++j)
{
bool readFail = false;
tsync.load(memory_order_acquire);
int minValue = 0;
for (int i=0; i!=arraySize; ++i)
{
int newValue = tat[i].load(memory_order_relaxed);
// If it fails, it stops the execution
if (newValue < minValue)
{
readFail = true;
cout << "fail " << endl;
break;
}
minValue = newValue;
}
if (readFail) break;
}
}
int main()
{
for (int i=0; i!=arraySize; ++i)
{
tat[i].store(0);
}
thread b(readArray);
thread a(writeArray);
a.join();
b.join();
}
工作原理:有一个原子数组。一个线程以相反的顺序以宽松的顺序存储,并以释放顺序结束存储一个控制原子。
另一个线程使用控制原子的获取顺序加载,然后它使用松散的原子加载数组的其余值。由于 parents 不能在 children 之前更新,newValue 应始终等于或大于 oldValue。
这个程序我已经在我的电脑上执行了好几次,debug和release,都没有触发失败。我使用的是普通的 x64 Intel i7 处理器。
因此,假设对多个原子的宽松存储至少在与控制原子和 acquire/release 同步时确实保持 'modification order' 是否安全?
关于放松给予修改顺序一致性的引用。仅意味着所有线程可以就那个一个对象的修改顺序达成一致。即存在订单。稍后与另一个线程中的获取加载同步的发布存储将保证它是可见的。 https://preshing.com/20120913/acquire-and-release-semantics/ 有一个很好的图表。
任何时候你存储一个其他线程可以加载和取消引用的指针,如果任何指向的数据最近也被修改,至少使用mo_release
, 如果有必要让读者也看到这些更新。 (这包括任何间接可达的东西,比如你的树的等级。)
在任何类型的树/链表/基于指针的数据结构上,几乎唯一可以使用 relaxed 的情况是在尚未 "published" 到其他节点的新分配节点中线程呢。 (理想情况下,您可以将 args 传递给构造函数,这样它们就可以被初始化,甚至根本不需要尝试成为原子;std::atomic<T>()
的构造函数本身不是原子的。因此,在发布指向新对象的指针时必须使用发布存储-构造原子对象。)
在 x86 / x86-64 上,mo_release
没有额外费用;普通 asm 存储已经具有与发布版一样强大的排序,因此编译器只需要阻止编译时重新排序即可实现 var.store(val, mo_release);
它在 AArch64 上也很便宜,特别是如果您不久之后不进行任何获取加载。
这也意味着您无法使用 x86 硬件测试 relaxed 是否不安全;编译器将在编译时为松弛存储选择一个顺序,以它选择的任何顺序将它们钉在发布操作中。 (并且 x86 原子 RMW 操作总是完全障碍,有效 seq_cst。在源代码中使它们变弱只允许编译时重新排序。一些非 x86 ISA 可以具有更便宜的 RMW 以及加载或存储较弱的订单,不过,即使 acq_rel 在 PowerPC 上也稍微便宜一些。)
遗憾的是,通过使用 x86_64 进行实验,您将对标准支持的内容知之甚少,因为 x86_64 的行为非常良好。特别是,除非您指定 _seq_cst:
所有读取有效_acquire
所有写入有效_release
除非它们跨越缓存行边界。并且:
- 所有读-修改-写都是有效的seq_cst
除了编译器(也)允许重新排序_relaxed操作。
你提到使用 _relaxed fetch_or... 如果我理解正确的话,你可能会失望地发现它并不比 便宜seq_cst,并且需要 LOCK
前缀指令,承载该指令的全部开销。
但是,是的 _relaxed 就顺序而言,原子操作与普通操作没有区别。所以是的,它们可能会被重新排序为其他 _relaxed 原子操作以及非原子操作——由编译器 and/or 机器。 [尽管如前所述,在 x86_64 上,而不是在机器上。]
并且,是的,在线程 X 中的释放操作与线程 Y 中的获取操作同步的情况下,线程 X 中所有在释放之前排序的写入将在线程 Y 中的获取之前发生。所以释放操作是一个信号,表明在 X 中它之前的所有写入都是 "complete",并且当获取操作看到该信号时,Y 知道它已经同步并且可以读取 X 写入的内容(直到释放)。
现在,这里要理解的关键是简单地存储 _release 是不够的,存储的 value 必须向负载 _acquire 发出存储已发生的明确信号。否则,负载如何判断?
通常像这样的_release/_acquire对用于同步访问一些数据集合。一旦该数据为 "ready",商店 _release 就会发出信号。看到信号的任何负载 _acquire(或看到信号的所有负载 _acquire)知道数据是 "ready",它们可以阅读。当然,任何写入存储 _release 之后的数据也可能(取决于时间)被负载 _acquire 看到.我在这里想说的是,如果要对数据进行进一步更改,可能需要 另一个 信号。
你的小测试程序:
将tsync
初始化为0
在作者中:毕竟tat[i].store(j, memory_order_relaxed)
,确实tsync.store(0, memory_order_release)
所以 tsync
的值没有改变 !
在reader中:在tat[i].load(memory_order_relaxed)
之前做tsync.load(memory_order_acquire)
并忽略从 tsync
读取的值
我在这里告诉你 _release/_acquire 对是 not 同步-- 所有这些 stores/load 也可能是 _relaxed。 [我认为如果作者设法保持领先 reader,您的测试将 "pass"。因为在 x86-64 上,所有写入都是按指令顺序完成的,所有读取也是如此。]
为了测试_release/_acquire语义,我建议:
将tsync
初始化为0,将tat[]
初始化为全零。
作者中:运行j = 1..numTries
在所有tat[i].store(j, memory_order_relaxed)
之后,写tsync.store(j, memory_order_release)
这表示传递已完成,所有 tat[]
现在都是 j
。
在reader中:做j = tsync.load(memory_order_acquire)
通过 tat[]
应该找到 j <= tat[i].load(memory_order_relaxed)
通过后,j == numTries
表示作者已完成。
这里writer发出的signal是刚写完j
,会继续j+1
,除非j == numTries
。但这不能保证tat[]
的书写顺序。
如果您想要编写器在每次通过后停止,并等待 reader 看到它并发出相同的信号——那么您需要另一个信号并且您需要线程等待它们各自的 "you may proceed" 信号。
我阅读了 en.cppreference.com specifications 宽松的原子操作:
"[...]only guarantee atomicity and modification order consistency."
所以,我在问自己,当你处理相同或不同的原子变量时,这样的 'modification order' 是否有效。
在我的代码中,我有一个原子树,其中一个低优先级、基于事件的消息线程填充应该更新的节点,使用 memory_order_relaxed
在红色“1”原子上存储一些数据(见图)。然后它继续使用 fetch_or 在其 parent 中写入以了解哪个 child 原子已更新。每个原子最多支持64位,所以我在红色操作'2'中填充位1。它连续继续,直到根原子也使用 fetch_or 标记,但这次使用 memory_order_release
.
然后一个快速、实时、不可阻塞的线程加载控制原子(memory_order_acquire
)并读取启用它的位。然后它用 memory_order_relaxed
递归地更新 childs 原子。这就是我将数据与高优先级线程的每个周期同步的方式。
由于此线程正在更新,因此可以 child 在其 parent 之前存储原子。问题是在我填充child信息之前,它存储了一个parent(填充children的位来更新)。
换句话说,就像标题说的那样,relaxed store 是在 release 之前重新排序的吗?我不介意 non-atomic 变量被重新排序。 Pseudo-code,假设 [x, y, z, control] 是原子的并且初始值为 0:
Event thread:
z = 1; // relaxed
y = 1; // relaxed
x = 1; // relaxed;
control = 0; // release
Real time thread (loop):
load control; // acquire
load x; // relaxed
load y; // relaxed
load z; // relaxed
我想知道在实时线程中是否总是这样:x <= y <=z。检查我是否写了这个小程序:
#define _ENABLE_ATOMIC_ALIGNMENT_FIX 1
#include <atomic>
#include <iostream>
#include <thread>
#include <assert.h>
#include <array>
using namespace std;
constexpr int numTries = 10000;
constexpr int arraySize = 10000;
array<atomic<int>, arraySize> tat;
atomic<int> tsync {0};
void writeArray()
{
// Stores atomics in reverse order
for (int j=0; j!=numTries; ++j)
{
for (int i=arraySize-1; i>=0; --i)
{
tat[i].store(j, memory_order_relaxed);
}
tsync.store(0, memory_order_release);
}
}
void readArray()
{
// Loads atomics in normal order
for (int j=0; j!=numTries; ++j)
{
bool readFail = false;
tsync.load(memory_order_acquire);
int minValue = 0;
for (int i=0; i!=arraySize; ++i)
{
int newValue = tat[i].load(memory_order_relaxed);
// If it fails, it stops the execution
if (newValue < minValue)
{
readFail = true;
cout << "fail " << endl;
break;
}
minValue = newValue;
}
if (readFail) break;
}
}
int main()
{
for (int i=0; i!=arraySize; ++i)
{
tat[i].store(0);
}
thread b(readArray);
thread a(writeArray);
a.join();
b.join();
}
工作原理:有一个原子数组。一个线程以相反的顺序以宽松的顺序存储,并以释放顺序结束存储一个控制原子。
另一个线程使用控制原子的获取顺序加载,然后它使用松散的原子加载数组的其余值。由于 parents 不能在 children 之前更新,newValue 应始终等于或大于 oldValue。
这个程序我已经在我的电脑上执行了好几次,debug和release,都没有触发失败。我使用的是普通的 x64 Intel i7 处理器。
因此,假设对多个原子的宽松存储至少在与控制原子和 acquire/release 同步时确实保持 'modification order' 是否安全?
关于放松给予修改顺序一致性的引用。仅意味着所有线程可以就那个一个对象的修改顺序达成一致。即存在订单。稍后与另一个线程中的获取加载同步的发布存储将保证它是可见的。 https://preshing.com/20120913/acquire-and-release-semantics/ 有一个很好的图表。
任何时候你存储一个其他线程可以加载和取消引用的指针,如果任何指向的数据最近也被修改,至少使用mo_release
, 如果有必要让读者也看到这些更新。 (这包括任何间接可达的东西,比如你的树的等级。)
在任何类型的树/链表/基于指针的数据结构上,几乎唯一可以使用 relaxed 的情况是在尚未 "published" 到其他节点的新分配节点中线程呢。 (理想情况下,您可以将 args 传递给构造函数,这样它们就可以被初始化,甚至根本不需要尝试成为原子;std::atomic<T>()
的构造函数本身不是原子的。因此,在发布指向新对象的指针时必须使用发布存储-构造原子对象。)
在 x86 / x86-64 上,mo_release
没有额外费用;普通 asm 存储已经具有与发布版一样强大的排序,因此编译器只需要阻止编译时重新排序即可实现 var.store(val, mo_release);
它在 AArch64 上也很便宜,特别是如果您不久之后不进行任何获取加载。
这也意味着您无法使用 x86 硬件测试 relaxed 是否不安全;编译器将在编译时为松弛存储选择一个顺序,以它选择的任何顺序将它们钉在发布操作中。 (并且 x86 原子 RMW 操作总是完全障碍,有效 seq_cst。在源代码中使它们变弱只允许编译时重新排序。一些非 x86 ISA 可以具有更便宜的 RMW 以及加载或存储较弱的订单,不过,即使 acq_rel 在 PowerPC 上也稍微便宜一些。)
遗憾的是,通过使用 x86_64 进行实验,您将对标准支持的内容知之甚少,因为 x86_64 的行为非常良好。特别是,除非您指定 _seq_cst:
所有读取有效_acquire
所有写入有效_release
除非它们跨越缓存行边界。并且:
- 所有读-修改-写都是有效的seq_cst
除了编译器(也)允许重新排序_relaxed操作。
你提到使用 _relaxed fetch_or... 如果我理解正确的话,你可能会失望地发现它并不比 便宜seq_cst,并且需要 LOCK
前缀指令,承载该指令的全部开销。
但是,是的 _relaxed 就顺序而言,原子操作与普通操作没有区别。所以是的,它们可能会被重新排序为其他 _relaxed 原子操作以及非原子操作——由编译器 and/or 机器。 [尽管如前所述,在 x86_64 上,而不是在机器上。]
并且,是的,在线程 X 中的释放操作与线程 Y 中的获取操作同步的情况下,线程 X 中所有在释放之前排序的写入将在线程 Y 中的获取之前发生。所以释放操作是一个信号,表明在 X 中它之前的所有写入都是 "complete",并且当获取操作看到该信号时,Y 知道它已经同步并且可以读取 X 写入的内容(直到释放)。
现在,这里要理解的关键是简单地存储 _release 是不够的,存储的 value 必须向负载 _acquire 发出存储已发生的明确信号。否则,负载如何判断?
通常像这样的_release/_acquire对用于同步访问一些数据集合。一旦该数据为 "ready",商店 _release 就会发出信号。看到信号的任何负载 _acquire(或看到信号的所有负载 _acquire)知道数据是 "ready",它们可以阅读。当然,任何写入存储 _release 之后的数据也可能(取决于时间)被负载 _acquire 看到.我在这里想说的是,如果要对数据进行进一步更改,可能需要 另一个 信号。
你的小测试程序:
将
tsync
初始化为0在作者中:毕竟
tat[i].store(j, memory_order_relaxed)
,确实tsync.store(0, memory_order_release)
所以
tsync
的值没有改变 !在reader中:在
之前做tat[i].load(memory_order_relaxed)
tsync.load(memory_order_acquire)
并忽略从
tsync
读取的值
我在这里告诉你 _release/_acquire 对是 not 同步-- 所有这些 stores/load 也可能是 _relaxed。 [我认为如果作者设法保持领先 reader,您的测试将 "pass"。因为在 x86-64 上,所有写入都是按指令顺序完成的,所有读取也是如此。]
为了测试_release/_acquire语义,我建议:
将
tsync
初始化为0,将tat[]
初始化为全零。作者中:运行
j = 1..numTries
在所有
tat[i].store(j, memory_order_relaxed)
之后,写tsync.store(j, memory_order_release)
这表示传递已完成,所有
tat[]
现在都是j
。在reader中:做
j = tsync.load(memory_order_acquire)
通过
tat[]
应该找到j <= tat[i].load(memory_order_relaxed)
通过后,
j == numTries
表示作者已完成。
这里writer发出的signal是刚写完j
,会继续j+1
,除非j == numTries
。但这不能保证tat[]
的书写顺序。
如果您想要编写器在每次通过后停止,并等待 reader 看到它并发出相同的信号——那么您需要另一个信号并且您需要线程等待它们各自的 "you may proceed" 信号。