理解 Acquire 语义的核心
Acquire 语义的核心是提供一个单向的屏障,确保在它之后的内存访问不会被乱序到它之前。这个“之后”的内存访问,确实包括了读和写操作。
具体来说:
-
它确保了
Acquire
操作之前的所有内存访问(包括读写)与Acquire
操作之后的内存访问之间有一个顺序关系。 这意味着Acquire
之后的操作不会被重排到Acquire
之前。 -
它保证了
Acquire
操作自身以及它之前的、其他处理器可见的写入,对当前处理器在Acquire
之后的所有内存访问都是可见的。
为什么会有“保证后面的读写不被重排到前面”的说法?
这种说法,虽然在技术上需要更精确的措辞,但通常是在强调 Acquire 语义的一个重要结果:确保了在 Acquire 之后,你所看到的数据是最新且一致的,因为任何依赖于该 Acquire 操作的后续内存访问(无论是读新数据还是写新数据),都必须在 Acquire 操作完成并“看到”之前的所有相关更新之后才能执行。
我们可以从以下几个角度来解释这种表述的合理性:
1. 阻止后续依赖性操作的乱序
如果 Acquire 之后的读操作(例如,读取被锁保护的数据)被重排到 Acquire 之前,那么这个读操作可能读取到旧的、不一致的数据。Acquire 语义的存在,就是为了阻止这种情况。
同样,如果 Acquire 之后的写操作(例如,写入被锁保护的数据)被重排到 Acquire 之前,那么这个写操作可能在获取到最新状态之前就修改了数据,或者其写入可能在 Acquire 所建立的同步点之前就被其他核观察到,从而破坏了并发模型的正确性。Acquire 语义阻止了这种写操作“过早”地发生或可见。
2. 宏观上的同步效果
在同步原语(如锁)的语境中,Acquire 操作(例如,LDAR
读取锁状态)的目的是为了获取共享资源的最新状态。一旦获取成功,线程就会继续操作这些资源。
-
读取资源: 如果后续的读操作被重排到 Acquire 之前,就可能读到旧数据。
-
写入资源: 如果后续的写操作被重排到 Acquire 之前,就可能在未完全获取资源所有权或最新视图的情况下就修改了数据。
Acquire 语义通过其单向屏障,确保了所有在它之后的内存访问(读或写)都发生在一个“已获取”的上下文里,即它们都能基于 Acquire 操作所建立的最新内存视图。
3. 与 Release 语义的对称性
在顺序一致性内存模型中,Acquire 和 Release 语义是关键的构建块。
-
Release (释放) 语义:保证其之前的所有内存操作(读和写)都对其他处理器可见,并且不会被重排到 Release 之后。
-
Acquire (获取) 语义:保证**其之后的所有内存操作(读和写)**都将看到 Release 操作所同步的数据,并且不会被重排到 Acquire 之前。
从这个角度看,Acquire 确保了屏障“之后”的读写操作,不会“跳到”屏障“之前”。
精确的理解
尽管存在那种表述,但最精确的理解是:
-
Acquire
语义确保所有在Acquire
之后的内存访问(读和写)都能看到Acquire
操作本身,以及所有在Acquire
之前、且在内存中对当前处理器可见的写入。 -
它阻止了
Acquire
之后的内存访问(读或写)被重排到Acquire
之前。
它不阻止 Acquire
之前的内存访问(包括写操作)与 Acquire
本身发生重排,或者与 Acquire
之后的不依赖于 Acquire
屏障的内存访问发生重排。
因此,当您看到“保证了后面的读也包括写指令不被重排到前面”时,其重点是强调 Acquire 屏障能够防止后续的、依赖于屏障所建立的内存状态的操作被过早地执行,从而确保了程序的正确性和数据的一致性。
Release(释放)内存语义的精确解释
Release 语义的核心作用是:确保在 Release 操作之前发生的所有内存访问(包括读和写)都对其他处理器可见,并且这些操作不会被重排到 Release 操作之后。
可以把它想象成一个**“向前看”的屏障**,或者一个**“信号发送器”**:
-
强制之前的写入对其他处理器可见: 当一个处理器执行一个带有 Release 语义的存储操作(例如,写入一个锁变量),它会确保在此 Release 操作之前,该处理器所执行的所有写操作(比如对受锁保护的数据的修改)都已经完成并被推送到缓存一致性系统,从而对其他处理器是可见的。
-
这意味着,其他处理器在随后通过 Acquire 操作读取到这个 Release 写入的值时,它们就能保证看到 Release 之前的所有写入。
-
-
阻止指令重排: Release 语义会阻止所有在 Release 操作之前的内存访问(包括读和写)被编译器或处理器重排到 Release 操作之后。
-
换句话说,Release 操作就像一个“截止点”,它之前的所有相关操作都必须在它发生之前完成,并且它们的顺序不能被颠倒到 Release 之后。
-
Release 语义的 ARM64 实现
在 ARM64 架构中,实现 Release 语义的常见指令包括:
-
STLR
(Store-Release Register):这是标准的、非原子但具有 Release 语义的存储指令。 -
STLXR
(Store-Release Exclusive Register):与STXR
类似,用于独占存储,但带有 Release 语义。 -
CASAL
(Compare And Swap, Acquire/Release):这个原子指令既包含 Acquire 语义也包含 Release 语义,因为它是一个读-改-写操作,既需要看到最新状态(Acquire),也需要将自己的修改对外可见(Release)。 -
DMB
(Data Memory Barrier) 或DSB
(Data Synchronization Barrier):这些通用的内存屏障指令,如果使用SY
或ISH
选项,也可以提供比 Release 语义更强的全屏障效果,自然也包含了 Release 的保证。
为什么 Release 语义如此重要?
Release 语义对于传递状态或数据至关重要。考虑一个线程更新了某些共享数据,然后释放了一个锁或设置了一个标志位来通知其他线程这些数据已准备好:
-
如果没有 Release 语义,数据写入操作可能会被重排到释放锁操作之后,导致其他线程在读取到“锁已释放”的状态时,却看到了旧的数据,从而引发数据不一致或程序错误。
-
有了 Release 语义,写入线程就能保证,当它释放锁时,所有它对数据的修改都已经对其他线程可见。
Release 语义与 Acquire 语义的配对
Release 和 Acquire 语义通常协同工作,形成一个“同步对”:
-
释放方 (Release):将自己的修改推到内存中,并确保之前的操作对其他处理器可见。
-
获取方 (Acquire):从内存中拉取数据,并确保之后的内存访问能看到 Release 方推送到内存的所有修改。
通过这种配对,它们共同保证了在并发编程中,不同处理器核心对共享内存的一致性视图和操作顺序。