CAS无锁算法

要实现无锁(lock-free)的非阻塞算法有多种实现方法,其中CAS(比较与交换,Compare and swap)是一种有名的无锁算法。CAS是CPU指令,在大多数处理器架构,包括 IA32、Space中采用的都是CAS指令,CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS在golang的实现

for {if atomic.CompareAndSwapInt64(&data, old, new) {return new}
}

CompareAndSwap会先进行比较,如果data的值等于old,那么就会执行替换操作并返回true,如果不等于,则说明已经被其他线程操作了就返回false,所以它并不一定总能成功,尤其是在并发大的情况下,所以使用for循环来自旋。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。

通常使用的i++操作,其过程是先从内存读取出i,然后加1,然后赋值给内存,很显然这个过程并不是原子的,我们可以使用CAS来实现原子化自增操作。

func AddInt64(addr *int64, inc int64) int64 {for {old := *addrif atomic.CompareAndSwapInt64(addr, old, old+inc) {return old}}
}

所以,基于CAS,我们可以对一个内存地址上的值先比较再赋值,保证了原子性。

atomic是由汇编语言实现的,在golang中

src/sync/atomic/asm.s
--> src/runtime/internal/atomic/stubs.go
--> src/runtime/internal/atomic/atomic_amd64.s

找到了文件之后,发现它们的名字并不相同,于是要接着查找。
src/cmd/compile/internal/ssagen/ssa.go 在里面可以找到对于的映射关系,比如
alias("sync/atomic", "CompareAndSwapInt32", "runtime/internal/atomic", "Cas", all...)

CAS指令

知道了CAS是个什么意思之后,接下来就要去探究CAS的实现原理,以及CAS的比较+赋值过程是如何做到原子性的。

CAS自身的原子性是由CPU指令来实现的,我没有找到具体代码,下面是网上的代码。

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \__asm je L0      \__asm _emit 0xF0 \__asm L0:inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {// alternative for InterlockedCompareExchangeint mp = os::is_MP();__asm {mov edx, destmov ecx, exchange_valuemov eax, compare_valueLOCK_IF_MP(mp)cmpxchg dword ptr [edx], ecx}
}

程序会根据当前处理器的类型来决定是否为 cmpxchg指令 添加 lock前缀。如果程序是在多处理器上运行,就为 cmpxchg指令 加上 lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略 lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

我们知道,应用程序的一行代码,即使是一个简单的加1操作,会转化成很多的CPU指令,而实际上需要被锁住的是极少的指令,大部分指令都是可以并行执行的,所有应用程序层面上的加锁粒度较大。

intel的手册对lock前缀的说明如下

  1. 确保对内存的 读-改-写 操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
  2. 禁止该指令与之前和之后的读和写指令重排序。
  3. 把写缓冲区中的所有数据刷新到内存中。

关于CPU的锁有如下2种

1. 使用总线锁保证原子性

第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。

处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。

2. 使用缓存锁保证原子性

第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。

但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。

锁(lock)的代价

锁是用来做并发最简单的方式,当然其代价也是最高的。内核态的锁的时候需要操作系统进行一次上下文切换,加锁、释放锁会导致比较多的上下文切换和调度延时,等待锁的线程会被挂起直至锁释放。在上下文切换的时候,cpu之前缓存的指令和数据都将失效,对性能有很大的损失。操作系统对多线程的锁进行判断就像两姐妹在为一个玩具在争吵,然后操作系统就是能决定他们谁能拿到玩具的父母,这是很慢的。用户态的锁虽然避免了这些问题,但是其实它们只是在没有真实的竞争时才有效。

CAS缺点

1、 CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。

ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。也就是说,此时变量中存储的值要分成两块,一块是版本号,一块是实际的值,比如说一个64位的整型,高32位是版本号,低32位是实际值,那么更新操作就需要用到位运算和逻辑运算了,而版本号是循环的,比如从0–>MAX–>0,这个MAX尽量设置的大一些,否则起不到效果。

循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。

2、比较花费CPU资源,即使没有任何争用也会做一些无用功。

3、会增加程序测试的复杂度,稍不注意就会出现问题。

应用

CAS操作的优势是,可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。
这可以大大的减少同步对程序性能的损耗。

当然,CAS操作也有劣势。在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。

1. 增或减
被用于进行增或减的原子操作(以下简称原子增/减操作)的函数名称都以“Add”为前缀,并后跟针对的具体类型的名称。
不过,由于atomic.AddUint32函数和atomic.AddUint64函数的第二个参数的类型分别是uint32uint64,所以我们无法通过传递一个负的数值来减小被操作值。atomic.AddUint32(&uint32, ^uint32(-NN-1)) 其中NN代表了一个负整数。

2. 比较并交换
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
第一个参数的值应该是指向被操作值的指针值。该值的类型即为*int32。后两个参数的类型都是int32类型。它们的值应该分别代表被操作值的旧值和新值。CompareAndSwapInt32函数在被调用之后会先判断参数addr指向的被操作值与参数old的值是否相等。仅当此判断得到肯定的结果之后,该函数才会用参数new代表的新值替换掉原先的旧值。否则,后面的替换操作就会被忽略。

3. 载入
v := atomic.LoadInt32(&value)
接受一个*int32类型的指针值,并会返回该指针值指向的那个值。 有了“原子的”这个形容词就意味着,在这里读取value的值的同时,当前计算机中的任何CPU都不会进行其它的针对此值的读或写操作。这样的约束是受到底层硬件的支持的。

