#include <thread>
#include <atomic>
#include <iostream>
#include <cassert>
struct RMW_Ordering
{
std::atomic<bool> flag {false};
std::atomic<unsigned> done {0}, counter {0};
unsigned race_cancel {0}, race_success {0}, sum {0};
void thread1() // fail
{
race_cancel = 1; // data produced
if (counter.fetch_add(1, std::memory_order_release) == 1 &&
!flag.exchange(true, std::memory_order_relaxed))
{
counter.store(0, std::memory_order_relaxed);
done.store(1, std::memory_order_relaxed);
}
}
void thread2() // success
{
race_success = 1; // data produced
if (counter.fetch_add(1, std::memory_order_release) == 1 &&
!flag.exchange(true, std::memory_order_relaxed))
{
done.store(2, std::memory_order_relaxed);
}
}
void thread3()
{
while (!done.load(std::memory_order_relaxed)); // livelock test
counter.exchange(0, std::memory_order_acquire);
sum = race_cancel + race_success;
}
};
int main()
{
for (unsigned i = 0; i < 1000; ++i)
{
RMW_Ordering test;
std::thread t1([&]() { test.thread1(); });
std::thread t2([&]() { test.thread2(); });
std::thread t3([&]() { test.thread3(); });
t1.join();
t2.join();
t3.join();
assert(test.counter == 0);
}
std::cout << "Done!" << std::endl;
}
两个线程竞争进入一个受保护的区域,最后一个修改“done”,从无限循环中释放第三个线程。这个例子有些牵强,但原始代码需要通过“flag”来声明此区域以发出“完成”的信号。
最初,“fetch_add”具有“acq_rel”排序,因为我担心交换可能在它之前重新排序,可能导致一个线程声明标志,首先尝试“fetch_add”检查,并防止另一个线程(通过增量检查)成功地修改计划。在使用Relacy进行测试时,我想看看如果将“acq_rel”改为“release”,我预期会发生的活锁是否会发生,令我惊讶的是,没有发生。然后我对所有内容都使用了“relaxed”,再次没有活锁。
我试图在C++标准中找到任何关于此的规则,但只找到了以下内容:
1.10.7 此外,还有宽松的原子操作,并不是同步操作,还有原子读取修改写入操作,具有特殊的特性。
29.3.11 原子读取修改写入操作应始终读取与其关联的写入之前(在修改顺序中)写入的最后一个值。
我是否可以始终依赖RMW操作不被重新排序 - 即使它们影响不同的内存位置 - 并且标准中是否有保证这种行为的内容?
编辑:
我想出了一个更简单的设置,应该能更好地解释我的问题。以下是该CppMem脚本:
int main()
{
atomic_int x = 0; atomic_int y = 0;
{{{
{
if (cas_strong_explicit(&x, 0, 1, relaxed, relaxed))
{
cas_strong_explicit(&y, 0, 1, relaxed, relaxed);
}
}
|||
{
if (cas_strong_explicit(&x, 0, 2, relaxed, relaxed))
{
cas_strong_explicit(&y, 0, 2, relaxed, relaxed);
}
}
|||
{
// Is it possible for x and y to read 2 and 1, or 1 and 2?
x.load(relaxed).readsvalue(2);
y.load(relaxed).readsvalue(1);
}
}}}
return 0;
}
我认为该工具并不足够复杂,无法评估这种情况,但它似乎表明这是可能的。以下是几乎相当的Relacy设置:
#include "relacy/relacy_std.hpp"
struct rmw_experiment : rl::test_suite<rmw_experiment, 3>
{
rl::atomic<unsigned> x, y;
void before()
{
x($) = y($) = 0;
}
void thread(unsigned tid)
{
if (tid == 0)
{
unsigned exp1 = 0;
if (x($).compare_exchange_strong(exp1, 1, rl::mo_relaxed))
{
unsigned exp2 = 0;
y($).compare_exchange_strong(exp2, 1, rl::mo_relaxed);
}
}
else if (tid == 1)
{
unsigned exp1 = 0;
if (x($).compare_exchange_strong(exp1, 2, rl::mo_relaxed))
{
unsigned exp2 = 0;
y($).compare_exchange_strong(exp2, 2, rl::mo_relaxed);
}
}
else
{
while (!(x($).load(rl::mo_relaxed) && y($).load(rl::mo_relaxed)));
RL_ASSERT(x($) == y($));
}
}
};
int main()
{
rl::simulate<rmw_experiment>();
}
断言从未被违反,因此根据Relacy的说法,1和2(或其反向)是不可能的。