43、内存一致性:从连贯性到顺序一致性的深入剖析

内存一致性:从连贯性到顺序一致性的深入剖析

在计算机系统中,内存一致性是一个至关重要的概念,它直接影响着多线程程序的正确性和性能。本文将深入探讨内存一致性的相关概念,包括连贯性、存储原子性和顺序一致性,并详细分析它们的特点、模型以及应用。

1. 连贯性与存储原子性

在多线程环境中,内存访问的顺序和结果是需要严格控制的。连贯性和存储原子性是两个重要的内存系统属性,它们分别从不同的角度对内存访问进行约束。

1.1 存储原子性的限制与需求

存储原子性要求对同一地址的访问在实时中进行排序。例如,在某些执行场景下,如果T1对A的存储操作要在T1执行对A的加载以及对B的加载之前传播到所有副本(包括T2缓存中的副本)。然而,存储原子性仅关注单个内存位置,对于不同位置的访问以及屏障操作,它无法提供足够的约束。这就需要其他机制来避免不正确的执行,例如硬件必须能够识别屏障,并在屏障执行之前检测到存储操作已全局执行。

1.2 示例分析

考虑以下执行示例:

INIT A=0;B=0;
T1
T2
S1(A)1
S2(B)1
L1(B)0
L2(A)0

这个执行是连贯的,因为每个地址只有两个访问,且它们在不同的线程中。同时,它也是存储原子的。即使在两个线程的存储和加载之间添加屏障,结果仍然是连贯和存储原子的,但可能是不正确的。

2. 存储原子性与内存交错

存储原子性和内存交错的模型考虑了对所有地址的访问。每个地址分配到一个单独的内存,线程可以按照与线程内依赖关系以及对不同地址访问的可能顺序一致的任何顺序发出内存访问请求。

2.1 模型特点
  • 每个内存依次响应每个请求(加载或存储),使得对每个位置的内存访问是原子的。
  • 每个线程可以对不同的内存有多个未完成的请求,并且所有线程可以并行访问内存。
2.2 内存访问排序规则
  • 如果两个内存访问是针对同一位置,或者针对不同位置但它们对内存的访问不是并发的(时间上不重叠),则可以按照它们访问内存的实时顺序进行排序,同时保持执行的正确结果。
  • 如果两个对不同地址的内存访问并发访问它们各自的内存,由于它们已经在内存中,并且所有输入操作数的值都可用,因此它们不会相互影响对方的地址或输入值。这样的并发内存访问可以任意排序,同时保持执行的正确结果,例如可以按照它们开始的时间进行排序。

这种内存交错系统的一个有趣特性是,在尊重执行约束的情况下,对所有内存位置的所有存储操作进行全局排序是可能的。与普通连贯性不同,存储原子性是可组合的,即通过仅对每个单独的内存位置的存储操作进行排序,就可以对所有内存位置的所有存储操作实施全局顺序。

3. 顺序一致性

普通连贯性或存储原子性虽然是理想的内存系统属性,但通常不足以保证执行的正确性,因为对不同共享内存位置的访问可能会相互影响。顺序一致性是一种更严格的内存一致性模型,它要求多线程程序的执行必须与线程中所有指令的顺序、交错执行一致。

3.1 顺序一致性的重要性

从软件的角度来看,顺序一致性模型非常有吸引力。程序员通常期望不同线程的连续内存访问指令的效果在实时中按照线程顺序原子地交错。例如,在以下代码片段中:

INIT: A=FLAG=0
T1
T2
A=1 ;
while(FLAG==0);
FLAG=1;
Print A;

程序员期望T1对A的更新在对FLAG的更新之前到达T2,以便T2始终打印值1。

3.2 形式模型

顺序一致性的形式模型中,每个线程按照线程顺序依次执行对共享内存的访问,并且同一时间只执行一个对共享内存的访问。一个系统如果每个执行在该形式模型上都是有效的执行,则该系统是顺序一致的。

以下是顺序一致性的定义:
一个多处理器是顺序一致的,如果任何执行的结果与所有线程的内存操作按某种顺序执行,并且每个线程的操作按线程顺序出现的结果相同。

3.3 示例分析

考虑以下代码:

INIT: A=B=0
T1
T2
A=1
Print B;
B=1
Print A;

