CAS指令与MESI缓存一致性协议、 “轻量级锁” 与原子操作

简介: CAS指令与MESI缓存一致性协议、 “轻量级锁” 与原子操作“最轻量级的锁”,通常也叫”原子操作”,之所以加引号是因为他们在汇编级别并不是原子操作,是用多条指令完成的,这些操作大多都是利用CPU支持的汇编指令.CAS(Compare-And-Swap)指令是并行程序设计最基础的基石。

CAS指令与MESI缓存一致性协议、 “轻量级锁” 与原子操作

“最轻量级的锁”,通常也叫”原子操作”,之所以加引号是因为他们在汇编级别并不是原子操作,是用多条指令完成的,这些操作大多都是利用CPU支持的汇编指令.

CAS(Compare-And-Swap)指令是并行程序设计最基础的基石。

CAS指令,在Intel CPU上称为CMPXCHG。最常见的原子操作有Compare and Exchange,Self Increase/Decrease等等

80486 CPU相关指令:

LOCK:这是一个指令前缀,在所对应的指令操作期间使此指令的目标操作数指定的存储区域锁定,以得到保护。

XADD:先交换两个操作数的值,再进行算术加法操作。多处理器安全,在80486及以上CPU中支持。

CMPXCHG:比较交换指令,第一操作数先和AL/AX/EAX比较,如果相等ZF置1,第二操作数赋给第一操作数,否则ZF清0,第一操作数赋给AL/AX/EAX。多处理器安全,在80486及以上CPU中支持。

XCHG:交换两个操作数,其中至少有一个是寄存器寻址.其他寄存器和标志位不受影响.

80486以上都支持这四个操作,因此当今几乎100%CPU都支持这两个指令

这一系列操作是原子的,不可能被中断。基本上所有的同步机制,与信号量、Java中的synchronized等的实现最终都要用到CAS指令,即使锁无关的数据结构也离不开CAS指令。

CMPXCHG指令详解

cmpxchg是汇编指令  

作用:比较并交换操作数

这条指令将al\ax\eax\rax中的值与首操作数比较:

1.如果相等,第2操作数的直装载到首操作数,zf置1。(相当于相减为0,所以0标志位置位)

2.如果不等, 首操作数的值装载到al\ax\eax\rax,并将zf清0

例如:

CMPXCHG CX,DX


首操作数: CX
第2操作数:DX

(1) 如果指令执行前:

(AX) = 2300H
(CX) = 2300H
(DX) = 2400H

则指令执行后, 因(CX)= (AX), 故
第2操作数(DX)直装载到首操作数(CX),ZF置1。
(CX)=2400H,ZF=1

(2) 如果指令执行前:

(AX) = 2500H
(CX) = 2300H
(DX) = 2400H

则指令执行后因首操作数(CX)不等于(AX), 即(CX)!=(AX) 。
寄存器( al\ax\eax\rax )中的值与首操作数(CX)不等, 那么首操作数的值 (CX)直接装载到al\ax\eax\rax中,即(AX)= (CX 的值2300H),并将zf清0。

最终得到:

(AX)=2300H,ZF=0

CMPXCHG隐含使用EAX寄存器。象这种隐含使用其他寄存器的指令还有不少。对于哪种操作影响标志位也需要慢慢熟悉。

Compares the value in the AL, AX, or EAX register (depending on the size of the operand) with the first operand (destination operand). If the two values are equal, the second operand (source operand) is loaded into the destination operand. Otherwise, the destination operand is loaded into the AL, AX, or EAX register.
This instruction can be used with a LOCK prefix to allow the instruction to be executed atomi-cally. To simplify the interface to the processor's bus, the destination operand receives a write cycle without regard to the result of the comparison. The destination operand is written back if the comparison fails; otherwise, the source operand is written into the destination. (The processor never produces a locked read without also producing a locked write.)
参考: Intel文档 http://faydoc.tripod.com/cpu/cmpxchg.htm

MESI 缓存一致性协议

关于CAS指令最著名的传闻是CAS需要锁总线,因此CAS指令不但慢而且会严重影响系统并发度,即使没有冲突是也一样。不过在较新的CPU中(对于Intel CPU来说是486之后),事实并非如此。目前的CPU一般都采用了很好的缓存一致性协议,在很多情况下能够防止锁总线的发生,这其中最著名的就是Intel CPU中使用的MESI缓存一致性协议。

先来说说缓存一致性问题。为了提高数据访问效率,每个CPU上都有一个容量很小(现在一般是1M这个数量级),速度很快的缓存,用于缓存最常访问的那些数据。由于操作内存的速度实在太慢,数据被修改时也只更新缓存,并不直接写出到内存中去,这一来就造成了缓存中的数据与内存不一致。如果系统中只有一个CPU,所有线程看到的都是缓存中的最新数据,当然没问题。但如果系统中有多个CPU,同一份内存可能会被缓存到多个CPU中,如果在不同CPU中运行的不同线程看到同一份内存的缓存值不一样就麻烦了,因此有必要维护这多种缓存的一致性。当然要做到这一点只要一有修改操作,就通知所有CPU更新缓存,或者放弃缓存下次访问的时候再重新从内存中读取。但这 Stupid 的实现显然不会有好的性能,为解决这一问题,产生了很多维护缓存一致性的协议,MESI就是其中一种。

MESI协议的名称由来是指这一协议为缓存的每个数据单位(称为cache line,在Intel CPU上一般是64字节)维护两个状态位,使得每个数据单位可能处于M、E、S或I这四种状态之一。各种状态含义如下:

  • M: 被修改的。处于这一状态的数据只在本CPU中有缓存,且其数据已被修改,没有更新到内存中

  • E: 独占的。处于这一状态的数据只在本CPU中有缓存,且其数据没有被修改,与内存一致

  • S: 共享的。处于这一状态的数据在多个CPU中有缓存

  • I: 无效的。本CPU中的这份缓存已经无效了。

