“在此之前发生”是什么意思?


9

在C ++草案标准中多次使用“强烈发生于”这一短语。

例如:终止 [basic.start.term] / 5

如果具有静态存储持续时间的对象的初始化强烈发生在对std :: atexit的调用之前(请参见[support.start.term]),则对该函数的调用将传递给std :: atexit在调用对象的析构函数之前进行排序。如果对std :: atexit的调用强烈发生在具有静态存储持续时间的对象的初始化完成之前,则在调用传递给std :: atexit的函数之前对对象的析构函数的调用进行排序。如果对std :: atexit的调用在另一次对std :: atexit的调用之前强烈发生,则传递给第二个std :: atexit的函数的调用在传递给第二个std :: atexit的函数之前被排序。第一个std :: atexit调用。

并在 数据竞赛 [intro.races] / 12中定义

评估A发生在评估D之前,如果发生以下情况之一

(12.1)A在D之前排序,或

(12.2)A与D同步,并且A和D都是顺序​​一致的原子操作([atomics.order]),或

(12.3)对B和C进行求值,使得A在B之前排序,B仅发生在C之前,而C在D之前排序,或者

(12.4)有一个评估B,使得A强烈发生在B之前,而B强烈发生在D之前。

[注意:非正式地,如果A强烈地发生在B之前,那么在所有情况下A似乎都在B之前被评估。强烈发生在排除消耗操作之前。—尾注]

为什么引入“强烈发生”?直觉上,它与“之前发生的事情”有什么区别和关系?

注释中的“在所有情况下,似乎A在B之前都被评估”是什么意思?

(注意:此问题的动机是Peter Cordes在此答案下的评论。)

标准报价附加草案(感谢Peter Cordes)

有序性和一致性[atomics.order] / 4

所有memory_order :: seq_cst操作(包括篱笆)上都有一个总顺序S,它满足以下约束。首先,如果A和B是memory_order ::: seq_cst运算,并且A强烈地发生在B之前,那么A在S中先于B.其次,对于对象M上的每对原子操作A和B,A的相干性在B之前,S必须满足以下四个条件:

(4.1)如果A和B都是memory_order :: seq_cst操作,则A在S中先于B; 和

(4.2)如果A是一个memory_order ::: seq_cst操作,而B发生在memory_order :: seq_cst栅栏Y之前,则A在S中位于Y之前; 和

(4.3)如果memory_order :: seq_cst栅栏X发生在A之前,而B是memory_order :: seq_cst操作,则X在S之前位于B之前; 和

(4.4)如果memory_order :: seq_cst栅栏X发生在A之前,而B发生在memory_order :: seq_cst栅栏Y之前,那么X在S中位于Y之前。


1
当前的标准草案seq_cstAtomics 31.4顺序和一致性中还引用了“ A强烈发生在B之前”作为规则的条件 4。这不在C ++ 17 n4659标准中,在该标准中32.4-3定义了seq_cst ops的单个总顺序的存在与所有受影响位置的“先发生”顺序和修改顺序一致;在稍后的草稿中添加了“强烈”一词。
彼得·科德斯

2
@PeterCordes我认为不包括消费在内的评论,说它是“在所有情况下” /“强”的HB,并且谈论对函数指针的调用是一种死气沉沉的赠品。如果一个多线程程序atexit()在一个线程中调用另一个线程exit(),则仅初始化器仅携带基于消费的依赖关系是不够的,因为其结果不同于exit()同一线程调用的结果。我的一个较旧的答案与此差异有关。
Iwillnotexist Idonotexist


@IwillnotexistIdonotexist您甚至可以退出MT程序吗?从根本上来说这不是一个破烂的主意吗?
curiousguy19年

1
@curiousguy就是这个目的exit()任何线程都可以通过退出来杀死整个程序,或者主线程可以通过return-ing 退出。这将导致atexit()处理程序的调用以及所有正在执行的线程的死亡。
Iwillnotexist Idonotexist

Answers:


5

为什么引入“强烈发生”?直觉上,它与“之前发生的事情”有什么区别和关系?

也为“之前发生的事情”做好准备!看看cppref的当前快照 https://en.cppreference.com/w/cpp/atomic/memory_order

在此处输入图片说明

似乎在C ++ 20中添加了“之前发生”。

发生之前

与线程无关,只要满足以下任一条件,评估A就会在评估B之前发生:

1)A在B之前排序

2)A与B同步

3)A发生在X之前,而X发生在B之前