在顺序一致性下,打印的A和B的值的有效结果为(A,B) = (0,0)、(1,0)和(1,1),而(A,B) = (0,1)是不可能的。假设T1在实时中先全局执行B = 1,然后再执行A = 1,这可能是因为T1的缓存决定先执行对B的存储(如果对A的存储在缓存中缺失,而对B的存储不缺失)。但如果T2执行两个打印语句的时间较晚,在T1对A和B的更新都全局执行之后,使得T2的加载与T1的存储在实时中不重叠,那么结果是(A,B) = (1,1),这意味着该执行是顺序一致的。

4. 顺序一致性的相关规则与证明

确定一个执行是否是顺序一致的通常是复杂且容易出错的,但可以通过一组简单的规则在执行图上进行验证。

4.1 访问排序规则
  • 线程顺序规则 :如果Opi(A)在线程顺序上先于Opi(B),则Opi(A)在顺序一致性顺序上也先于Opi(B),即Opi(A) t.o. → Opi(B) ⇒ Opi(A) s.c. → Opi(B)。
  • 存储源规则 :如果存储Si(A)为加载Lj(A)提供值,则Si(A)在顺序一致性顺序上先于Lj(A),即Val[Si(A)]: Val[Lj(A)] ⇒ Si(A) s.c. → Lj(A)。
  • 隐含规则
  • 如果存储Si(A)为加载Lj(A)提供值,并且Sk(A)在顺序一致性顺序上先于Lj(A),则Sk(A)在顺序一致性顺序上先于Si(A),即(Val[Si(A)]: Val[Lj(A)]).and.(Sk(A) s.c. → Lj(A)) ⇒ Sk(A) s.c. → Si(A)。
  • 如果存储Si(A)为加载Lj(A)提供值,并且Si(A)在顺序一致性顺序上先于Sk(A),则Lj(A)在顺序一致性顺序上先于Sk(A),即(Val[Si(A)]: Val[Lj(A)]).and.(Si(A) s.c. → Sk(A)) ⇒ Lj(A) s.c. → Sk(A)。

这些规则表明,如果一个存储为后续的加载提供值,那么在该存储和加载之间不能插入对同一地址的其他存储。

4.2 证明示例

考虑以下执行,它是Dekker算法的一部分:

INIT: A=B=0
T1
T2
S1(A)1
S2(B)1
L1(B)0
L2(A)0

为了证明这个执行不是顺序一致的,我们根据顺序一致性的规则构建执行图。如果最终的图有循环,则该执行无法按照顺序一致性进行排序。在这个例子中,构建的执行图有循环,因此该执行不是顺序一致的。

5. 入站消息管理

在顺序一致的系统中,缓存层次结构和互连之间的访问缓冲是一个重要的问题。我们主要关注本地缓存层次结构发送和接收的请求/回复,而忽略本地目录控制器发送和接收的消息。

5.1 硬件模型

一个简化的具有深内存层次结构和无锁定缓存的系统架构模型使用入站和出站缓冲区来抽象通过复杂缓存层次结构的延迟。在顺序一致性中,每个线程一次只能有一个出站内存请求(BusRd、BusRdX或BusUpdate)处于待处理状态,但由于每个处理器中有多个线程运行,可能会有与线程数量相同的出站请求。入站消息也会被缓冲,并且处理器节点的入站缓冲区可能包含由不同远程节点的请求触发的多个消息。

5.2 协议优化

在基于目录的MSI - 无效协议和MSI - 更新协议中,可以对入站消息的处理进行优化,同时不损害存储原子性和顺序一致性。

  • MSI - 无效协议
  • 当本地块副本处于共享状态时,无效请求可以在到达本地节点的入站缓冲区时立即被确认,并且一旦入站缓冲区接收到无效请求,就可以认为相对于本地节点的存储操作已执行。
  • 当本地副本处于修改状态时,传入的刷新请求不能在到达缓存层次结构的入站缓冲区时被确认,而必须从本地缓存层次结构中检索块副本,并在处理刷新请求之前先处理所有在其之前的入站消息。
  • MSI - 更新协议
  • 当本地副本处于共享状态时,入站更新在到达处理器节点时被确认。更新会修改缓存块并锁定它,随后接收存储全局执行(store GP)信号以解锁副本并使其可访问。
  • 当本地节点处于修改状态时,节点必须响应刷新请求。这些请求在到达具有有效副本的本地缓存时通过刷新块副本进行确认。在处理过程中,所有先前的入站消息都要应用到缓存中。
