synchronizes-with关系
synchronizes-with
关系是你只能在原子类型上的操作之间得到的东西。如果一个数据结构包含原子类型,并且在该数据结构上的操作会内部执行适当的原子操作,该数据结构上的操作(如锁定互斥元)可能会提供这种关系,但是从根本上说synchronizes-with
关系只出自原子类型上的操作。
基本思想: 在一个变量x
上的一个被适当标记的原子写操作w
, 与在x
上的一个被适当标记的,通过写入(W)
,或是由与执行最初的写操作W
相同的线程在x
上的后续原子写操作,或是由任意线程在x
上一系列的原子的读-修改-写操作(fetch_add()
或compare_exchange_weak()
)来读取所存储的值的原子读操作同步,其中随后通过第一个线程读取的值是通过W
写入的值。
换个说法:如果线程A存储一个值而线程B读取该值,那么线程A中存储和线程B中的载入之间存在一种synchronizes-with
关系。
happens-before 关系
happens-before
(发生于之前)关系是程序中操作顺序的基本构件,它指定了哪些操作看到其他操作的结果。对于单个线程,它是直观的,如果一个操作排在另一个操作之前,那么该操作就发生于另一个操作之前。这就意味着,如果一个操作(A)发生于另一个操作(B)之前的语句里,那么A就发生于B之前。
有时候,单条语句中的操作是有顺序的,例如使用内置的逗号操作符或者使用一个表达式的结果作为另一个表达式的参数。
但一般来说,单条语句中的操作是非顺序的,而且也没有sequenced-before
(因此也没有happens-before
).当然,一条语句的所有操作在下一句的所有操作之前发生。
对于多线程中,如果线程间的一个线程上的操作A发生于另一个线程上的操作B之前,那么A发生于B之前。
原子操作的内存顺序
有六种内存顺序选项可以应用到原子类型上的操作:
- memory_order_relaxed
- memory_order_consume
- memory_order_acquire
- memory_order_release
- memory_order_acq_rel
- memory_order_seq_cst
除非你为某个特定的操作做出指定,原子类型上的所有操作的内存顺序选项都是memory_order_seq_cst
, 这是最严格的可用选项。
尽管有六种选项,它们其实代表了三种模型:
- 顺序一致(sequentially consistent)顺序(
memory_order_seq_cst
) - 获得-释放(acquire_release)顺序(
memory_order_consume
、memory_order_acquire
、memory_order_release
和memory_order_acq_rel
) - 松散(relaxed)顺序(
memory_order_relaxed
)
这些不同的内存顺序模型在不同的CPU架构上可能有着不同的成本。
例如:在基于具有通过处理器而非做更改者对操作的可见性进行良好的控制架构上的系统中,
顺序一致的顺序相对于获取-释放顺序或松散顺序,可能会要求额外的同步指令。
获取-释放相对于松散顺序,可能会要求额外的同步指令。
如果这些系统拥有很多处理器,这些额外的同步指令可能占据显著的时间量,从而降低该系统的整体性能。
另一方面,为了确保原子性,对于超出需要的获得=释放排序,使用x86或x86-64架构的CPU不会要求额外的指令,甚至对于载入操作,顺序一致顺序不需要任何特殊的处理,尽管在存储时会有一点额外的成本。
不同的内存顺序模型的可用性,允许高手们利用更细粒度的顺序关系来提升性能,在不太关键的情况下,当允许使用默认的顺序一致顺序时,它们是有优势的。
1. 顺序一致顺序
默认的顺序被命名为顺序一致(sequentially consistent
), 因为这意味着程序的行为与一个简单的顺序的世界观时一致的。
如果所有原子类型实例上的操作时顺序一致的,多线程程序的行为,就好像是所有这些操作由单个线程以某种特定的顺序执行一致的,多线程程序的行为,就好像是所有这些操作由单个线程以某种特定的顺序进行执行一样。
这意味着如果你的代码在一个线程中有一个操作在另一个之前,其顺序必须对所有其他的线程可见。
从同步的观点来看,顺序一致的存储与读取该存储值的同一个变量的顺序一致载入是同步的。这提供了一种两个(或多个)线程操作的顺序约束,但顺序一致比它更加强大。
在使用顺序一致原子操作的系统中,所有在载入后完成的顺序一致原子操作,也必须出现在其他线程的存储之后。该约束并不会推荐使用具有松散内存顺序的原子操作,它们仍然可以看到操作处于不同的顺序,所以你必须在所有的线程上使用顺序一致的操作。
但易于理解就产生了代价,在一个带有许多处理器的弱顺序机器上,他可能导致显著的性能惩罚,因为操作的整体顺序必须与处理器之间保持一致,可能需要处理器之间密集且昂贵的同步操作。
1 |
|
顺序一致时最直观和直觉的排序,但也是最昂贵的内存顺序,因为它要求所有线程之间的全局同步。在多处理器系统中,这可能需要处理器之间相当密集和耗时的通信。
2. 非顺序一致的内存顺序
时间不再有单一的全局顺序,这意味着不同的线程可能看到相同的操作的不同方面。你不仅得考虑事情真正的并行发生,而且线程不必和事件的顺序一致。
即使线程正在运行完全相同的代码,由于其他线程中的操作没有明确的顺序约束,它们可能与时间的顺序不一致,因为不同的CPU缓存和内部缓冲区可能为相同的内存保存了不同的值。
在没有其他的顺序约束时,唯一的要求是所有的线程对每个独立变量的修改顺序达成一致。
3. 松散顺序
以松散顺序执行的原子类型上的操作不参与synchronzes-with
关系。单线程中的同一个变量的操作仍然服从happens-before
关系,但对于其他线程的顺序几乎没有任何要求。唯一的要求是,从同一个线程对单个原子变量的访问不能被重排,一旦给定的线程看到了原子变量的特定值,该线程之后的读取就不能获取该变量更早的值。
在没有任何线程同步的情况下,每个变量的修改顺序时使用memory_order_relaxed
的线程之间唯一共享的东西。
1 |
|
这里的assert
可以触发,因为x的载入能够读到false,即使y的载入读到了true,并且x的存储发生于y存储之前。x和y是不同的变量,所以关于每个操作所产生的值的可见性没有顺序保证。
不同变量的松散操作可以被自由地重排前提是它们服从所有约束下的happens-before
关系(例如在同一个线程中)。
它们并不引入synchronizes-with
关系。即便在存储操作中存在happens-before
关系,但任一存储和任一载入之间却不存在,所以载入可以在顺序之外看到存储。
4. 获取-释放顺序
获取-释放顺序是松散顺序的进步,操作仍然没有总的顺序,但的确引入了一些同步。在这用顺序模型下,原子载入时获取操作(memory_order_acquire
),原子存储时释放操作(memory_order_release
), 原子的读-修改-写操作是获取、释放或两者兼备(memory_order_acq_rel
)。同步在进行释放的线程和进行获取的线程之间是对偶的。释放操作与读取写入值的获取操作同步。这意味着,不同的线程仍然可以看到不同的排序,但这些顺序是受到限制的
5. 使用获取-释放顺序和MEMORY_ORDER_CONSUME的数据依赖
有两个处理数据历来的新的关系:依赖顺序在其之前(dependency-ordered-before)和带有对其的以来(carries-a-dependency-to)。
与sequenced-before
相似,carries-a-dependency-to
严格适用于的单个线程之内,是操作间数据以来的基本模型。如果操作A的结果被用于操作B的操作数,那么A带有对B的依赖。如果操作A的结果是类似int的标量类型的值,那么如果A的结果存储一个变量中,并且该变量随后被用作操作B的操作数,此关系也是适用的。这种操作具有传递性,所以如果A带有对B的以来且B带有对C的依赖,那么A带有对C的依赖。
另一方面,depency-order-before
的关系可以适用于线程之间。它是通过使用标记了memory_order_consume
的原子载入操作引入的。
这是memory_order_acquire
的一种特例,它限制了对直接依赖的数据同步。标记为memroy_order_release
、memory_order_acq_rel
或memory_order_seq_cst
的存储操作A的依赖顺序在标记为memory_order_acquire
,那么这与synchronizes-with
关系所得到的是相反的。如果操作B带有对操作C的某种依赖,那么A也是依赖顺序在C之前。
如果这对线程间happens-before
关系没有影响,那么在同步目的上就无法为你带来任何好处,但它的却实现了:如果A依赖顺序在B之前,则A也是线程间发生于B之前。
这种内存顺序的一个重要用途,是在原子操作载入指向某数据的指针的场合。通过在载入上使用memory_order_consume
以及在之前的存储上使用memory_order_release
, 你可以确保所指向的数据得到正确的同步,无需在其他非依赖的数据上强加任何同步需求。
1 |
|