什么是硬件同步原语?
为什么硬件同步原语能够代替锁呢?要了解这个问题,你要首先晓得硬件同步原语是什么。
硬件同步原语(Atomic Hardware Primitives)是由计算机硬件提供的一组原子操作,咱们比拟罕用的原语次要是CAS和FAA这两种。
CAS(Compare and Swap),它的字面意思是:先比拟,再替换。咱们看一下CAS实现的伪代码:
<< atomic >>function cas(p : pointer to int, old : int, new : int) returns bool { if *p ≠ old { return false } *p ← new return true}
它的输出参数一共有三个,别离是:
- p: 要批改的变量的指针。
- old: 旧值。
- new: 新值。
返回的是一个布尔值,标识是否赋值胜利。
通过这个伪代码,你就能够看出CAS原语的逻辑,非常简单,就是先比拟一下变量p以后的值是不是等于old,如果等于,那就把变量p赋值为new,并返回true,否则就不扭转变量p,并返回false。
这是CAS这个原语的语义,接下来咱们看一下FAA原语(Fetch and Add):
<< atomic >>function faa(p : pointer to int, inc : int) returns int { int value <- *location *p <- value + inc return value}
FAA原语的语义是,先获取变量p以后的值value,而后给变量p减少inc,最初返回变量p之前的值value。
讲到这儿预计你会问,这两个原语到底有什么非凡的呢?
下面的这两段伪代码,如果咱们用编程语言来实现,必定是无奈保障原子性的。而原语的非凡之处就是,它们都是由计算机硬件,具体说就是CPU提供的实现,能够保障操作的原子性。
咱们晓得, 原子操作具备不可分割性,也就不存在并发的问题。所以在某些状况下,原语能够用来代替锁,实现一些即平安又高效的并发操作。
CAS和FAA在各种编程语言中,都有相应的实现,能够来间接应用,无论你是应用哪种编程语言,它们底层的实现是一样的,成果也是一样的。
接下来,还是拿咱们相熟的账户服务来举例说明一下,看看如何应用CAS原语来代替锁,实现同样的安全性。
CAS版本的账户服务
假如咱们有一个共享变量balance,它保留的是以后账户余额,而后咱们模仿多个线程并发转账的状况,看一下如何应用CAS原语来保证数据的安全性。
这次咱们应用Go语言来实现这个转账服务。先看一下应用锁实现的版本:
package mainimport ( "fmt" "sync")func main() { // 账户初始值为0元 var balance int32 balance = int32(0) done := make(chan bool) // 执行10000次转账,每次转入1元 count := 10000 var lock sync.Mutex for i := 0; i < count; i++ { // 这里模仿异步并发转账 go transfer(&balance, 1, done, &lock) } // 期待所有转账都实现 for i := 0; i < count; i++ { <-done } // 打印账户余额 fmt.Printf("balance = %d \n", balance)}// 转账服务func transfer(balance *int32, amount int, done chan bool, lock *sync.Mutex) { lock.Lock() *balance = *balance + int32(amount) lock.Unlock() done <- true}
这个例子中,咱们让账户的初始值为0,而后启动多个协程来并发执行10000次转账,每次往账户中转入1元,全副转账执行实现后,账户中的余额应该正好是10000元。
如果你没接触过Go语言,不理解协程也没关系,你能够简略地把它了解为过程或者线程都能够,这里咱们只是心愿能异步并发执行转账,咱们并不关怀这几种“程”他们之间轻微的差异。
这个应用锁的版本,重复屡次执行,每次balance的后果都正好是10000,那这段代码的安全性是没问题的。接下来咱们看一下,应用CAS原语的版本。
func transferCas(balance *int32, amount int, done chan bool) { for { old := atomic.LoadInt32(balance) new := old + int32(amount) if atomic.CompareAndSwapInt32(balance, old, new) { break } } done <- true}
这个CAS版本的转账服务和下面应用锁的版本,程序的总体构造是一样的,次要的区别就在于,“异步给账户余额+1”这一小块儿代码的实现。
那在应用锁的版本中,须要先获取锁,而后变更账户的值,最初开释锁,实现一次转账。咱们能够看一下应用CAS原语的实现:
首先,它用for来做了一个没有退出条件的循环。在这个循环的外部,重复地调用CAS原语,来尝试给账户的余额+1。先获得账户以后的余额,临时寄存在变量old中,再计算转账之后的余额,保留在变量new中,而后调用CAS原语来尝试给变量balance赋值。咱们刚刚讲过,CAS原语它的赋值操作是有前置条件的,只有变量balance的值等于old时,才会将balance赋值为new。
咱们在for循环中执行了3条语句,在并发的环境中执行,这外面会有两种可能状况:
一种状况是,执行到第3条CAS原语时,没有其余线程同时扭转了账户余额,那咱们是能够平安变更账户余额的,这个时候执行CAS的返回值肯定是true,转账胜利,就能够退出循环了。并且,CAS这一条语句,它是一个原子操作,赋值的安全性是能够保障的。
另外一种状况,那就是在这个过程中,有其余线程扭转了账户余额,这个时候是无奈保障数据安全的,不能再进行赋值。执行CAS原语时,因为无奈通过比拟的步骤,所以不会执行赋值操作。本次尝试转账失败,以后线程并没有对账户余额做任何变更。因为返回值为false,不会退出循环,所以会持续重试,直到转账胜利退出循环。
这样,每一次转账操作,都能够通过若干次重试,在保障安全性的前提下,实现并发转账操作。
其实,对于这个例子,还有更简略、性能更好的形式:那就是,间接应用FAA原语。
func transferFaa(balance *int32, amount int, done chan bool) { atomic.AddInt32(balance, int32(amount)) done <- true}
FAA原语它的操作是,获取变量以后的值,而后把它做一个加法,并且保障这个操作的原子性,一行代码就能够搞定了。看到这儿,你可能会想,那CAS原语还有什么意义呢?
在这个例子外面,必定是应用FAA原语更适合,然而咱们下面介绍的,应用CAS原语的办法,它的适用范围更加宽泛一些。相似于这样的逻辑:先读取数据,做计算,而后更新数据,无论这个计算是什么样的,都能够应用CAS原语来爱护数据安全,然而FAA原语,这个计算的逻辑只能局限于简略的加减法。所以,咱们下面讲的这种应用CAS原语的办法并不是没有意义的。
另外,你须要晓得的是,这种应用CAS原语重复重试赋值的办法,它是比拟消耗CPU资源的,因为在for循环中,如果赋值不胜利,是会立刻进入下一次循环没有期待的。如果线程之间的碰撞十分频繁,经常性的重复重试,这个重试的线程会占用大量的CPU工夫,随之零碎的整体性能就会降落。
缓解这个问题的一个办法是应用Yield(), 大部分编程语言都反对Yield()这个零碎调用,Yield()的作用是,通知操作系统,让出以后线程占用的CPU给其余线程应用。每次循环完结前调用一下Yield()办法,能够在肯定水平上缩小CPU的使用率,缓解这个问题。你也能够在每次循环完结之后,Sleep()一小段时间,然而这样做的代价是,性能会重大降落。
所以,这种办法它只适宜于线程之间碰撞不太频繁,也就是说绝大部分状况下,执行CAS原语不须要重试这样的场景。
总结
本文讲述了CAS和FAA这两个原语。这些原语,是由CPU提供的原子操作,在并发环境中,独自应用这些原语不必放心数据安全问题。在特定的场景中,CAS原语能够代替锁,在保障安全性的同时,提供比锁更好的性能。
本文由mdnice多平台公布