CAS的底层实现(转)
问了下DeepSeek原文的内容是否正确,DeepSeek认为原文对Atomic的compareAndSet方法(即CAS)在硬件层面的实现原理进行了较为全面的分析,核心内容是正确的。但有个别地方需要补充,比如DeepSeek说 “现代CPU通常不会完全放弃总线锁定。例如,当操作的数据跨多个缓存行(如未对齐的8字节操作)时,CPU可能仍会退化为总线锁。”
CAS的核心有两点
1. CAS是CPU层的原子操作,无法被中断,对于单核CPU,它不需要加锁,因为CPU执行该命令时,要执行完才能中断。目前市面上应该已经没有单核CPU了
2. 对于多核CPU,只有CAS原子操作是不行的,因为多核是并行执行,所以要加锁,有两种锁,如下。这种硬件锁比操作系统加锁速度要快的多,操作系统再快也是软件层面。
- 在内存总线上加锁,这种方式影响大一些,其它CPU无法访问所有内存地址
- 在缓存行上加锁,这种影响小很多。缓存行就是缓存L1、L2、L3这些CPU高速缓存上的数据行
下面是DeepSeek总结的这两种锁的区别
原文:https://juejin.cn/post/7450695647407325210
来源:稀土掘金
作者:凯歌_掘金
背景
在学习并发编程的过程中,学习到了Atomic类,知道里面的compareAndSet方法,是通过硬件层面,实现的原子操作,保证数据的原子性,那么硬件层面到底是怎么实现的那?对此,笔者感到很好奇,于是去查询了下,先将结果整理如下:
底层实现粗解
compareAndSet() 的原子性是由底层硬件支持的,通常通过 CPU 提供的原子指令来实现。在大多数现代处理器中,CAS 操作是通过专门的指令(如 x86 架构中的 CMPXCHG 指令)来完成的。这些指令确保了比较和交换的操作在同一时刻完成,不会被其他线程或进程打断。
CPU 的原子指令是如何实现原子性操作的?
CPU 的原子指令通过硬件级别的机制确保某些操作在多核或多处理器环境下是不可分割的,即这些操作不会被其他线程或处理器中断。这种机制保证了在并发环境中,多个线程可以安全地对共享资源进行读取、比较和写入操作,而不会导致数据不一致或其他竞态条件。
1. 什么是原子性操作?
原子性操作 指的是一个操作在执行时不会被其他线程或处理器打断,整个操作要么完全成功,要么完全失败。换句话说,其他线程或处理器无法在该操作的中间状态观察到部分完成的结果。这在多线程编程中非常重要,尤其是在处理共享资源时,确保数据的一致性和正确性。
2. CPU 原子指令的工作原理
CPU 实现原子性操作的核心思想是通过锁总线(Bus Locking) 或 锁缓存行(Cache Line Locking) 来确保某个内存地址的操作是独占的。具体来说,CPU 会通过以下几种方式来实现原子性:
2.1 锁总线(Bus Locking)
-
锁总线 是一种早期的机制,用于确保在整个操作期间,当前 CPU 核心独占访问系统总线。这意味着在操作期间,其他 CPU 核心无法访问同一块内存。
-
工作原理:
- 当 CPU 执行原子指令时,它会发出一个信号,锁定系统总线,防止其他 CPU 核心在同一时刻访问相同的内存地址。
- 在操作完成后,CPU 会释放总线锁,允许其他核心继续访问内存。
-
优点:简单且有效,适用于早期的多处理器系统。
-
缺点:锁总线会导致性能下降,因为它会阻塞所有其他 CPU 核心的内存访问,即使它们访问的是不同的内存地址。因此,现代 CPU 通常不再使用锁总线,而是转向更高效的机制。
2.2 锁缓存行(Cache Line Locking,也称为 MESI 协议)
-
锁缓存行 是现代多核处理器中常用的机制,它利用了缓存一致性协议(如 MESI 协议)来确保对特定内存地址的独占访问。MESI 是一种常见的缓存一致性协议,它定义了缓存行的状态:修改(Modified)、独占(Exclusive)、共享(Shared)和无效(Invalid)。
-
工作原理:
- 当 CPU 执行原子指令时,它会尝试获取目标内存地址对应的缓存行,并将其状态设置为独占(Exclusive)或修改(Modified)。
- 如果其他 CPU 核心已经缓存了该内存地址的副本,它们的缓存行将被标记为无效(Invalid),并且该内存地址的所有后续访问将被重定向到当前 CPU 核心的缓存。
- 这样,当前 CPU 核心可以在不锁定整个总线的情况下,独占地访问该内存地址,确保操作的原子性。
-
优点:相比于锁总线,锁缓存行的开销更小,因为它只锁定特定的缓存行,而不是整个总线。这使得其他 CPU 核心可以继续访问其他内存地址,从而提高了系统的并行性和性能。
-
缺点:如果多个 CPU 核心频繁竞争同一个缓存行,可能会导致缓存行乒乓效应(Cache Line Ping-Pong),即缓存行在不同核心之间频繁传递,影响性能。
常见的原子指令如下:
2.3 测试与设置(Test-and-Set, TSL)
-
测试与设置 是一种常见的原子指令,它允许 CPU 在一次操作中同时读取和修改某个内存位置的值。具体来说,
TSL
指令会读取一个内存位置的值,并将其设置为某个特定值(通常是 1),以表示该资源已被占用。 -
工作原理:
TSL
指令首先读取指定内存位置的值,然后将其设置为 1(或其他特定值),并将原来的值返回给程序。- 这个操作是原子的,意味着在读取和设置之间不会有其他线程或 CPU 核心能够插入操作。
- 通过这种方式,
TSL
可以用于实现简单的自旋锁(Spin Lock),线程可以在TSL
失败时继续“自旋”(即重复尝试),直到成功为止。
2.4 Compare-and-Swap (CAS)
-
Compare-and-Swap (CAS) 是另一种常见的原子指令,它允许 CPU 在一次操作中比较内存中的值并与预期值进行对比,如果匹配,则更新内存中的值;如果不匹配,则返回当前值。
-
工作原理:
CAS
指令接受三个参数:内存地址、预期值和新值。- 它首先读取内存地址中的值,并与预期值进行比较。
- 如果内存中的值等于预期值,则将内存中的值更新为新值,并返回
true
表示更新成功。 - 如果内存中的值不等于预期值,则不进行更新,并返回
false
表示更新失败。 - 这个操作是原子的,意味着在比较和交换之间不会有其他线程或 CPU 核心能够插入操作。
-
优点:
CAS
是无锁算法的基础,广泛应用于实现高性能的并发数据结构,如无锁队列、无锁栈等。 -
缺点:
CAS
存在 ABA 问题,即某个值从 A 变为 B,再变回 A,CAS
仍然会认为它是相同的值,这可能导致错误的结果。为了解决这个问题,Java 提供了AtomicStampedReference
和AtomicMarkableReference
,它们可以跟踪值的变化历史。
3. 现代 CPU 中的优化
现代多核处理器为了提高性能,还引入了一些优化机制来减少原子操作的开销:
-
缓存一致性协议(如 MESI、MOESI 等):这些协议确保多个 CPU 核心之间的缓存保持一致,避免了不必要的总线锁定。
-
存储顺序缓冲区(Store Buffer):每个 CPU 核心都有一个存储顺序缓冲区,用于暂存写操作。当多个 CPU 核心竞争同一个缓存行时,存储顺序缓冲区可以帮助减少冲突,提高性能。
-
乱序执行(Out-of-Order Execution):现代 CPU 支持乱序执行,允许指令在不影响程序语义的前提下重新排序。对于原子指令,CPU 会确保它们按照正确的顺序执行,以保证原子性。
4. 总结
-
CPU 的原子指令通过硬件级别的机制(如锁总线、锁缓存行、
TSL
和CAS
)来实现原子性操作。这些机制确保了在多核或多处理器环境下,某些关键操作不会被其他线程或处理器中断,从而保证了数据的一致性和正确性。 -
锁总线 是一种早期的机制,虽然简单但性能较差,现代 CPU 更倾向于使用锁缓存行来实现原子性操作,因为它只锁定特定的缓存行,减少了对其他核心的影响。
-
TSL
和CAS
是两种常见的原子指令,分别用于实现简单的自旋锁和无锁算法。CAS
特别适合用于实现高性能的并发数据结构,但需要注意 ABA 问题。
如果其他 CPU 核心已经缓存了该内存地址的副本,它们的缓存行将被标记为无效(Invalid),这个过程是如何实现的?
在现代多核处理器中,多个 CPU 核心共享同一块物理内存,但每个核心都有自己的缓存(L1、L2、L3 等)。为了确保多个核心之间的缓存一致性,处理器使用了缓存一致性协议(Cache Coherency Protocol)。这些协议确保了所有核心对共享内存的视图是一致的,即使它们各自有独立的缓存。
当一个核心需要对某个内存地址进行独占访问时(例如执行原子操作或写入操作),它会通过缓存一致性协议通知其他核心,要求它们将该内存地址对应的缓存行标记为无效(Invalid)。这个过程被称为缓存行失效(Cache Line Invalidations) 或 缓存行作废。
1. 缓存一致性协议(Cache Coherency Protocol)
缓存一致性协议定义了多个核心之间如何协作,以确保它们对共享内存的访问是同步的。最常用的缓存一致性协议之一是 MESI 协议(Modified, Exclusive, Shared, Invalid),它定义了缓存行的四种状态:
- M (Modified):缓存行已被修改,且只有当前核心拥有该缓存行的副本。其他核心没有该缓存行的副本。
- E (Exclusive):缓存行未被修改,且只有当前核心拥有该缓存行的副本。其他核心没有该缓存行的副本。
- S (Shared):缓存行未被修改,且可能有多个核心拥有该缓存行的副本。
- I (Invalid):缓存行无效,表示该缓存行的内容不可用,必须从主内存或其他核心重新加载。
2. 缓存行失效的过程
当一个核心需要对某个内存地址进行独占访问时,它会执行以下步骤来确保其他核心不会同时访问该内存地址:
2.1 发出失效请求(Invalidate Request)
- 当核心 A 需要对某个内存地址进行写入操作或执行原子指令时,它会首先检查自己的缓存行状态:
- 如果缓存行状态是 M 或 E,则核心 A 已经拥有该缓存行的独占访问权,可以直接进行写入或原子操作。
- 如果缓存行状态是 S,则说明其他核心也可能拥有该缓存行的副本。此时,核心 A 会向其他核心发送失效请求(Invalidate Request),要求它们将该缓存行标记为 I (Invalid)。
- 失效请求 是通过系统的互连网络(Interconnect Network) 发送的,通常是通过总线或点对点通信链路。每个核心都会监听这些请求,并根据请求内容更新自己的缓存状态。
2.2 其他核心响应失效请求
- 当其他核心接收到失效请求后,它们会检查自己是否缓存了该内存地址的副本:
- 如果某个核心 B 拥有该缓存行的副本(状态为 S),它会将该缓存行的状态标记为 I (Invalid),并停止对该缓存行的进一步访问。
- 如果某个核心 C 拥有该缓存行的副本并且它是脏的(Dirty,状态为 M),它会将该缓存行的内容写回到主内存(称为回写 Write-Back),然后将缓存行状态标记为 I (Invalid)。
- 这样,所有其他核心都不会再拥有该内存地址的有效副本,确保了核心 A 对该内存地址的独占访问。
2.3 核心 A 获取独占访问权
-
在所有其他核心响应失效请求并将缓存行标记为 I (Invalid) 后,核心 A 可以安全地将该缓存行的状态设置为 M (Modified) 或 E (Exclusive),并进行写入或原子操作。
-
如果核心 A 需要读取该内存地址的最新值,它会从主内存或其他核心重新加载该缓存行,确保数据的一致性。
3. 具体的实现机制
不同的处理器架构和缓存一致性协议可能会有不同的实现细节,但大多数现代多核处理器都遵循类似的原理。以下是几种常见的实现机制:
3.1 基于总线的协议(Bus-Based Protocols)
在早期的多核处理器中,缓存一致性是通过共享总线 实现的。所有核心通过同一个总线与主内存通信,因此当一个核心发出失效请求时,所有其他核心都能通过总线监听到该请求并作出响应。
- 优点:简单且易于实现。
- 缺点:随着核心数量的增加,总线带宽成为瓶颈,性能下降明显。因此,现代多核处理器通常不再使用基于总线的协议。
3.2 基于目录的协议(Directory-Based Protocols)
在现代多核处理器中,缓存一致性通常通过目录表(Directory Table) 来管理。目录表记录了每个缓存行的状态以及哪些核心拥有该缓存行的副本。当一个核心需要对某个内存地址进行独占访问时,它会查询目录表,确定哪些核心拥有该缓存行的副本,并直接向这些核心发送失效请求。
- 优点:可以支持更多的核心,减少了总线带宽的压力。
- 缺点:目录表的维护和查询增加了额外的开销。
3.3 基于消息传递的协议(Message-Passing Protocols)
在一些高性能计算系统中,缓存一致性是通过点对点的消息传递 实现的。每个核心都可以直接与其他核心通信,发送和接收缓存行的状态信息。这种方式可以减少全局通信的开销,适用于大规模并行系统。
- 优点:适合大规模并行系统,减少了全局通信的瓶颈。
- 缺点:实现复杂,硬件设计难度较大。
4. 缓存行失效的影响
缓存行失效虽然确保了数据的一致性,但也带来了一些性能上的开销:
-
缓存行乒乓效应(Cache Line Ping-Pong):如果多个核心频繁竞争同一个缓存行,导致缓存行在不同核心之间频繁传递,这会增加缓存失效的次数,降低性能。
-
内存延迟:当缓存行被标记为无效后,核心需要从主内存或其他核心重新加载该缓存行,这会导致额外的内存访问延迟。
为了减轻这些影响,现代处理器采用了多种优化技术,如存储顺序缓冲区(Store Buffer) 和 乱序执行(Out-of-Order Execution),以减少缓存行失效的频率和延迟。
5. 总结
-
当一个核心需要对某个内存地址进行独占访问时,它会通过缓存一致性协议(如 MESI)发出失效请求,要求其他核心将该内存地址对应的缓存行标记为无效(Invalid)。这个过程确保了只有当前核心可以对该内存地址进行写入或原子操作,避免了数据不一致的问题。
-
缓存行失效的实现依赖于系统的互连网络和缓存一致性协议,具体实现方式包括基于总线的协议、基于目录的协议和基于消息传递的协议。