我将给出我理解内存一致性模型(简称内存模型)的类比。它的灵感来自莱斯利·兰波特的开创性论文《分布式系统中的时间、时钟和事件排序》。这个比喻很恰当,具有根本意义,但对许多人来说可能是过度的。然而,我希望它提供了一个有助于记忆一致性模型推理的心理图像(图像表示)。
让我们在时空图中查看所有存储位置的历史,其中横轴表示地址空间(即,每个存储位置由该轴上的一个点表示),纵轴表示时间(我们将看到,一般来说,时间没有一个通用的概念)。因此,每个存储器位置所保持的值的历史记录由该存储器地址处的垂直列表示。每个值更改都是由于其中一个线程向该位置写入一个新值。通过内存映像,我们将指特定线程在特定时间可观察到的所有内存位置的值的集合/组合。
引用“内存一致性和缓存一致性入门”
直观的(也是最具限制性的)内存模型是顺序一致性(SC),其中多线程执行应该看起来像是每个组成线程的顺序执行的交织,就好像线程在单核处理器上进行了时间复用。
该全局内存顺序可能因程序运行的不同而不同,并且可能事先不知道。SC的特征是地址空间时间图中表示同时性平面(即存储器图像)的一组水平切片。在给定平面上,其所有事件(或内存值)都是同时发生的。有一个绝对时间的概念,其中所有线程都同意哪些内存值是同时的。在SC中,在每个时刻,只有一个内存映像被所有线程共享。也就是说,在每一个时刻,所有处理器都同意内存映像(即内存的聚合内容)。这不仅意味着所有线程都查看所有内存位置的相同值序列,而且所有处理器都观察所有变量的相同值组合。这与所有线程以相同的总顺序观察所有内存操作(在所有内存位置上)相同。
在宽松的内存模型中,每个线程都将以自己的方式分割地址空间时间,唯一的限制是每个线程的切片不能相互交叉,因为所有线程必须在每个单独内存位置的历史上达成一致(当然,不同线程的切片可能也会相互交叉)。没有通用的方法来分割它(没有地址空间时间的特权叶理)。切片不必是平面(或线性)的。它们可以是弯曲的,这可以使线程读取由另一个线程写入的值,而不是它们的写入顺序。当被任何特定线程查看时,不同内存位置的历史记录可能会相对于彼此任意滑动(或拉伸)。每个线程将对哪些事件(或相当于内存值)是同时发生的有不同的感知。与一个线程同时发生的一组事件(或内存值)与另一个线程不同时发生。因此,在宽松的内存模型中,所有线程仍然对每个内存位置观察相同的历史(即,值序列)。但是他们可以观察到不同的记忆图像(即,所有记忆位置的值的组合)。即使同一线程按顺序写入两个不同的内存位置,其他线程也可能以不同的顺序观察到两个新写入的值。
[图片来自维基百科]
熟悉爱因斯坦的狭义相对论的读者会注意到我的意思。将闵可夫斯基的话翻译成记忆模型领域:地址空间和时间是地址空间时间的影子。在这种情况下,每个观察者(即线程)将事件的阴影(即内存存储/加载)投射到自己的世界线(即时间轴)和自己的同时平面(即地址空间轴)上。C++11内存模型中的线程对应于在狭义相对论中相对移动的观察者。顺序一致性对应于伽利略时空(即,所有观察者都同意事件的一个绝对顺序和全局同时性)。
记忆模型和狭义相对论之间的相似之处在于,两者都定义了一组部分有序的事件,通常称为因果集。某些事件(即内存存储)可能会影响(但不受影响)其他事件。C++11线程(或物理中的观察者)不过是一个事件链(即,一个完全有序的集合)(例如,内存加载和存储到可能不同的地址)。
在相对论中,部分有序事件的混沌图景恢复了某种秩序,因为所有观测者都同意的唯一时间顺序是“类时间”事件之间的顺序(即,那些原则上可由任何比真空中光速慢的粒子连接的事件)。只有与时间相关的事件是按不变顺序排列的。《物理学时间》,克雷格·卡伦德。
在C++11记忆模型中,使用类似的机制(获取-释放一致性模型)来建立这些局部因果关系。
为了提供内存一致性的定义和放弃SC的动机,我将引用“内存一致性和缓存一致性入门”
对于共享内存机器,内存一致性模型定义了其内存系统的架构可见行为。单处理器内核的正确性标准将行为划分为“一个正确的结果”和“许多不正确的选择”。这是因为处理器的体系结构要求线程的执行将给定的输入状态转换为单个定义良好的输出状态,即使是在无序的内核上。然而,共享内存一致性模型涉及多个线程的加载和存储,通常允许许多正确的执行,而不允许许多(更多)错误的执行。多个正确执行的可能性是由于ISA允许多个线程同时执行,通常来自不同线程的指令之间存在许多合法的交错。宽松或弱内存一致性模型的动机是,强模型中的大多数内存排序是不必要的。如果一个线程更新了十个数据项,然后更新了一个同步标志,那么程序员通常不关心数据项是否按照彼此的顺序进行更新,而只关心所有数据项在标志更新之前进行更新(通常使用FENCE指令实现)。宽松的模型试图捕捉这种增加的排序灵活性,并只保留程序员“需要”的顺序,以获得更高的性能和SC的正确性。例如,在某些架构中,每个内核使用FIFO写入缓冲区来保存提交(失效)存储的结果,然后再将结果写入缓存。这种优化提高了性能,但违反了SC。写入缓冲区隐藏了服务存储未命中的延迟。由于存储是常见的,因此能够避免在大多数存储上出现延迟是一个重要的好处。对于单核处理器,通过确保加载到地址a将最新存储的值返回到a,即使写入缓冲区中有一个或多个存储到a,也可以使写入缓冲区在架构上不可见。这通常是通过将最新存储到A的值旁路到从A加载来完成的,其中“最新”由程序顺序决定,或者如果到A的存储在写入缓冲区中,则通过暂停加载A来完成。当使用多个内核时,每个内核都有自己的旁路写入缓冲区。没有写缓冲区,硬件是SC,但有写缓冲区就不是SC,这使得写缓冲区在多核处理器中在架构上是可见的。如果核心具有非FIFO写入缓冲区,允许存储以不同于其输入顺序的顺序离开,则可能会发生存储重新排序。如果第一个存储在缓存中未命中,而第二个命中,或者如果第二个存储可以与较早的存储合并(即,在第一个存储之前),则可能发生这种情况。加载-加载重新排序也可能发生在动态调度的内核上,这些内核按照程序顺序执行指令。这可以像重新排序另一个内核上的存储一样(你能想出一个两个线程之间的交错示例吗?)。用稍后的存储重新排序先前的加载(加载存储重新排序)可能会导致许多错误行为,例如在释放保护它的锁之后加载值(如果存储是解锁操作)。注意,由于通常实现的FIFO写入缓冲区中的本地旁路,甚至在内核按程序顺序执行所有指令的情况下,也可能出现存储加载重新排序。
由于缓存一致性和内存一致性有时会被混淆,因此也可以引用以下内容:
与一致性不同,缓存一致性对软件来说既不可见,也不需要。Coherence试图使共享内存系统的缓存在功能上与单核系统中的缓存一样不可见。正确的一致性确保程序员无法通过分析加载和存储的结果来确定系统是否以及在何处有缓存。这是因为正确的一致性确保了缓存永远不会启用新的或不同的功能行为(程序员仍然可以使用定时信息推断可能的缓存结构)。缓存一致性协议的主要目的是保持单写多读(SWMR)对每个内存位置的不变。一致性和一致性之间的一个重要区别是,一致性是在每个记忆位置的基础上指定的,而一致性是针对所有记忆位置指定的。
继续我们的心理图景,SWMR不变量对应于物理要求,即在任何一个位置最多有一个粒子,但任何位置都可以有无限数量的观察者。