CAS:
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
func CAS(old interface{}, new interface{}, expect interface{}) {
if old == expect {
update old to new
}
return 更新失败
}
现在的CAS更多的是一种范称呼,是一种乐观锁机制,指的是一种算法思想,而且衍生了无数的变种及内部实现,但是原理都是类似的
算法流程:实现原理就是,更新之前先判断内存对象的值是否是获取的时候相同,如果相同,则认为没有更新过,更新,如果不同,则认为更新了,重新获取数据并计算
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
这个思路在软件层面(DB层面可以怎么实现)
CAS的核心需要保证比较和替换的原子性(保证并发安全)
(1)传统数据库
mongodb,mysql:利用一条记录的更新是原子性的
下面是利用Mongo实现的例子:
type DataBlock struct {
Id string `json:"id" bson:"id"`
Data interface{} `json:"data"`
Version string `json:"version" bson:"version"`
}
func (this *dataBlockRepository) Update(dBlock *DataBlock, oldVersion string) (code int, err error) {
id, _ := primitive.ObjectIDFromHex(dBlock.Id)
condition := bson.M{"_id": id, "version": oldVersion}
var data map[string]interface{}
err = utility.DecodeByJson(dBlock, &data)
updateContent := bson.M{"$set": data}
result, err := this.mongoClient.UpdateWithResult(collection, condition, updateContent)
if result.ModifiedCount <= 0 {
return response.RESPONSE_FOR_SYNCHRONOUS_UPDATE, nil
}
return response.SUCCESS, nil
}
其实:mongo本身也提供了一个原生的更方便实现CAS思想的原子命令:
db.collection.findAndModify({
query: <document>,
sort: <document>,
new: <boolean>,
fields: <document>,
upsert: <boolean>
})
new:选择返回更新前或者更新后的文档
(2)redis实现CAS
redis网络是单线程的,处理客户端命令,也就是网络IO和键值对读写是由一个线程来完成的。但是redis并没有提供原生的比较命令,
目前也没有类似CAS的原子性命令。
目前redis实现CAS的操作都是通过lua脚本(脚本语言)来实现
String CAS Lua Script:
KEYS[1] 对应要操作的String 类型的 redis 缓存的 key,ARGV[1]对应要比较的值,值相同则更新成 ARGV[2],并返回 1,否则返回 0
if redis.call(""get"", KEYS[1]) == ARGV[1] then
redis.call(""set"", KEYS[1], ARGV[2])
return 1
else
return 0
end
(3)go和java 内置CAS乐观锁的底层实现(通过硬件,也就是CPU来实现)
go:sync/atomic 内置了侠义的CAS
atomic.CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
底层汇编代码:
TEXT runtime∕internal∕atomic·Cas64(SB), NOSPLIT, $0-25
MOVQ ptr+0(FP), BX
MOVQ old+8(FP), AX
MOVQ new+16(FP), CX
LOCK
CMPXCHGQ CX, 0(BX)
SETEQ ret+24(FP)
RET
底层也是通过加锁来实现的,借用了CPU提供的原子性指令来实现。CAS操作修改共享变量时候不需要对共享变量加锁,而是通过类似乐观锁的方式进行检查,本质还是不断的占用CPU 资源换取加锁带来的开销(比如上下文切换开销)。当冲突概率低,该实现方式效率远远大于传统意义的锁(悲观锁)
怎么理解CAS能避免频繁的上下文切换,为啥高效:
并发编程+传统锁(阻塞):这种实现方式必然带来线程的频繁切换,线程切换是需要用户态到内核态的切换的,需要保留上一个线程的上下文(执行到那里等信息,方便下一次回来继续执行)
从硬件层面来实现原子操作,有两种方式:
1、总线加锁:因为CPU和其他硬件的通信都是通过总线控制的,所以可以通过在总线加LOCK#锁的方式实现原子操作,但这样会阻塞其他硬件对CPU的访问,开销比较大。
2、缓存锁定:频繁使用的内存会被处理器放进高速缓存中,那么原子操作就可以直接在处理器的高速缓存中进行而不需要使用总线锁,主要依靠缓存一致性来保证其原子性。
java:juc里面的CAS逻辑, 其中自旋那一部分也叫自旋锁,在某种程度上说,如果冲突比较多,那么自旋的for循环也会消耗一部分性能
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
// v是要返回的旧值
int v;
// do while自旋,乐观锁。直到CAS成功
do {
v = getIntVolatile(o, offset);
// CAS操作,一次没成功就继续尝试,直到set成功
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
下面的例子是在go中运用,也就是广义的CAS:开5个线程同时某个数做+1操作,并保证并发安全性
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var (
counter int32 //计数器
wg sync.WaitGroup //信号量
)
func main() {
threadNum := 5
//1. 五个信号量
wg.Add(threadNum)
//2.开启5个线程
for i := 0; i < threadNum; i++ {
go incCounter(i)
}
//3.等待子线程结束
wg.Wait()
fmt.Println(counter)
}
func incCounter(index int) {
defer wg.Done()
spinNum := 0
for {
//2.1原子操作
old := counter
ok := atomic.CompareAndSwapInt32(&counter, old, old+1)
if ok {
break
} else {
spinNum++
}
}
fmt.Printf("thread,%d,spinnum,%d\n",index,spinNum)
}