当CPU要读取数据时,只要缓存的状态不是I都可以从缓存中读,否则就要从主存中读。这一读操作可能会被某个处于M或E状态的CPU截获,该CPU将修改的数据写出到内存,并将自己设为S状态后这一读操作才继续进行。只有缓存状态是E或M时,CPU才可以修改其中的数据,修改后缓存即处于M状态。如果CPU要修改数据时发现其缓存不处于E或M状态,则需要发出特殊的RFO指令(Read For Ownership),将其它CPU的缓存设为I状态。

因此,如果一个变量在某段时间内只被一个线程频繁修改,则对应的缓存早就处于M状态,这时CAS操作就不会涉及到总线操作。所以频繁的加锁并不一定会影响系统并发度,关键是看锁冲突的情况严重不严重,如果经常出现冲突,即缓存一会被这个CPU独占,一会被那个CPU独占,这时才会不断产生RFO,影响到并发性能。

compareAndSwapInt 源码实现分析

在Java中的JamVM natives.c 的源码如下:

// compareAndSwapInt(this, offset, var5, var5 + 1)

//JamVM natives.c

uintptr_t *compareAndSwapInt(Class *class, MethodBlock *mb, uintptr_t *ostack) {

    long long offset = *((long long *)&ostack[2]);

    unsigned int *addr = (unsigned int*)((char *)ostack[1] + offset);

    unsigned int expect = ostack[4];

    unsigned int update = ostack[5];

    int result;

    //调用平台CPU特定的汇编代码实现

    result = COMPARE_AND_SWAP_32(addr, expect, update);

    *ostack++ = result;

    return ostack;

}

x86-64 的汇编实现宏

#define COMPARE_AND_SWAP_32(addr, old_val, new_val)        \

({                                                         \

    char result;                                           \

    __asm__ __volatile__ ("                                \

        lock;                                              \

        cmpxchgl %4, %1;                                   \

        sete %0"                                           \

    : "=q" (result), "=m" (*addr)/*out*/                   \

    : "m" (*addr), "a" (old_val), "r" (new_val) /*in*/     \

    : "memory");     /*Clobbers, reload from memory*/      \

    result;                                                \

})

COMPARE_AND_SWAP_32的伪代码如下

//下文的 <- 指赋值的意思,Intel文档也是这样写的

--INPUT:

rcx <- memory <- addr

al <- old_val

esi <- new_val

--OUTPUT:

rax <- result

rcx <-memory <- addr

--PROCEDURE:

//cmpxchg dword [ds:rcx], esi

IF al == rcx

THEN

  ZF <- 1;

  rcx <- esi;

ELSE

  ZF <- 0;

  al <- rcx;

FI;

//sete       al

IF ZF == 1

THEN

  al <- 0;

ELSE

  al <- 1;

FI;

//movsx      rax, al

rax <- al

return rax

用C语言简化是这样的

if(old_val == *addr){ // 这里的相等,判断的是什么? EAX 寄存器中的值与当前操作数的值比较?

  *addr = new_val;  // 把新的值赋给 *addr

  return true;

} else{

  old_val = *addr;

  return false;

}
相关文章
|
3月前
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
4月前
|
消息中间件 缓存 监控
如何保证缓存和数据库的一致性?
保证缓存和数据库的一致性的做法
|
1月前
|
缓存 NoSQL 关系型数据库
mysql和缓存一致性问题
本文介绍了五种常见的MySQL与Redis数据同步方法:1. 双写一致性,2. 延迟双删策略,3. 订阅发布模式(使用消息队列),4. 基于事件的缓存更新,5. 缓存预热。每种方法的实现步骤、优缺点均有详细说明。
|
2月前
|
缓存 监控 算法
小米面试题:多级缓存一致性问题怎么解决
【10月更文挑战第23天】在现代分布式系统中,多级缓存架构因其能够显著提高系统性能和响应速度而被广泛应用。
65 3
|
2月前
|
SQL 缓存 Java
JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)
这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。
36 3
|
2月前
|
消息中间件 缓存 中间件
缓存一致性问题,这么回答肯定没毛病!
缓存一致性问题,这么回答肯定没毛病!
|
4月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
这篇文章介绍了如何在SpringBoot项目中整合Redis,并探讨了缓存穿透、缓存雪崩和缓存击穿的问题以及解决方法。文章还提供了解决缓存击穿问题的加锁示例代码,包括存在问题和问题解决后的版本,并指出了本地锁在分布式情况下的局限性,引出了分布式锁的概念。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
|
3月前
|
消息中间件 缓存 NoSQL
奇怪的缓存一致性问题
本文记录了缓存一致性问题的排查过程和解决方案,同时带读者朋友们一起回顾下相关的八股文。
|
3月前
|
缓存 NoSQL 关系型数据库
MySQL与Redis缓存一致性的实现与挑战
在现代软件开发中,MySQL作为关系型数据库管理系统,广泛应用于数据存储;而Redis则以其高性能的内存数据结构存储特性,常被用作缓存层来提升数据访问速度。然而,当MySQL与Redis结合使用时,确保两者之间的数据一致性成为了一个重要且复杂的挑战。本文将从技术角度分享MySQL与Redis缓存一致性的实现方法及其面临的挑战。
161 2
|
4月前
|
消息中间件 缓存 监控
go-zero微服务实战系列(六、缓存一致性保证)
go-zero微服务实战系列(六、缓存一致性保证)