5.3 Dekker算法示例

以Dekker算法为例,展示了如何应用新的入站消息规则。通过逐步分析算法的执行过程,可以看到如何在保证顺序一致性的同时优化存储操作的执行速度。

6. 存储操作的快速执行

为了利用顺序一致性上下文中入站请求的优化,需要对“执行存储操作”的定义进行略微修改。如果按照上述方式对核心中的传入请求进行适当管理,当运行线程的核心收到请求并能够安全处理时,就可以认为相对于该线程的存储操作已执行。这允许更快地执行存储操作,因为它们比之前的定义更快地被确认。

当本地块处于共享状态时,远程存储操作不需要等到其通知(无效或更新)到达目标核心的缓存层次结构的所有副本才被认为相对于该核心已执行,而是在通知到达总线接口时就可以认为已执行。当本地块处于修改状态时,通知必须等到请求到达修改后的副本并刷新块后才能执行。

综上所述,内存一致性是多线程系统中一个复杂而关键的问题。从连贯性和存储原子性到顺序一致性,不同的模型和规则为我们提供了控制内存访问顺序和结果的方法。通过合理应用这些概念和优化策略,可以提高多线程程序的正确性和性能。

内存一致性:从连贯性到顺序一致性的深入剖析

7. 顺序一致性的形式模型与验证

顺序一致性有着明确的形式模型,它对于判断系统是否满足顺序一致性至关重要。

7.1 形式模型的规则

顺序一致性的形式模型中,每个线程按线程顺序逐个执行对共享内存的访问,且同一时间仅执行一个共享内存访问。具体规则如下:
- 线程按自身顺序依次执行对共享内存的访问。
- 同一时刻仅允许一个共享内存访问操作执行。

只有当系统的每个执行在该形式模型上都是有效执行时,该系统才是顺序一致的。

7.2 验证步骤

若要证明一个系统是顺序一致的,需为每个执行构建所有线程对所有内存位置的内存访问的连贯串行顺序。具体步骤如下:
1. 针对每个执行,收集所有线程对所有内存位置的内存访问信息。
2. 按照线程顺序和顺序一致性规则,对这些访问进行排序。
3. 检查排序后的顺序是否满足顺序一致性的定义,即结果是否与所有线程的内存操作按某种顺序执行,且每个线程的操作按线程顺序出现的结果相同。

8. 顺序一致性的访问排序规则详解

顺序一致性的访问排序规则是判断执行是否符合顺序一致性的关键依据。

8.1 基本规则
  • 线程顺序规则 :若在线程顺序中Opi(A)先于Opi(B),那么在顺序一致性顺序中Opi(A)也先于Opi(B),即Opi(A) t.o. → Opi(B) ⇒ Opi(A) s.c. → Opi(B)。
  • 存储源规则 :当存储Si(A)为加载Lj(A)提供值时,在顺序一致性顺序中Si(A)先于Lj(A),即Val[Si(A)]: Val[Lj(A)] ⇒ Si(A) s.c. → Lj(A)。
8.2 隐含规则
  • 若存储Si(A)为加载Lj(A)提供值,且Sk(A)在顺序一致性顺序中先于Lj(A),则Sk(A)在顺序一致性顺序中先于Si(A),即(Val[Si(A)]: Val[Lj(A)]).and.(Sk(A) s.c. → Lj(A)) ⇒ Sk(A) s.c. → Si(A)。
  • 若存储Si(A)为加载Lj(A)提供值,且Si(A)在顺序一致性顺序中先于Sk(A),则Lj(A)在顺序一致性顺序中先于Sk(A),即(Val[Si(A)]: Val[Lj(A)]).and.(Si(A) s.c. → Sk(A)) ⇒ Lj(A) s.c. → Sk(A)。

这些规则保证了在存储为后续加载提供值时,同一地址的其他存储不能插入其间。

9. 入站消息管理的优化策略

在顺序一致的系统中,入站消息管理的优化对于提高系统性能至关重要。

9.1 优化目标

优化入站消息的处理,在不损害存储原子性和顺序一致性的前提下,减少确认入站消息所需的时间。

