在计算机科学中,一致性模型指定了程序员和系统之间的契约,其中系统保证如果程序员按照规则对内存进行操作,内存将是一致的,读取、写入或更新内存的结果将是一致的。s可预测。一致性模型用于分布式系统,如分布式共享内存系统或分布式数据存储(如文件系统、数据库、乐观复制系统或网络缓存)。一致性不同于一致性,一致性发生在缓存的系统中或无缓存,并且是关于所有处理器的数据一致性。Coherence 处理维护全局顺序,在该顺序中,所有处理器都可以看到对单个位置或单个变量的写入。一致性处理与所有处理器相关的多个位置的操作顺序。
高级语言(例如C++和Java)通过以保留内存语义的方式将内存操作转换为低级操作、重新排序某些内存指令以及封装所需的与库调用(例如pthread_mutex_lock(). [1]
stronge consistency model
Strict consistency
严格一致性是最强的一致性模型。在这种模型下,任何处理器对变量的写入都需要被所有处理器立即看到。
严格模型图和非严格模型图描述的是时间约束——瞬时。可以更好地理解,好像存在一个全局时钟,其中每次写入都应在该时钟周期结束时反映在所有处理器缓存中。下一个操作必须仅在下一个时钟周期内发生。
| Sequence | Strict model | Non-strict model | ||
| P1 | P2 | P1 | P2 | |
| 1 | W(x)1 | W(x)1 | ||
| 2 | R(x)1 | R(x)0 | ||
| 3 | R(x)1 |
这是最严格的模型。在这个模型中,每次都会收到程序员的预期结果。它是确定性的。它的实际相关性仅限于思想实验和形式主义,因为即时消息交换是不可能的。它无助于回答对同一数据项的并发写入中的冲突解决问题,因为它假定并发写入是不可能的。
Sequential consistency
顺序一致性模型是由Lamport(1979)提出的。它是一种比严格一致性模型更弱的内存模型。[3]不必立即看到对变量的写入,但是,不同处理器对变量的写入必须以相同的顺序被所有处理器看到。如果“任何执行的结果都相同,就好像数据存储上所有进程的(读和写)操作是按某种顺序执行的,并且每个单独处理器的操作出现在这个序列中,则满足顺序一致性它的程序指定的顺序。” [3] [4] Adve 和 Gharachorloo,1996 [5]定义实现顺序一致性的两个要求;程序顺序和写原子性。
- 程序顺序:程序顺序保证每个进程按照其程序顺序发出内存请求。
- 写原子性:写原子性定义内存请求是根据单个 FIFO 队列的顺序提供服务的。
在顺序一致性中,没有时间或最近写入操作的概念。有一些操作交错对所有进程都是相同的。一个进程可以看到所有进程的写操作,但它只能看到自己的读操作。应保持每个处理器内的程序顺序和处理器之间的操作顺序。为了保持处理器之间的执行顺序,所有操作都必须看起来是即时或原子地相对于其他每个处理器执行的。
这些操作只需要“出现”就可以完成,因为在物理上不可能瞬间发送信息。例如,在一个使用单一全局共享总线的系统中,一旦一条总线上发布了信息,就可以保证所有处理器都能同时看到该信息。因此,将信息传递给总线就完成了对所有处理器的执行,并且看起来已经执行了。无缓存架构或具有非瞬时互连网络的缓存架构可能包含处理器和内存之间的慢速路径。这些慢速路径会导致顺序不一致,因为一些存储器比其他存储器更快地接收广播数据。
顺序一致性会产生不确定的结果。这是因为在程序的不同运行期间,处理器之间的顺序操作顺序可能不同。所有内存操作都需要按照程序顺序进行。
线性化[6] (也称为原子一致性或原子内存)[7]可以定义为具有实时约束的顺序一致性,通过考虑每个操作的开始时间和结束时间。如果每个操作通过在其开始时间和结束时间之间放置一个点以可线性化的顺序发生并保证顺序一致性,则执行是可线性化的。
通过模型检查来验证顺序一致性通常是不可判定的,即使对于有限状态缓存一致性协议也是如此。[8]
Causal consistency
因果一致性[4]由 Hutto 和 Ahamad 在 1990 年定义,[9]是对顺序一致性模型的弱化,它通过将事件分类为因果相关和不相关的事件。它定义只有因果关系的写操作需要被所有进程以相同的顺序看到。例如,如果事件 b 从较早的事件 a 生效,则因果一致性保证所有进程在事件 a 之后看到事件 b。Tanenbaum et al., 2007 提供了一个更严格的定义,即数据存储在以下条件下被认为是因果一致的:[4]
- 具有潜在因果关系的写入必须以相同的顺序被所有进程看到。
- 在不同的机器上可能会以不同的顺序看到并发写入。
该模型放宽了处理器并发写入和非因果相关写入的顺序一致性。如果对变量的一次写入依赖于对任何变量的先前写入,并且执行第二次写入的处理器刚刚读取了第一次写入,则两次写入可能会变得因果相关。两次写入可能由同一处理器或不同处理器完成。
与顺序一致性一样,读取不需要立即反映更改,但是,它们需要按顺序反映对变量的所有更改。
| Sequence | P1 | P2 | P3 | P4 |
| 1 | W(x)1 | R(x)1 | R(x)1 | R(x)1 |
| 2 | W(x)2 | |||
| 3 | W(x)3 | R(x)3 | R(x)2 | |
| 4 | R(x)2 | R(x)3 |
W(x)2 发生在 W(x)1 之后,因为 P2 在 W(x)2 之前对 x 进行了读取,因此这个例子在 Hutto 和 Ahamad 的定义下是因果一致的(尽管不是在 Tanenbaum 等人的定义下,因为 W(x)2 和 W(x)3 对于所有进程来说并不是以相同的顺序出现的)。然而 R(x)2 和 R(x)3 在 P3 和 P4 上以不同的顺序出现,因此这个例子是顺序不一致的。[10]
Processor consistency
为了保持数据的一致性并获得每个处理器都有自己的内存的可扩展处理器系统,推导出了处理器一致性模型。[10] 所有处理器都需要在它们看到一个处理器完成写入的顺序以及它们看到不同处理器对同一位置的写入的方式上保持一致(保持一致性)。但是,当不同处理器写入不同位置时,它们不需要保持一致。
每一次写操作都可以分成若干次对所有内存的子写。在对该内存的写入完成之前,可能会从一个这样的内存中读取数据。因此,读取的数据可能是陈旧的。因此,当需要停止较旧的存储时,PC 下的处理器可以执行较新的负载。写前读、读后读和写前写顺序仍然保留在此模型中。
处理器一致性模型[11]类似于PRAM 一致性模型,具有更强的条件,即所有其他进程必须以相同的顺序看到对同一内存位置的所有写入。处理器一致性弱于顺序一致性但强于 PRAM 一致性模型。
斯坦福 DASH 多处理器系统实现了处理器一致性的变体,这与 Goodman 的定义无可比拟(既不弱也不强)。[12]所有处理器都需要在它们看到一个处理器的写入顺序以及它们看到不同处理器对同一位置的写入的方式上保持一致。但是,当不同处理器写入不同位置时,它们不需要保持一致。
Pipelined RAM consistency, or FIFO consistency
流水线 RAM 一致性(PRAM 一致性)由 Lipton 和 Sandberg 在 1988 年[13] 提出,是最早描述的一致性模型之一。由于其非正式定义,实际上至少有两种略有不同的实现方式,[12]一种由 Ahamad 等人提出。还有一个是 Mosberger 的。
在 PRAM 一致性中,所有进程都按照单个进程发出的相同顺序查看单个进程的操作,而不同进程发出的操作可以从不同的进程以不同的顺序查看。PRAM 一致性弱于处理器一致性。PRAM 放宽了保持其所有处理器位置一致性的需要。在这里,可以在处理器中写入之前执行对任何变量的读取。写前读、读后读和写前写顺序仍然保留在此模型中。
| Sequence | P1 | P2 | P3 | P4 |
| 1 | W(x)1 | |||
| 2 | R(x)1 | |||
| 3 | W(x)2 | |||
| 4 | R(x)1 | R(x)2 | ||
| 5 | R(x)2 | R(x)1 |
Cache consistency
高速缓存一致性[11] [14]要求对同一内存位置的所有写操作都按某种顺序执行。Cache一致性弱于处理器一致性,无法与PRAM一致性相比。
Slow consistency
在慢一致性中,[14]如果进程读取先前写入内存位置的值,则它随后无法从该位置读取任何更早的值。进程执行的写入对该进程立即可见。慢速一致性是比 PRAM 和缓存一致性更弱的模型。
示例: 慢速内存图描绘了一个慢速一致性示例。第一个进程将 1 写入内存位置 X,然后将 1 写入内存位置 Y。第二个进程从 Y 读取 1,然后从 X 读取 0,即使 X 在 Y 之前写入。
Hutto、Phillip W. 和 Mustaque Ahamad (1990) [9]说明,通过适当的编程,慢速记忆(一致性)可以具有表现力和效率。他们提到慢速记忆有两个有价值的特性;局部性并支持从原子内存中减少。他们提出了两种算法来呈现慢速记忆的表现力。
Session gurantees
这4个一致性模型是在1994年的一篇论文中提出的。他们专注于在只有单个用户或应用程序进行数据修改的情况下的保证。[15]
Monotonic read consistency
Tanenbaum et al., 2007 [4]对单调读一致性的定义如下:
“如果一个进程读取数据项 x 的值,则该进程对 x 的任何连续读取操作将始终返回相同的值或更新的值。” [4]
单调读取一致性保证进程在时间 t 读取数据项 x 的值后,它永远不会看到该数据项的旧值。
Monotonic write consistency
单调写入一致性条件由 Tanenbaum et al., 2007 [4]定义如下:
“进程对数据项 X 的写操作在同一进程对 X 的任何后续写操作之前完成。” [4]
Read-your-writes consistency
进程对数据项 X 写入的值将始终可用于同一进程对数据项 X 执行的连续读取操作。[4]
Writes-follows-reads consistency
在写后读一致性中,更新在执行之前的读操作之后传播。Tanenbaum et al., 2007 [4]定义了写入-跟随-读取一致性的以下条件:
“在同一进程对数据项 x 进行先前读取操作之后,进程对数据项 x 的写入操作保证发生在读取的相同或更新的 x 值上。” [4]
Weak memory consistency models
以下模型需要程序员进行特定的同步。
Weak ordering
弱排序将内存操作分为两类:数据操作和同步操作。为了强制执行程序顺序,程序员需要在程序中找到至少一个同步操作。同步操作向处理器发出信号,以确保它已完成并看到所有处理器完成的所有先前操作。仅在同步操作中维护程序顺序和原子性,而不是在所有读取和写入操作中维护程序顺序和原子性。这是源于这样的理解:某些内存操作(例如在关键部分中进行的操作)在关键部分中的所有操作完成之前不需要被所有处理器看到。它假设在同步操作之间对数据区域的内存操作重新排序不会影响程序的结果。这利用了这样一个事实:为在多处理器系统上执行而编写的程序包含所需的同步,以确保不会发生数据争用并且始终会产生 SC 结果。
| P1 | P2 |
X = 1; fence | fence fence |
在这个模型中,连贯性并没有放松。一旦满足这些要求,所有其他“数据”操作都可以重新排序。其工作方式是计数器跟踪数据操作的数量,直到该计数器变为零为止,不会发出同步操作。此外,除非之前的所有同步都完成,否则不会发出更多数据操作。两个同步变量之间的内存操作可以重叠和重新排序,而不影响程序的正确性。该模型确保始终保持写入原子性,因此弱排序不需要额外的安全网。
为了维持弱排序,同步操作之前的写操作必须在同步操作之前全局执行。同步操作之后出现的操作也应该仅在同步操作完成后执行。因此,对同步变量的访问是顺序一致的,并且只有在先前的同步操作完成之后才应执行任何读取或写入。
程序中对显式同步的依赖程度很高。对于弱排序模型,程序员必须使用原子锁定指令,例如测试和设置、获取和操作、条件存储、加载链接或必须标记同步变量或使用栅栏。
Release consistency(RC)
发布一致性模型通过区分入口同步操作和出口同步操作,放宽了弱一致性模型。在弱排序下,当要看到同步操作时,在同步操作完成并且处理器继续之前,所有处理器中的所有操作都需要可见。然而,在释放一致性模型下,在进入称为“获取”的临界区期间,需要完成与本地内存变量有关的所有操作。在退出期间(称为“发布”),本地处理器所做的所有更改都应传播到所有其他处理器。
- 获取(Acquire)操作:在进入临界区时执行,确保当前处理器可以看到其他处理器对共享变量的更改。获取操作通常是加载/读取操作。
- 释放(Release)操作:在退出临界区时执行,确保当前处理器对共享变量的更改会传播给其他处理器。释放操作通常是存储/写入操作。
- 同步操作 (Synchronization Operations):除了获取和释放之外,其他一些特定的同步指令或操作也可能被视为特殊操作。例如,某些架构可能有专门的锁、解锁指令、内存屏障(Memory Barrier)、栅栏指令(Fence Instruction)等。这些操作可以强制处理器按一定顺序执行或观察内存操作,确保一致性。
发布一致性模型允许在获取或释放操作之间的普通内存操作不必完全遵循顺序一致性,从而提升性能。只有获取和释放操作才强制确保临界区的同步。
RCsc 和 RCpc
发布一致性有两种不同的类型:
- RCsc(Release Consistency with Sequential Consistency):基于顺序一致性。
- RCpc(Release Consistency with Processor Consistency):基于处理器一致性。
顺序一致性 (Sequential Consistency, SC)
顺序一致性是一种严格的模型,要求所有处理器的所有操作在全局上都遵循统一的顺序。在 RCsc 中,发布一致性模型的同步操作(获取和释放)会严格遵守顺序一致性。
处理器一致性 (Processor Consistency, PC)
处理器一致性是一种更宽松的模型。它允许在一个处理器中,写操作与读操作之间的顺序可以打乱,只要在同一对竞争同步变量上最终维持正确的顺序即可。在 RCpc 中,同步操作(获取和释放)遵循处理器一致性模型,从而进一步放宽了一些顺序约束,提高并发性。
对于顺序一致性 (RCsc),约束是:
- 获取→全部,获取操作必须在它后面的所有内存操作之前完成。这确保获取操作之后的操作都看到最新的共享变量。
- 全部→释放,释放操作必须在它前面的所有内存操作之后完成。这确保在释放操作前的所有更改都已经提交,以便其他处理器可以看到这些更改。
- 特殊→特殊,对特殊同步操作的顺序必须严格遵守程序顺序。例如,如果一个获取操作在一个释放操作之前,那么它在实际执行时也必须在释放操作之前。
为了处理器一致性 (RCpc),写入读取程序顺序被放宽,具有以下约束:
- 获取→全部,获取操作在程序顺序上位于其他操作之前的约束和 RCsc 相同,确保所有操作在获取操作后看到一致的状态。
- 全部→释放,释放操作在程序顺序上位于其他操作之后的约束和 RCsc 相同,确保所有操作的更改都在释放后可见。
- 特殊→特殊(特殊写入后跟随特殊读取的情况除外),特殊操作的顺序也需满足程序顺序,但允许某些写操作在读操作之前。具体来说,对于一些竞争同步变量,写可以在读之前发生。RCpc 因此比 RCsc 更宽松。
注意:上面的符号 A→B 意味着如果操作 A 在程序顺序中先于 B,则强制执行程序顺序。
Entry consistency
这是发布一致性模型的一个变体。它还需要使用获取和释放指令来明确声明进入或退出关键部分。然而,在条目一致性下,每个共享变量都被分配了一个特定于它的同步变量。这样,只有当获取的是变量x时,所有与x相关的操作都需要相对于该处理器完成。这允许不同共享变量的不同关键部分发生并发操作。对于同一共享变量上的关键操作看不到并发性。当可以同时处理不同的矩阵元素时,这种一致性模型将非常有用。
Local consistency
在本地一致性中,每个进程按照其程序定义的顺序执行自己的操作。对于其他进程的写操作的执行顺序没有限制。局部一致性是共享内存系统中最弱的一致性模型。
General consistency
在一般一致性中,在所有进程的写入完成后,内存位置的所有副本最终都是相同的。
Eventual consistency
最终一致性是系统中缺乏同步更新的弱一致性模型。它定义如果很长时间没有更新,所有副本最终都会变得一致。
大多数共享的去中心化数据库都有最终一致性模型,要么 BASE:基本可用;软状态;最终一致,或ACID和 BASE 的组合有时称为 SALT:顺序;同意;账本;防篡改,而且对称;免管理;账本;并且时间一致。
Relaxed memory consistency models
一些不同的一致性模型可以通过放宽顺序一致性中的一个或多个要求来定义,称为relaxed一致性模型。这些一致性模型不提供硬件级别的内存一致性。事实上,程序员负责通过应用同步技术来实现内存一致性。上述模型根据四个标准进行分类并进一步详细说明。
有四种比较来定义宽松一致性(relaxed consistency):
松弛
对放宽一致性进行分类的一种方法是定义放宽了哪些顺序一致性要求。我们可以通过放宽 Adve 和 Gharachorloo,1996 定义的程序顺序或写入原子性要求来获得不太严格的模型。程序顺序保证每个进程发出按其程序排序的内存请求,而写入原子性定义根据顺序为内存请求提供服务单个 FIFO 队列。在放宽程序顺序中,可以放宽操作对的任何或所有顺序、先写后写、先读后写或先读/先写后读。在宽松的写入原子性模型中,进程可以在任何其他处理器之前查看自己的写入。
同步 vs 非同步
同步模型可以通过将内存访问分为两组并为每组分配不同的一致性限制来定义,考虑到一组可以具有弱一致性模型,而另一组需要更严格的一致性模型。相反,非同步模型将相同的一致性模型分配给内存访问类型。
问题 vs 基于视图
方法通过定义进程发出内存操作的限制来提供顺序一致性模拟。而视图方法描述了流程事件顺序的可见性限制。
相对模型强度
一些一致性模型比其他模型更具限制性。换句话说,严格的一致性模型强制执行更多的约束作为一致性要求。模型的强度可以通过程序顺序或原子性松弛来定义,并且模型的强度也可以进行比较。如果某些模型应用相同或更多的松弛,则它们是直接相关的。另一方面,放宽不同要求的模型并不直接相关。
顺序一致性有两个要求,程序顺序和写入原子性。
通过放宽这些要求可以获得不同的放宽一致性模型。这样做是为了在放松约束的同时提高性能,但程序员负责通过应用同步技术来实现内存一致性,并且必须对硬件有很好的了解。
潜在的放松:
- 写入读取程序顺序
- 写入写入程序顺序
- 读到读和读到写程序命令
Relaxed write to read
提高硬件级别性能的一种方法是放宽先写后读的 PO,从而有效隐藏写操作的延迟。这种类型的放松所依赖的优化是,它允许后续读取相对于处理器的先前写入处于宽松的顺序。由于这种放宽,像 XXX 这样的一些程序可能会因为这种放宽而无法给出 SC 结果。然而,由于其余程序顺序限制的执行,像 YYY 这样的程序仍有望给出一致的结果。
三种型号属于此类别。 IBM 370 模型是最严格的模型。读取可以在较早写入不同地址之前完成,但禁止返回写入的值,除非所有处理器都已看到该写入。 SPARC V8 全存储排序模型 (TSO) 模型部分放宽了 IBM 370 模型,它允许读取返回其自己的处理器写入的值(相对于同一位置的其他写入),即它返回之前其自己的写入的值其他人看到了。与之前的模型类似,除非所有处理器都看到了写入,否则无法返回写入的值。处理器一致性模型 (PC) 是三个模型中最宽松的模型,它放宽了这两个约束,使得读取可以在较早的写入之前完成,甚至可以在其他处理器可见之前完成。
在示例 A 中,结果仅在 IBM 370 中可能,因为在该处理器中的 write(A) 完成之前不会发出 read(A)。另一方面,这种结果在 TSO 和 PC 中是可能的,因为它们允许在单个处理器中写入标志之前读取标志。
在示例 B 中,结果只能通过 PC 实现,因为它允许 P2 在写入的值对 P3 可见之前就返回该值。这在其他两种型号中是不可能的。
为了确保上述模型中的顺序一致性,使用安全网或栅栏来手动强制执行约束。 IBM370 模型有一些专门的序列化指令,可以在操作之间手动放置这些指令。这些指令可以由存储器指令或非存储器指令(例如分支)组成。另一方面,TSO 和 PC 模型不提供安全网,但程序员仍然可以使用读取-修改-写入操作,使其看起来像是在写入和后续读取之间仍保持程序顺序。在 TSO 的情况下,如果已经是 R-modify-W 一部分的 R 或 W 被 R-modify-W 替换,则 PO 似乎被保留,这要求 R-modify-W 中的 W 是返回读取值的“虚拟”。类似地,对于 PC,如果读取被写入替换或者已经是 R-modify-W 的一部分,则 PO 似乎会得到维护。
然而,单独进行这种放松后无法完成编译器优化。编译器优化需要对 PO 中的任意两个操作进行重新排序的完全灵活性,因此在这种情况下,相对于读取重新排序写入的能力并没有足够的帮助。
Example A
| P1 | P2 |
| A = flag1 = flag2 = 0 | |
| flag1 = 1 | flag2 = 1 |
| A = 1 | A = 2 |
| reg1 = A | reg3 = A |
| reg2 = flag2 | reg4 = flag1 |
| reg1 = 1; reg3 = 2, reg2 = reg4 = 0 |
Example B
| P1 | P2 | P3 |
| A = B = 0 | ||
| A = 1 | ||
| if (A == 1) | ||
| B = 1 | if (B == 1) | |
| reg1 = A | ||
| B = 1, reg1 = 0 |
Relaxed write to read and write to write
某些模型甚至通过放宽不同位置写入之间的顺序约束来进一步放宽程序顺序。 SPARC V8 部分存储排序模型 (PSO) 是此类模型的唯一示例。从同一处理器对不同位置进行管道和重叠写入的能力是 PSO 实现的关键硬件优化。 PSO 在原子性要求方面与 TSO 类似,因为它允许处理器读取自己写入的值,并防止其他处理器在写入对所有其他处理器可见之前读取另一个处理器的写入。两次写入之间的程序顺序由 PSO 使用显式 STBAR 指令来维护。在具有 FIFO 写入缓冲区的实现中,STBAR 被插入到写入缓冲区中。计数器用于确定 STBAR 指令之前的所有写入何时完成,这会触发对内存系统的写入以递增计数器。写入确认会使计数器递减,当计数器变为 0 时,表示之前的所有写入都已完成。
在示例 A 和 B 中,PSO 允许这两种非顺序一致的结果。 PSO 提供的安全网与 TSO 类似,它强制执行从写入到读取的程序顺序,并强制执行写入原子性。
与之前的模型类似,PSO 允许的松弛不够灵活,无法用于编译器优化,这需要更灵活的优化。
Relaxing read and read to write program orders: Alpha, RMO, and PowerPC
在某些模型中,对不同位置的所有操作都放宽。可以相对于不同位置中的不同读取或写入来重新排序读取或写入。弱排序可以归为此类,并且两种类型的发布一致性模型(RCsc 和 RCpc)也属于此模型。在此类放松下还提出了三种商业架构:Digital Alpha、SPARC V9 放松内存顺序 (RMO) 和 IBM PowerPC 模型。
这三个商业架构展示了明确的围栏指令作为其安全网。 Alpha模型提供两种类型的栅栏指令,内存屏障(MB)和写内存屏障(WMB)。 MB 操作可用于维护 MB 之前的任何内存操作以及屏障之后的内存操作的程序顺序。类似地,WMB 仅在写入期间维护程序顺序。 SPARC V9 RMO 模型提供了 MEMBAR 指令,可对其进行定制,以针对未来的读写操作对先前的读写进行排序。不需要使用读取-修改-写入来实现此顺序,因为 MEMBAR 指令可用于相对于后续读取来排序写入。 PowerPC 模型使用称为 SYNC 指令的单个栅栏指令。它与 MB 指令类似,但有一点例外,即使在对同一位置的两次读取之间放置了 SYNC,读取也可能会不按程序顺序进行。该模型在原子性方面也与 Alpha 和 RMO 不同。它允许在读取完成之前看到写入。可能需要读取修改写入操作的组合来产生写入原子性的假象。
RMO 和 PowerPC 允许对同一位置的读取重新排序。这些模型违反了示例 A 和 B 中的顺序。这些模型中允许的额外宽松是读操作之后的内存操作可以相对于读取进行重叠和重新排序。 Alpha 和 RMO 允许读取返回另一个处理器早期写入的值。从程序员的角度来看,这些模型必须保持写入原子性的假象,即使它们允许处理器提前读取自己的写入。
实例
TSO (Total Store Order) 和 ARM、RISC-V的 relaxed memory model 都属于宽松的内存一致性模型,但它们的顺序保证和重排规则有所不同。
1. TSO (Total Store Order)
TSO是x86架构使用的内存模型,具有如下特点:
- 写后读(Write-Read)重排:允许处理器将自己的写操作暂存在写缓冲区中,写操作可能在写缓冲区中延迟,使得在该处理器执行写操作后,其他处理器可能暂时不可见。但该处理器自身后续的读操作可以绕过该写缓冲区,从而重排执行。
- 保持读-读、写-写、读-写的顺序:除了写后读的重排,其他所有指令顺序依然会被严格维护。
- 内存屏障(Memory Barrier):需要使用
mfence等内存屏障指令来防止写后读的重排,从而确保多处理器下的全局一致性。
这种模型设计保证了内存的可预见性较强,但通过写缓冲区实现了一定的性能优化。
2. ARM的内存一致性模型(Release Consistency 和 Weak Ordering)
ARM架构使用了一种更宽松的内存一致性模型(Release Consistency 和 Weak Ordering),这意味着:
- 更多的重排:ARM允许写-读(Write-Read)、读-写(Read-Write)、读-读(Read-Read)和写-写(Write-Write)重排。因此,各种内存操作的顺序在不违背依赖性时可能会被重排。
- Acquire 和 Release语义:ARM架构引入了Acquire和Release的同步机制。通过Acquire(获取)确保进入临界区前看到最新的共享数据,通过Release(释放)确保退出临界区时让所有写操作可见。通常通过
dmb(数据内存屏障)等指令配合实现。 - 内存屏障需求更频繁:由于ARM的模型允许更多重排,因此在同步共享数据的场景下,开发者需要更频繁地使用屏障(例如
dmb、dsb)来确保正确的操作顺序。
ARM的这种设计提供了极大的性能优化空间,但开发者在多核系统中需要通过屏障指令来保证操作的正确顺序。
3. RISC-V的内存一致性模型(RVWMO - RISC-V Weak Memory Ordering)
RISC-V采用的是弱内存排序模型(RVWMO),设计上与ARM类似:
- 高度宽松的操作重排:RISC-V允许几乎所有类型的重排,包括写-读、读-写等,因此没有严格的顺序限制。
- Acquire 和 Release 语义:和ARM类似,RISC-V也支持Acquire和Release的同步机制,并引入了
fence指令(如fence.rw、fence.r等)来控制操作的顺序。 - 高度可配置的屏障:RISC-V的
fence指令可以灵活地指定顺序的约束,开发者可以细粒度地控制内存操作的执行顺序以确保数据一致性。
RISC-V的宽松内存模型使得它具有高度的性能潜力,但也需要开发者更细致地控制同步顺序。
总结对比
| 特性 | TSO (x86) | ARM (Release Consistency) | RISC-V (RVWMO) |
| 写后读重排 | 允许 | 允许 | 允许 |
| 读-写、写-写、读-读重排 | 不允许 | 允许 | 允许 |
| Acquire/Release语义 | 不显式支持 | 支持 | 支持 |
| 屏障使用频率 | 较少 | 较多 | 较多 |
| 性能潜力 | 较高 | 很高 | 很高 |
TSO模型相对简单,限制较多,但提供了更高的可预测性和易于编程的特性。而ARM和RISC-V模型则提供了更宽松的重排规则,具有更高的性能潜力,但需要更复杂的同步管理。
Transactional memory models
事务内存模型是缓存一致性和内存一致性模型的结合,作为软件或硬件支持的共享内存系统的通信模型;事务内存模型提供内存一致性和缓存一致性。事务是由将数据从一种一致状态转换为另一种一致状态的进程执行的一系列操作。事务要么在没有冲突时提交,要么中止。在提交中,当事务完成时,所有更改对所有其他进程都是可见的,而中止则丢弃所有更改。与宽松一致性模型相比,事务模型更易于使用,并且比顺序一致性模型可以提供更高的性能。
Other consistency models
其他的一些一致性模型如下:
- Causal+ consistency[22][23]
- Delta consistency
- Fork consistency
- One-copy serializability
- Serializability
- Vector-field consistency
- Weak consistency
- Strong consistency
其他几个一致性模型已经被构想来表达对操作的顺序或可见性的限制,或者处理特定的故障假设。
Consistency and replication
Tanenbaum 等人,2007 年定义了复制的两个主要原因;可靠性和性能。通过在当前副本发生故障的情况下切换到另一个副本,可以在复制的文件系统中实现可靠性。复制还通过在不同副本上提供多个数据副本来保护数据免遭损坏。它还通过划分工作来提高性能。虽然复制可以提高性能和可靠性,但它可能会导致多个数据副本之间的一致性问题。如果读取操作从所有副本返回相同的值,并且写入操作作为单个原子操作(事务)在任何其他操作发生之前更新所有副本,则多个副本是一致的。 Tanenbaum、Andrew 和 Maarten Van Steen,2007 年将这种类型的一致性称为同步复制提供的严格一致性。然而,应用全局同步来保持所有副本的一致性是昂贵的。降低全局同步成本并提高性能的一种方法是削弱一致性限制。
Data-centric consistency models
Tanenbaum 等人,2007 年将一致性模型定义为软件(进程)和内存实现(数据存储)之间的契约。该模型保证如果软件遵循一定的规则,内存就能正常工作。由于在没有全局时钟的系统中,定义写入中的最后一个操作很困难,因此可以对读取操作可以返回的值应用一些限制。以数据为中心的一致性模型的目标是提供数据存储的一致视图,其中进程可以执行并发更新。
一致的操作顺序
一些一致性模型(例如顺序一致性模型和因果一致性模型)处理共享复制数据上的操作顺序以提供一致性。在这些模型中,所有副本必须就一致的全局更新顺序达成一致。
分组操作
在分组操作中,对同步变量的访问是顺序一致的。一个进程被允许访问一个同步变量,之前的所有写入都已经完成。换句话说,在对同步变量的所有操作完全执行之前,不允许访问同步变量。[4]
Client-centric consistency models
在分布式系统中,保持顺序一致性以控制并发操作是必不可少的。在一些没有同时更新的特殊数据存储中,以客户端为中心的一致性模型可以以较低成本的方式处理不一致。以下模型是一些以客户端为中心的一致性模型:[4]
一致性协议
一致性模型的实现由一致性协议定义。Tanenbaum 等人,2007 [4]说明了一些以数据为中心的模型的一致性协议。
持续一致性
由 Yu 和 Vahdat (2000) 引入的连续一致性。[25]在这个模型中,应用程序的一致性语义是通过在应用程序中使用 conits 来描述的。由于一致性要求可能因应用程序语义而异,Yu 和 Vahdat (2000) [25]认为预定义的统一一致性模型可能不是合适的方法。应用程序应指定满足应用程序语义的一致性要求。在此模型中,应用程序将每个一致性要求指定为一个conits(一致性单元的缩写)。Conit 可以是物理或逻辑一致性,用于衡量一致性。Tanenbaum et al., 2007 [4]通过举例说明了 conit 的概念。
应用程序可以容忍三种不一致。
数值偏差[25]
数值偏差限制了 conit 值与上次更新的相对值之间的差异。可以为写入分配权重,该权重定义写入在特定应用程序中的重要性。conit 的未见写入的总权重可以定义为应用程序中的数值偏差。有两种不同类型的数值偏差;绝对和相对数值偏差。
订货偏差[25]
排序偏差是副本中写入的本地顺序与其在最终图像中的相对顺序之间的差异。
副本之间的陈旧偏差[25]
陈旧偏差通过限制当前时间和最旧写入时间之间的差异来定义最旧写入的有效性,这些时间在本地未看到的时间。每个服务器都有一个不确定写入的本地队列,需要一个实际的顺序来确定和应用一个conit。不确定写队列的最大长度是排序偏差的界限。当写入次数超过限制时,服务器不会接受新提交的写入,而是会根据写入应执行的顺序,通过与其他服务器通信来尝试提交不确定的写入。
如果三个偏差界都设置为零,则连续一致性模型为强一致性。
基于初级的协议
基于主的协议[4]可以被认为是一类更易于实现的一致性协议。例如,当考虑操作的一致排序时,顺序排序是一种流行的一致性模型。顺序排序可以确定为基于主要的协议。在这些协议中,数据存储中的每个数据项都有一个关联的主节点,以协调对该数据项的写入操作。
远程写入协议
在支持复制的最简单的基于主的协议(也称为主备份协议)中,写操作被转发到单个服务器,而读操作可以在本地执行。
示例: Tanenbaum 等人,2007 [4]给出了一个主备协议的例子。主备协议图显示了该协议的一个示例。当客户端请求写时,写请求被转发到主服务器。主服务器向备份发送请求以执行更新。服务器然后从所有备份接收更新确认并将写入完成的确认发送给客户端。任何客户端都可以在本地读取最新的可用更新。此协议的权衡是发送更新请求的客户端可能必须等待很长时间才能获得确认才能继续。这个问题可以通过在本地执行更新,然后让其他备份执行他们的更新来解决。非阻塞主备协议不保证所有备份服务器更新的一致性。然而,它提高了性能。在主备协议中,所有进程将看到相同的写操作顺序,因为该协议根据全局唯一时间对所有传入的写操作进行排序。阻塞协议保证进程查看最后一次写操作的结果。
主要备份协议
本地写入协议
在基于初级的本地写入协议中,[4]主副本在愿意执行更新的进程之间移动。要更新数据项,进程首先将其移动到其位置。因此,在这种方法中,可以在本地执行连续的写入操作,而每个进程都可以读取其本地数据项副本。主节点完成更新后,更新将转发给其他副本,所有副本都在本地执行更新。这种非阻塞方法可以带来改进。本地写协议图描述了基于主协议的本地写方法。进程请求对数据项 x 进行写操作。当前服务器被视为数据项 x 的新主服务器。执行写入操作,当请求完成时,主服务器向其他备份服务器发送更新请求。
主备协议(本地写入)
复制写入协议
在复制写入协议中,[4]与基于主的协议不同,所有更新都对所有副本执行。
主动复制
在主动复制中,[4]有一个与每个副本关联的进程来执行写操作。换句话说,更新以操作的形式发送到每个副本以便执行。所有更新都需要在所有副本中以相同的顺序执行。因此,需要一种完全有序的多播机制。在大型分布式系统中实现这种多播机制存在可伸缩性问题。还有另一种方法,其中每个操作都发送到中央协调器(定序器)。协调器首先为每个操作分配一个序列号,然后将操作转发给所有副本。第二种方法也不能解决可伸缩性问题。
基于群体的协议
投票可以是复制写入协议中的另一种方法。在这种方法中,客户端请求并接收来自多个服务器的许可,以便读取和写入复制的数据。例如,假设在一个分布式文件系统中,一个文件被复制到 N 个服务器上。要更新文件,客户端必须至少向N/2 + 1发送请求,以便他们同意执行更新。达成协议后,将对文件应用更改,并为更新后的文件分配新的版本号。类似地,为了读取复制的文件,客户端向N/2 + 1 个服务器发送请求,以便从这些服务器接收相关的版本号。如果所有接收到的版本号都是最新版本,则读取操作完成。[4]
缓存一致性协议
在复制文件系统中,缓存一致性协议[4]提供缓存一致性,而缓存通常由客户端控制。在许多方法中,缓存一致性由底层硬件提供。基于中间件的分布式系统中的一些其他方法应用基于软件的解决方案来提供缓存一致性。
缓存一致性模型在定义不一致何时发生的一致性检测策略方面可能有所不同。有两种方法可以检测不一致;静态和动态解决方案。在静态解决方案中,编译器确定哪些变量会导致缓存不一致。因此,编译器强制执行指令以避免不一致问题。在动态解决方案中,服务器在运行时检查不一致性,以控制缓存数据在缓存后发生变化的一致性。
一致性实施策略是另一种缓存一致性协议。它定义了如何通过使用位于服务器上的副本来提供缓存的一致性。保持数据一致的一种方法是从不缓存共享数据。服务器可以保留数据并应用一些一致性协议,例如基于主的协议来确保共享数据的一致性。在此解决方案中,客户端只能缓存私有数据。在缓存共享数据的情况下,有两种方法可以强制缓存一致性。
在第一种方法中,当共享数据更新时,服务器将失效转发给所有缓存。在第二种方法中,传播更新。大多数缓存系统应用这两种方法或在它们之间动态选择。