ixS*_*Sci 17 c++ multithreading atomic
有一个流行的自旋锁互斥锁版本在互联网上传播,人们可能会在Anthony Williams的书(C++ Concurrency in Action)中遇到这种版本.这里是:
class SpinLock
{
std::atomic_flag locked;
public:
SpinLock() :
locked{ATOMIC_FLAG_INIT}
{
}
void lock()
{
while(locked.test_and_set(std::memory_order_acquire));
}
void unlock()
{
locked.clear(std::memory_order_release);
}
};
Run Code Online (Sandbox Code Playgroud)
我不明白的是为什么每个人都使用std::memory_order_acquire
的test_and_set
是一个RMW操作.为什么不std::memory_acq_rel
呢?假设我们有2个线程同时尝试获取锁:
T1: test_and_set -> ret false
T2: test_and_set -> ret false
Run Code Online (Sandbox Code Playgroud)
这种情况应该是可能的,因为我们有两个彼此之间acquire
没有任何sync with
关系的操作.是的,在我们解锁了互斥锁之后,我们进行了release
一项后续操作,release sequence
生活变得丰富多彩,每个人都很开心.但为什么在release sequence
前进之前它是安全的?
由于很多人都提到了这个实现,我认为它应该可以正常工作.那我错过了什么?
更新1:
我完全理解操作是原子的,操作之间的操作lock
并unlock
不能超出临界区.这不是问题.问题是我没有看到上面的代码如何防止2个互斥锁同时进入临界区.为了防止它发生,应该在2 秒之间的关系之前发生lock
.有人能告诉我,使用C++标准概念,代码是完全安全的吗?
更新2:
好吧,我相信,我们接近正确的答案.我在标准中找到了以下内容:
[atomics.order]第11条
原子读 - 修改 - 写操作应始终读取与读 - 修改 - 写操作相关的写操作之前写入的最后一个值(按修改顺序).
在这个重要的说明中,我可以愉快地结束这个问题,但我仍然怀疑.in the modification order
部分怎么样?标准非常明确:
[intro.multithread]第8条
对特定原子对象M的所有修改都以某种特定的总顺序出现,称为M的修改顺序.如果A和B是原子对象M的修改,并且A 发生在 B 之前(如下面定义的那样)B,那么A应该以M的修改顺序在B之前,其定义如下.
因此,根据RMW操作具有最新写入值的该子句,最新的写操作应该在读取部分或RMW操作之前发生.在问题中不是这种情况.对?
更新3:
我越来越认为自旋锁的代码被破坏了.这是我的推理.C++指定了3种类型的操作:
让我们从RMW开始,找出它们的特别之处.首先,它们是形成的宝贵资产release sequence
,其次它们具有上面引用的特殊条款([atomics.order]第11条).我发现没什么特别的.
获取/发布是同步操作,release sync with acquire
因此形成happens before
关系.轻松的操作只是简单的原子,根本不参与修改顺序.
我们的代码中有什么?我们有一个使用获取内存语义的RMW操作,因此每当第一次解锁(释放)时,它就有两个角色:
sync with
与以前的关系形成了一种关系release
release sequence
.但只有在第一次unlock
完成之后,这才是真的.在此之前,如果我们有2个以上的线程同时运行我们的lock
代码,那么我们可以lock
同时输入传递,因为2个acquire
操作不会形成任何关系.它们和放松的操作一样无序.由于它们是无序的,因此我们不能使用任何关于RMW操作的特殊条款,因为没有happens before
关系,因此没有locked
标志的修改顺序.
所以要么我的逻辑存在缺陷,要么代码被破坏.请知道真相的人 - 对此发表评论.
Dav*_*rtz 14
我认为你所缺少的test_and_set
是原子,时期.没有内存排序设置使此操作不是原子操作.如果我们需要的只是一个原子测试和设置,我们可以指定任何内存排序.
但是,在这种情况下,我们需要的不仅仅是原子"测试和设置"操作.我们需要确保在我们确认锁定是我们要执行的操作之后执行的内存操作在我们观察到要解锁的互斥锁之前没有重新排序.(因为那些操作不是原子操作.)
考虑:
什么是不可能发生的事情?这是步骤6和7中的读取和写入以某种方式在步骤5之前被重新排序,踩在另一个线程上,在互斥锁的保护下访问共享数据.
该test_and_set
操作已经是原子操作,因此步骤4和5本质上是安全的.并且步骤1和2不能修改受保护的数据(因为它们在我们甚至尝试锁定之前发生)因此在我们的锁定操作周围重新排序它们没有任何害处.
但是步骤6和7 - 在我们观察锁被解锁之前不得重新排序,以便我们可以原子地锁定它.那将是一场灾难.
memory_order_acquire的定义:" 具有此内存顺序的加载操作会对受影响的内存位置执行获取操作:在此加载之前,不能对当前线程中的内存访问进行重新排序. "
正是我们需要的.
dav*_*mac 12
有人能告诉我,使用C++标准概念,代码是完全安全的吗?
我最初和你有同样的担忧.我认为关键是要理解std::atomic_flag
变量上的操作对于所有处理器/核心都是原子的.无论指定的内存顺序如何,在单独的线程中的两个原子"测试和设置"操作不能同时成功,因为它们不能是原子的; 该操作必须应用于实际变量,而不是缓存的本地副本(我认为,这甚至不是C++中的概念).
完整的推理链:
29.7 p5(谈论测试和设置操作):
效果:以原子方式将object指向的值或此值设置为true.内存受到订单价值的影响.这些操作是原子读 - 修改 - 写操作(1.10).返回:原子地,在效果之前的对象的值.
1.10 p6:
对特定原子对象M的所有修改都以某种特定的总顺序出现,称为M的修改顺序...
因此,如果在这种情况下两个线程试图同时锁定自旋锁,则其中一个必须是第一个而另一个必须是第二个.我们现在只需要表明第二个必须返回标志已经设置,从而阻止该线程进入临界区.
第6段继续说:
......如果A和B是原子对象的修改M和A发生在B之前(如下定义)B,则A应在M的修改顺序中位于B之前,其定义如下.[注意:这表明修改订单必须遵守"之前发生"关系. - 结束说明]
在两个线程中发生的两个测试和设置操作之间没有"发生之前"关系,因此我们无法确定哪个在修改顺序中排在第一位; 然而,由于p6中的第一句话(其中表明存在总排序),一定必须先出现在另一个之前.现在,从29.3 p12:
原子读 - 修改 - 写操作应始终读取与读 - 修改 - 写操作相关的写操作之前写入的最后一个值(按修改顺序).
这表明第二次测试和设置必须首先看到由测试和设置写入的值.任何获取/释放选择都不会影响这一点.
因此,如果"同时"执行两个测试和设置操作,它们将被赋予任意顺序,第二个将看到由第一个设置的标志值.因此,为测试和设置操作指定的存储器顺序约束无关紧要; 它们用于在获取自旋锁的期间控制对其他变量的写入顺序.
回答问题的"更新2":
因此,根据RMW操作具有最新写入值的该子句,最新的写操作应该在读取部分或RMW操作之前发生.在问题中不是这种情况.对?
纠正没有"发生在之前"的关系,但不正确的是RMW操作需要这样的关系以保证最新的书面价值.您列为"[atomics.order]子句11"的语句不需要"之前发生"关系,只是一个操作在原子标志的"修改顺序"中位于另一个操作之前.第8条规定将有这样的命令,它将是一个总排序:
对特定原子对象M的所有修改都以某种特定的总顺序出现,称为M的修改顺序...
...然后继续说总排序必须与任何"之前发生的"关系一致:
......如果A和B是原子对象的修改M和A发生在B之前(如下定义)B,则A应在M的修改顺序中位于B之前,其定义如下.
然而,在没有"之前发生"关系的情况下,仍然存在总排序 - 只是这种排序具有一定程度的随意性.也就是说,如果A和B之间没有"之前发生"关系,则不指定A是在B之前还是之后排序.但它必须是一个或另一个,因为存在特定的总顺序.
为什么需要memory_order_acquire呢?
诸如自旋锁之类的互斥锁通常用于保护其他非原子变量和数据结构.memory_order_acquire
锁定自旋锁时使用确保从这些变量读取将看到正确的值(即由先前保持自旋锁的任何其他线程写入的值).对于解锁,memory_order_release
还需要允许其他线程查看写入的值.
获取/释放都阻止编译器在锁的获取/释放之后重新排序读/写,并确保生成任何必要的指令以确保适当级别的高速缓存一致性.
进一步证据:
首先,从29.3开始的这个说明:
注意:指定memory_order_relaxed的原子操作在内存排序方面是放宽的.实现必须仍然保证对特定原子对象的任何给定原子访问对于该对象的所有其他原子访问都是不可分割的. - 结束说明
这基本上是说指定的内存排序不会影响原子操作本身.访问必须"与所有其他原子访问不可分割",包括来自其他线程的访问.允许两个测试和设置操作读取相同的值将有效地划分它们中的至少一个,以便它不再是原子的.
另外,从1.10第5段开始:
此外,还有轻松的原子操作,它们不是同步操作,还有原子读 - 修改 - 写操作,它们具有特殊的特性.
(测试和设置属于后一类),尤其是:
"轻松"原子操作不是同步操作,即使像同步操作一样,它们也无法为数据竞争做出贡献.
(强调我的).两个线程同时执行原子测试和设置(并且都执行'set'部分)的情况将是这样的数据竞争,因此该文本再次表明这不会发生.
1.10 p8:
注意:同步操作的规范定义何时读取另一个写入的值.对于原子对象,定义很清楚.
这意味着一个线程读取另一个线程写入的值.它说对于原子对象,定义是明确的,这意味着不需要其他同步 - 它足以对原子对象执行操作; 其他线程会立即看到效果.
特别是,1.10 p19:
[注意:前面的四个一致性要求有效地禁止编译器将原子操作重新排序到单个对象,即使两个操作都是放松加载.这有效地使大多数硬件提供的高速缓存一致性保证可用于C++原子操作. - 结束说明]
请注意,即使存在宽松的负载,也要提及缓存一致性.这清楚地表明,测试和设置一次只能在一个线程中成功,因为对于一个线程失败,高速缓存一致性被破坏或操作不是原子的.