9.2 不同协议下的优化策略
协议类型 本地块状态 优化策略
MSI - 无效协议 共享状态 无效请求到达本地节点入站缓冲区时立即确认,入站缓冲区接收到无效请求后,可认为相对于本地节点的存储操作已执行。
MSI - 无效协议 修改状态 传入的刷新请求到达缓存层次结构入站缓冲区时不能确认,需从本地缓存层次结构中检索块副本,并先处理所有在其之前的入站消息。
MSI - 更新协议 共享状态 入站更新到达处理器节点时确认,更新修改缓存块并锁定,随后接收存储全局执行信号解锁副本。
MSI - 更新协议 修改状态 节点响应刷新请求,请求到达具有有效副本的本地缓存时通过刷新块副本确认,处理过程中应用所有先前的入站消息。
10. 顺序一致性在实际应用中的挑战与解决方案

顺序一致性在实际应用中面临着一些挑战,需要相应的解决方案。

10.1 挑战分析
  • 优化难度大 :如在某些情况下,线程可能会提前全局执行某些存储操作,但要实现顺序一致性的优化,线程间需要高度的协调,而在实际系统中这种协调很难实现。
  • 难以利用存储缓冲区 :存储缓冲区允许加载操作在存储操作之前执行,这与顺序一致性要求的全局连贯顺序相冲突,使得顺序一致性难以利用存储缓冲区。
10.2 解决方案
  • 合理规划线程执行 :尽量减少线程间的冲突,避免不必要的提前存储操作,以降低协调难度。
  • 优化存储缓冲区管理 :制定严格的规则,确保存储缓冲区的操作符合顺序一致性的要求,例如在执行加载操作前,确保所有先前的存储操作已全局执行。
11. 顺序一致性的优化流程 mermaid 图
graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([开始]):::startend --> B{选择协议类型}:::decision
    B -->|MSI - 无效协议| C{本地块状态}:::decision
    B -->|MSI - 更新协议| D{本地块状态}:::decision
    C -->|共享状态| E(立即确认无效请求):::process
    C -->|修改状态| F(检索块副本,处理先前入站消息):::process
    D -->|共享状态| G(确认入站更新,锁定缓存块):::process
    D -->|修改状态| H(响应刷新请求,刷新块副本):::process
    E --> I([结束]):::startend
    F --> I
    G --> I
    H --> I
12. 总结

内存一致性是多线程系统中的核心问题,从连贯性、存储原子性到顺序一致性,不同的模型和规则为控制内存访问顺序和结果提供了方法。顺序一致性作为最严格的内存一致性模型,虽然在实际应用中面临挑战,但通过合理应用其规则和优化策略,如入站消息管理优化、存储操作定义修改等,可以提高多线程程序的正确性和性能。在未来的多线程系统设计中,深入理解和应用这些概念将有助于构建更高效、更可靠的系统。

内容概要:本文详细介绍了“秒杀商城”微服务架构的设计与实战全过程,涵盖系统从需求分析、服务拆分、技术选型到核心功能开发、分布式事务处理、容器化部署及监控链路追踪的完整流程。重点解决了高并发场景下的超卖问题,采用Redis预减库存、消息队列削峰、数据库乐观锁等手段保障数据一致性,并通过Nacos实现服务注册发现与配置管理,利用Seata处理跨服务分布式事务,结合RabbitMQ实现异步下单,提升系统吞吐能力。同时,项目支持Docker Compose快速部署和Kubernetes生产级编排,集成Sleuth+Zipkin链路追踪与Prometheus+Grafana监控体系,构建可观测性强的微服务系统。; 适合人群:具备Java基础和Spring Boot开发经验,熟悉微服务基本概念的中高级研发人员,尤其是希望深入理解高并发系统设计、分布式事务、服务治理等核心技术的开发者;适合工作2-5年、有志于转型微服务或提升架构能力的工程师; 使用场景及目标:①学习如何基于Spring Cloud Alibaba构建完整的微服务项目;②掌握秒杀场景下高并发、超卖控制、异步化、削峰填谷等关键技术方案;③实践分布式事务(Seata)、服务熔断降级、链路追踪、统一配置中心等企业级中间件的应用;④完成从本地开发到容器化部署的全流程落地; 阅读建议:建议按照文档提供的七个阶段循序渐进地动手实践,重点关注秒杀流程设计、服务间通信机制、分布式事务实现和系统性能优化部分,结合代码调试与监控工具深入理解各组件协作原理,真正掌握高并发微服务系统的构建能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值