4. 存储
在原子的存储某个值的过程中,任何CPU都不会进行针对同一个值的读或写操作。如果我们把所有针对此值的写操作都改为原子操作,那么就不会出现针对此值的读操作因被并发的进行而读到修改了一半的值的情况了。原子的值存储操作总会成功,因为它并不会关心被操作值的旧值是什么。函数atomic.StoreInt32会接受两个参数。第一个参数的类型是*int 32类型的,其含义同样是指向被操作值的指针。而第二个参数则是int32类型的,它的值应该代表欲存储的新值。其它的同类函数也会有类似的参数声明列表。

5. 交换
与CAS操作不同,原子交换操作不会关心被操作值的旧值。它会直接设置新值。但它又比原子载入操作多做了一步。作为交换,它会返回被操作值的旧值。此类操作比CAS操作的约束更少,同时又比原子载入操作的功能更强。以atomic.SwapInt32函数为例。它接受两个参数。第一个参数是代表了被操作值的内存地址的*int32类型值,而第二个参数则被用来表示新值。注意,该函数是有结果值的。该值即是被新值替换掉的旧值。atomic.SwapInt32函数被调用后,会把第二个参数值置于第一个参数值所表示的内存地址上(即修改被操作值),并将之前在该地址上的那个值作为结果返回。

用原子操作来替换mutex锁

其主要原因是,原子操作由底层硬件支持,而锁则由操作系统提供的API实现。若实现相同的功能,前者通常会更有效率。

golang中的atomic,以及CAS操作相关推荐

  1. Golang 中 sync/atomic 包的原子操作

    背景 Go中多协程的情况下, 要保证操作的原子性,一般要使用RWMutex或者Mutex, 但是锁使用起来比较复杂,还要考虑lock 和unlock 顺序和成对出现,不注意就容易出错. 于是在sync ...

  2. golang中slice切片的修改操作

    本文主要介绍go语言中切片slice的增删修改操作. 如果我们需要往切片中追加元素,可以使用内置的append()函数.这个函数接受一个需要被追加的切片,以及一个或者多个需要被追加的元素.如果我们需要 ...

  3. 1、Java中“并发编程”详解【voliate、synchronized、JMM内存模型、原子类操作Atomic..、CAS原理】

    文章目录 1.对volatile 的理解? JMM(Java 内存模型) JMM模型的三大特性: 禁止指令排序 线程安全性保证 你在哪些地方用到过 volatile?单例 2.CAS 你知道吗?CAS ...

  4. Handle table中CAS操作与A-B-A Problem解析

    在研究handle table的时候顺便研究的东西.Baidu了下,发现国内这方面的资料几乎没得,然后就准备瞎bb下,为下面的一篇介绍handle table的结构做准备. 关于lock-free d ...

  5. atomic java_在Java中添加@atomic操作

    atomic java 总览 原子操作如何在Java中工作,OpenJDK / Hotspot中是否存在可以转换为原子的当前替代方法. 反馈 在我以前的文章中, 对可变字段进行原子操作. 有几次指出, ...

  6. Java中CAS操作

    定义 CAS:Compare and Swap 其中在java中使用CAS底层借助 Unsafe 来操作,实现非阻塞的修改变量值,CAS是使用了硬件指令保证原子性 每次进行compareAndSwap ...

  7. java中的CAS操作以及锁机制详解

    关于CAS操作 CAS:Compare And Swap   --  乐观锁策略 CAS(无锁操作):使用CAS叫做比较交换来判断是否出现冲突,出现冲突就重试当前操作直到不冲突为止. 悲观锁(JDK1 ...

  8. Java中CAS操作详解

    在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁 锁机制存在以下问题: (1)在多线程竞争下,加锁.释放锁会导致比较多的上下文切换和调度延时,引起性能问题. (2 ...

  9. 关于JCU并发包中的Atomic原子类及其CAS

    希望造成叫醒你的是梦想 而不是闹钟 目录 类型 常用方法 CAS CAS与Atomic原子类 自旋锁 CAS缺点 Atomic原子类: 在化学中,原子是构成一般物质的最小单位,是不可分割的.而在这里, ...

最新文章

  1. mongodb的CRUD操作二查询
  2. python和c语言有什么关系-Python与C语言有什么区别?
  3. spingboot和mybatis,纯注解方式
  4. appcan php图片上传,appcan文件上传php,asp通用
  5. IO 多路复用之poll总结
  6. 关于MUI框架中,“侧滑导航“之“div模式下拉菜单“的a标签(超链接)的失效问题?
  7. Django(九)admin相关知识
  8. 服务器安装系统难吗,服务器安装系统难吗
  9. 域账户登录时提示“你的账户配置不允许使用这台电脑。请试一下其他电脑” 解决方案
  10. 高性能tornado框架简单实现restful接口及运维开发实例
  11. Delphi 重启应用程序
  12. php valid函数,PHP函数uasort()在类中的使用问题解决
  13. SQLyog v12.09 (64 bit) 注册码
  14. 对普通文件 霍夫曼编解码 matlab,JPEG编解码过程详解(二)
  15. Zema自学--3(实际操作)
  16. 打cf提示计算机内存不足怎么办,玩cf游戏提示内存不足的解决方法
  17. Laravel 邮件发送
  18. 标签体系及应用方法论,一文读懂
  19. 定期定量采购_定量、定期订货法的比较
  20. AcWing:Dijkstra

热门文章

  1. 在地图上创建热力图的方法
  2. SpringBoot+Vue部署ssl证书
  3. 如何利用SEO推介火狐狸浏览器
  4. 拿shell的一些方式
  5. dev和media和mnt的区别
  6. 带宽日益升级 华硕无线路由开启多维覆盖
  7. 如何解决视频聊天图片或声音问题
  8. 模块化-AMD模块化
  9. 推动商业运营 IDC云计算路演至上海金桥
  10. 图像处理注意力机制Attention汇总(附代码)