注意:如果没有消耗操作,则前发生和前发生关系是相同的。

因此,Simply-HB和HB相同,除了它们处理消耗操作的方式不同。见HB

发生之前

与线程无关,如果满足以下任一条件,则在评估B之前进行评估A:

1)A在B之前排序

2)线程间发生在B之前

需要通过确保在必要时引入额外的同步来确保事前发生关系是非循环的实现(仅在涉及消耗操作时才有必要,请参见Batty等人)

它们在消费方面有何不同?请参阅Inter-Thread-HB

线程间发生之前

在线程之间,如果满足以下任一条件,则评估A会在评估B之前发生线程间

1)A与B同步

2)A在B之前是依序排列的

3)...

...

按依赖关系排序的操作(即,使用释放/使用)是HB,但不一定是Simply-HB。

消费比获取更轻松,因此,如果我理解正确,HB比Simply-HB更轻松。

强烈发生在之前

与线程无关,如果满足以下任一条件,则评估A会强烈地发生在评估B之前:

1)A在B之前排序

2)A与B同步,并且A和B都是顺序一致的原子操作

3)A在X之前被排序,X在Y之前简单地发生,而Y在B之前被排序

4)A强烈发生在X之前,而X强烈发生在B之前

注意:非正式地,如果A发生在B之前,则在所有情况下A似乎都在B之前被评估。

注意:强烈发生-在排除消耗操作之前。

因此,释放/消费操作不能是Strongly-HB。

释放/获取可以是HB和Simply-HB(因为释放/获取与之同步),但不一定是Strongly-HB。因为Strongly-HB特别指出A必须与B同步,并且是顺序一致的操作。

                            Is happens-before guaranteed?

                        HB             Simply-HB          Strongly-HB

relaxed                 no                 no                 no
release/consume        yes                 no                 no      
release/acquire        yes                yes                 no
S.C.                   yes                yes                yes

注释中的“在所有情况下,似乎A在B之前都被评估”是什么意思?

所有上下文:所有线程/所有CPU看到(或“最终会同意”)相同的顺序。这是顺序一致性的保证-所有变量的全局总修改顺序。获取/发布链仅保证参与链的线程的感知修改顺序。从理论上讲,链外的线程可以看到不同的顺序。

我不知道为什么要引入Strongly-HB和Simply-HB。也许可以帮助阐明如何围绕消耗进行操作?Strongly-HB具有很好的属性-如果一个线程在B之前观察到A发生强烈,则它知道所有线程都将观察到同一件事。

消费历史:

Paul E. McKenney负责使用C和C ++标准。消费保证指针分配与其指向的内存之间的顺序。它是由于DEC Alpha发明的。DEC Alpha可以推测性地取消对指针的引用,因此它还具有一个内存屏障来防止这种情况。DEC Alpha不再生产,并且今天没有处理器具有此行为。打算使消费非常轻松。


1
真是的 我几乎后悔提出这个问题。我想回过头来解决一些简单的 C ++问题,例如迭代器无效的规则,参数相关的名称查找,模板用户定义的转换运算符,模板参数推导,名称查找在模板成员的基类中以及当您可以在对象构建开始时将其转换为虚拟基础。
curiousguy19年

回复:消耗。您是否认为消费订购的命运与DEC Alpha的命运联系在一起,并且在那个特定拱门之外没有任何价值?
curiousguy19年

1
这是个好问题。从现在开始进一步研究,听起来从理论上讲消耗可以使性能较弱的拱门(如ARM和PowerPC)提高性能。给我更多时间研究它。
汉弗莱·温

1
我说消费的存在是因为除了 Alpha 之外,所有其他顺序较弱的ISA都存在。在Alpha asm中,只有选项放宽并获取(和seq-cst),而不是依赖项排序。mo_consume旨在利用实际CPU上的数据依赖关系排序,并正式化表示编译器无法通过分支预测来破坏数据依赖关系。例如int *p = load(); tmp = *p;if(p==known_address) tmp = *known_address; else tmp=*p;如果编译器由于某种原因期望某个指针值是通用的,则可能会被破坏。放松但不消耗是合法的。
彼得·科德斯

@PeterCordes对...顺序较弱的拱门必须发出获取障碍,但(理论上)不应该消耗。听起来您好像认为如果Alpha不存在,我们还会消耗吗?另外,您基本上是在说,消耗是一种幻想(或“标准”)编译器障碍。
汉弗莱·温
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.