我想编写可移植的代码(Intel,ARM,PowerPC ...)来解决经典问题的变体:
Initially: X=Y=0
Thread A:
X=1
if(!Y){ do something }
Thread B:
Y=1
if(!X){ do something }
其中的目的是为了避免在这两个线程都在做的情况something
。(如果两者都不运行,这很好;这不是一次运行的机制。)如果您在下面的推理中发现一些缺陷,请更正我。
我知道,我可以通过memory_order_seq_cst
原子store
s和load
s 实现目标,如下所示:
std::atomic<int> x{0},y{0};
void thread_a(){
x.store(1);
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!x.load()) bar();
}
之所以能够达到目标,是因为
{x.store(1), y.store(1), y.load(), x.load()}
事件上必须有一些总订单,并且必须与程序订单的“优势”相符:
x.store(1)
“在TO之前”y.load()
y.store(1)
“在TO之前”x.load()
如果foo()
被调用,那么我们还有其他优势:
y.load()
“先读值”y.store(1)
如果bar()
被调用,那么我们还有其他优势:
x.load()
“先读值”x.store(1)
所有这些边缘结合在一起将形成一个循环:
x.store(1)
“在TO之前” y.load()
“先读取值” y.store(1)
“在TO之前” x.load()
“先读取值”x.store(true)
这违反了订单没有周期的事实。
我故意使用非标准术语“在TO之前”和“在之前读取值”,而不是像那样的标准术语happens-before
,因为我想征求有关我的假设的正确性的反馈,即这些边确实暗示着happens-before
关系,可以将它们合并为一个图,并且禁止这种组合图中的循环。我不确定。我所知道的是,这段代码对Intel gcc&clang和ARM gcc产生了正确的障碍
现在,我的实际问题更加复杂,因为我无法控制“ X”-它隐藏在某些宏,模板等后面,并且可能比 seq_cst
我什至不知道“ X”是单个变量还是其他概念(例如,轻量级信号灯或互斥量)。我所知道的是,我有两个宏set()
,check()
它们在另一个线程调用之后“ check()
返回true
” set()
。(这是也知道,set
和check
是线程安全的,也不能创建数据竞争UB)。
因此,从概念上讲,set()
它有点像“ X = 1”,check()
也很像“ X”,但是我无法直接访问所涉及的原子(如果有)。
void thread_a(){
set();
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!check()) bar();
}
我担心,这set()
可能会在内部实现x.store(1,std::memory_order_release)
和/或check()
可能在内部实现x.load(std::memory_order_acquire)
。或假设std::mutex
一个线程正在解锁而另一个线程正在解锁try_lock
;或者 在ISO标准std::mutex
中,仅保证具有获取和发布顺序,而不是seq_cst。
如果是这种情况,那么check()
就可以在此之前对主体进行“重新排序” y.store(true)
(请参阅Alex的答案,他们证明这是在PowerPC上发生的)。
这真的很糟糕,因为现在可能发生以下一系列事件:
thread_b()
首先加载x
(0
)的旧值thread_a()
执行包括foo()
thread_b()
执行包括bar()
所以,无论是foo()
和bar()
接到电话,我必须避免。我有什么选择可以防止这种情况发生?
选项A
尝试强行设置存储屏障。实际上,这可以通过以下方式实现std::atomic_thread_fence(std::memory_order_seq_cst);
-正如Alex在另一个答案中所解释的那样,所有经过测试的编译器都发出了完整的围栏:
- x86_64:MFENCE
- PowerPC:hwsync
- Itanuim:MF
- ARMv7 / ARMv8:dmb ish
- MIPS64:同步
这种方法的问题是,我在C ++规则中找不到任何保证,std::atomic_thread_fence(std::memory_order_seq_cst)
必须将其转换为完整的内存屏障。实际上,atomic_thread_fence
C ++ 中s 的概念似乎与内存屏障的汇编概念处于不同的抽象级别,并且更多地处理诸如“什么原子操作与什么同步”之类的问题。有没有理论上的证明可以实现以下目标?
void thread_a(){
set();
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!y.load()) foo();
}
void thread_b(){
y.store(true);
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!check()) bar();
}
选项B
通过对Y使用read-modify-write memory_order_acq_rel操作,使用对Y的控制来实现同步:
void thread_a(){
set();
if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
}
void thread_b(){
y.exchange(1,std::memory_order_acq_rel);
if(!check()) bar();
}
这里的想法是,对单个原子(y
)的访问必须形成所有观察者都同意的单一顺序,因此在访问fetch_add
之前exchange
或相反。
如果fetch_add
在之前,exchange
则的“发布”部分fetch_add
与的“获取”部分同步,exchange
因此的所有副作用对于set()
执行代码都必须是可见的check()
,因此bar()
不会被调用。
否则,exchange
在之前fetch_add
,fetch_add
则将看到1
并且不调用foo()
。因此,不可能同时调用foo()
和bar()
。这个推理正确吗?
选项C
使用虚拟原子,以引入防止灾难的“边缘”。考虑以下方法:
void thread_a(){
std::atomic<int> dummy1{};
set();
dummy1.store(13);
if(!y.load()) foo();
}
void thread_b(){
std::atomic<int> dummy2{};
y.store(1);
dummy2.load();
if(!check()) bar();
}
如果您认为这里的问题是atomic
s的,那么可以想象将它们移到全局范围,按照以下理由,这对我来说似乎并不重要,我特意编写了这样的代码,以显示dummy1有多有趣和dummy2完全分开。
为什么在地球上这可能起作用?好吧,必须有一些总顺序{dummy1.store(13), y.load(), y.store(1), dummy2.load()}
必须与程序顺序“边缘”一致:
dummy1.store(13)
“在TO之前”y.load()
y.store(1)
“在TO之前”dummy2.load()
(seq_cst存储+加载有望形成包括StoreLoad在内的完整内存屏障的C ++等效项,就像它们在真实ISA上的asm中所做的一样,甚至包括不需要单独的屏障指令的AArch64。)
现在,我们有两种情况需要考虑:在总顺序y.store(1)
之前y.load()
或之后。
如果y.store(1)
是之前,y.load()
那么foo()
就不会被调用,我们是安全的。
如果y.load()
为before y.store(1)
,则将其与我们在程序顺序中已经拥有的两个边结合起来,我们得出以下结论:
dummy1.store(13)
“在TO之前”dummy2.load()
现在,dummy1.store(13)
是释放操作,它释放的效果set()
,并且dummy2.load()
是获取操作,因此check()
应该看到的效果,set()
因此bar()
不会被调用,我们很安全。
认为check()
会看到的结果是否正确set()
?我可以像这样组合各种“边缘”吗(“程序顺序”又名“先后顺序”,“总顺序”,“发布前”,“获取后”)?我对此有严重的怀疑:C ++规则似乎在谈论同一位置上的存储和负载之间的“同步”关系-这里没有这种情况。
请注意,我们只担心在那里的情况dumm1.store
是已知的(通过其他推理)是之前dummy2.load
在seq_cst总订单。因此,如果他们一直在访问相同的变量,则负载将看到存储的值并与其进行同步。
(对于实现原子加载和存储至少编译为1向存储屏障(并且seq_cst操作无法重新排序:例如seq_cst存储不能传递seq_cst加载)的实现的内存屏障/重新排序推理是任何负载/之后的存储dummy2.load
肯定在之后的其他线程中可见,对于其他线程, y.store
类似地,在...之前)y.load
。
您可以在https://godbolt.org/z/u3dTa8上使用我的选项A,B,C的实现
foo()
和bar()
调用两者。
compare_exchange_*
对原子布尔执行RMW操作而无需更改其值(只需将期望值和新值设置为相同的值)。
atomic<bool>
具有exchange
和compare_exchange_weak
。后者可用于(通过尝试)CAS(true,true)或false,false来进行虚拟RMW。它要么失败,要么原子替换其自身的值。(在x86-64 asm中,该技巧lock cmpxchg16b
就是如何保证16原子的原子加载;效率低下,但比单独锁定更糟糕。)
foo()
不会发生也不会发生这种情况bar()
。我不想引入代码的许多“现实世界”元素,以避免出现“您认为自己有问题X但您有问题Y”的回答。但是,如果真的需要知道背景层set()
是什么:是some_mutex_exit()
,check()
是try_enter_some_mutex()
,y
是“有一些服务员”,foo()
是“退出而无需唤醒任何人”,bar()
是“等待上床” ...但是,我拒绝在这里讨论这个设计-我真的不能更改它。
std::atomic_thread_fence(std::memory_order_seq_cst)
确实编译成一个完整的障碍,但是由于整个概念都是实现细节,因此您找不到标准中对此的任何提及。(CPU内存模型通常是根据相对于顺序一致性所允许的重新分配来定义的。例如,x86是seq-cst +具有转发功能的存储缓冲区)