如图所示,使用这些具有宽松内存顺序和 rel/acq 的原子操作,此 C++ 代码段是否有效?
Using these atomic operations with memory order relaxed and rel/acq as shown, does this C++ snippet work?
写在cppmem伪代码中:
int main()
{
atomic_int n = -1;
atomic_int n2 = 0;
{{{
{
n2.store(1, mo_relaxed);
if (n.load(mo_relaxed) != -1)
n.store(1, mo_release);
}
|||
{
n.store(0, mo_release);
int expected = 0;
do
{
desired = n2.load(mo_relaxed);
}
while (!n.compare_exchange_strong(&expected, desired, mo_acquire));
}
}}}
assert(n == 1);
}
换句话说,两个原子变量被初始化为n = -1和n2 = 0;
线程 1 首先将 1 写入 n2,然后写入 n,前提是 n 不是(仍然)-1。
线程 2 首先将 0 写入 n,然后加载 n2 并分配 n = n2,只要自上次读取 n 以来(或当 n 仍为 0 时)n 未更改。
两个线程加入后,n 必须在每次可能的执行中都等于 1。
此代码是我的一个开源项目的一部分,涉及将 streambuf 实现重置为无锁缓冲区的开头,同时两个线程同时读取和写入它。此特定部分与 'sync-ing'(或刷新写入输出)有关。
我设计了这个,它在每个操作顺序一致时工作(这是蛮力测试),但我无法理解内存顺序要求:/。
如果按以下顺序执行指令(和缓存更新),则可能会触发此断言:
- 第一个线程 运行 它的所有指令。所以它只是将
n2
的值从 0
更改为 1
.
- 然后线程 2 运行s。首先它将
n
的值从 -1
更改为 0
.
- 然后线程 2 加载
n2
(在 n2.load(mo_relaxed)
中)。此时没有同步,因此可以加载之前存储在 n2
中的任何值(包括初始化值,请参阅 [intro.race]/1)。假设它加载 0
.
- 所以threads 2的变量值分别是
n==0
(n
修改顺序的最后一个),n2==0
,expected==0
,desired==0
之前比较交换指令。然后比较交换成功,将0
存入n
.
在两个线程的执行结束时,你得到 n==0
和 n2==1
。
由于顺序一致性,我所描述的情况不会发生,因为如果线程 1 看到 n2==1 && n==-1
,线程 2 就看不到 n2==0 && n==0
。
对于这个算法,我确信除了顺序一致性之外不可能使用任何其他内存顺序。
使用我在 https://plv.mpi-sws.org/rcmc/ 上找到的工具
我能够通过实验发现最宽松的要求是:
线程 1:
n2.store(1, std::memory_order_seq_cst);
if (n.load(std::memory_order_seq_cst) != -1)
n.store(1, std::memory_order_release);
线程 2:
n.store(0, std::memory_order_seq_cst);
int expected = 0;
int desired;
do
{
desired = n2.load(std::memory_order_seq_cst);
}
while (!n.compare_exchange_strong(expected, desired,
std::memory_order_acquire, std::memory_order_relaxed));
编辑:
同一作者的更新工具(当然也更好)
现在可以从 https://github.com/MPI-SWS/genmc
下载
事实证明,该工具非常快速且有用,即使对于测试使用弱序原子的真实算法也是如此,例如我在这里所做的:genmc_buffer_reset_test.c
给定行的#include 是生成的 C 文件,从我的 C++ 代码中提取并使用 awk 脚本转换为 C,因为在我看来 genmc 只能(不幸地)用于 C 代码(?) .
写在cppmem伪代码中:
int main()
{
atomic_int n = -1;
atomic_int n2 = 0;
{{{
{
n2.store(1, mo_relaxed);
if (n.load(mo_relaxed) != -1)
n.store(1, mo_release);
}
|||
{
n.store(0, mo_release);
int expected = 0;
do
{
desired = n2.load(mo_relaxed);
}
while (!n.compare_exchange_strong(&expected, desired, mo_acquire));
}
}}}
assert(n == 1);
}
换句话说,两个原子变量被初始化为n = -1和n2 = 0;
线程 1 首先将 1 写入 n2,然后写入 n,前提是 n 不是(仍然)-1。
线程 2 首先将 0 写入 n,然后加载 n2 并分配 n = n2,只要自上次读取 n 以来(或当 n 仍为 0 时)n 未更改。
两个线程加入后,n 必须在每次可能的执行中都等于 1。
此代码是我的一个开源项目的一部分,涉及将 streambuf 实现重置为无锁缓冲区的开头,同时两个线程同时读取和写入它。此特定部分与 'sync-ing'(或刷新写入输出)有关。
我设计了这个,它在每个操作顺序一致时工作(这是蛮力测试),但我无法理解内存顺序要求:/。
如果按以下顺序执行指令(和缓存更新),则可能会触发此断言:
- 第一个线程 运行 它的所有指令。所以它只是将
n2
的值从0
更改为1
. - 然后线程 2 运行s。首先它将
n
的值从-1
更改为0
. - 然后线程 2 加载
n2
(在n2.load(mo_relaxed)
中)。此时没有同步,因此可以加载之前存储在n2
中的任何值(包括初始化值,请参阅 [intro.race]/1)。假设它加载0
. - 所以threads 2的变量值分别是
n==0
(n
修改顺序的最后一个),n2==0
,expected==0
,desired==0
之前比较交换指令。然后比较交换成功,将0
存入n
.
在两个线程的执行结束时,你得到 n==0
和 n2==1
。
由于顺序一致性,我所描述的情况不会发生,因为如果线程 1 看到 n2==1 && n==-1
,线程 2 就看不到 n2==0 && n==0
。
对于这个算法,我确信除了顺序一致性之外不可能使用任何其他内存顺序。
使用我在 https://plv.mpi-sws.org/rcmc/ 上找到的工具 我能够通过实验发现最宽松的要求是:
线程 1:
n2.store(1, std::memory_order_seq_cst);
if (n.load(std::memory_order_seq_cst) != -1)
n.store(1, std::memory_order_release);
线程 2:
n.store(0, std::memory_order_seq_cst);
int expected = 0;
int desired;
do
{
desired = n2.load(std::memory_order_seq_cst);
}
while (!n.compare_exchange_strong(expected, desired,
std::memory_order_acquire, std::memory_order_relaxed));
编辑:
同一作者的更新工具(当然也更好) 现在可以从 https://github.com/MPI-SWS/genmc
下载事实证明,该工具非常快速且有用,即使对于测试使用弱序原子的真实算法也是如此,例如我在这里所做的:genmc_buffer_reset_test.c
给定行的#include 是生成的 C 文件,从我的 C++ 代码中提取并使用 awk 脚本转换为 C,因为在我看来 genmc 只能(不幸地)用于 C 代码(?) .