Go語言原子操作及互斥鎖的區別

原子操作就是不可中斷的操作,外界是看不到原子操作的中間狀態,要麼看到原子操作已經完成,要麼看到原子操作已經結束。在某個值的原子操作執行的過程中,CPU絕對不會再去執行其他針對該值的操作,那麼其他操作也是原子操作。

Go語言中提供的原子操作都是非侵入式的,在標準庫代碼包sync/atomic中提供瞭相關的原子函數。

增或減

用於增或減的原子操作的函數名稱都是以”Add”開頭的,後面跟具體的類型名,比如下面這個示例就是int64類型的原子減操作

func main() {
   var  counter int64 =  23
   atomic.AddInt64(&counter,-3)
   fmt.Println(counter)
}
---output---
20

原子函數的第一個參數都是指向變量類型的指針,是因為原子操作需要知道該變量在內存中的存放位置,然後加以特殊的CPU指令,也就是說對於不能取得內存存放地址的變量是無法進行原子操作的。第二個參數的類型會自動轉換為與第一個參數相同的類型。此外,原子操作會自動將操作後的值賦值給變量,無需我們自己手動賦值瞭。

對於 atomic.AddUint32() 和 atomic.AddUint64() 的第二個參數為 uint32 與 uint64,因此無法直接傳遞一個負的數值進行減法操作,Go語言提供瞭另一種方法來迂回實現:使用二進制補碼的特性

註意:unsafe.Pointer 類型的值無法被加減。

比較並交換(Compare And Swap)

簡稱CAS,在標準庫代碼包sync/atomic中以”Compare And Swap“為前綴的若幹函數就是CAS操作函數,比如下面這個

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

第一個參數的值是這個變量的指針,第二個參數是這個變量的舊值,第三個參數指的是這個變量的新值。

運行過程:調用CompareAndSwapInt32 後,會先判斷這個指針上的值是否跟舊值相等,若相等,就用新值覆蓋掉這個值,若相等,那麼後面的操作就會被忽略掉。返回一個 swapped 佈爾值,表示是否已經進行瞭值替換操作。

與鎖有不同之處:鎖總是假設會有並發操作修改被操作的值,而CAS總是假設值沒有被修改,因此CAS比起鎖要更低的性能損耗,鎖被稱為悲觀鎖,而CAS被稱為樂觀鎖。

CAS的使用示例

var value int32
func AddValue(delta int32)  {
   for {
      v:= value
      if atomic.CompareAndSwapInt32(&value,v,(v+delta)) {
         break
      }
   }
}

由示例可以看出,我們需要多次使用for循環來判斷該值是否已被更改,為瞭保證CAS操作成功,僅在 CompareAndSwapInt32 返回為 true時才退出循環,這跟自旋鎖的自旋行為相似。

載入與存儲

對一個值進行讀或寫時,並不代表這個值是最新的值,也有可能是在在讀或寫的過程中進行瞭並發的寫操作導致原值改變。為瞭解決這問題,Go語言的標準庫代碼包sync/atomic提供瞭原子的讀取(Load為前綴的函數)或寫入(Store為前綴的函數)某個值

將上面的示例改為原子讀取

var value int32
func AddValue(delta int32)  {
   for {
      v:= atomic.LoadInt32(&value)
      if atomic.CompareAndSwapInt32(&value,v,(v+delta)) {
         break
      }
   }
}

原子寫入總會成功,因為它不需要關心原值是什麼,而CAS中必須關註舊值,因此原子寫入並不能代替CAS,原子寫入包含兩個參數,以下面的StroeInt32為例:

//第一個參數是被操作值的指針,第二個是被操作值的新值
func StoreInt32(addr *int32, val int32) 

交換

這類操作都以”Swap“開頭的函數,稱為”原子交換操作“,功能與之前說的CAS操作與原子寫入操作有相似之處。

func SwapInt32(addr *int32, new int32) (old int32)

以 SwapInt32 為例,第一個參數是int32類型的指針,第二個是新值。原子交換操作不需要關心原值,而是直接設置新值,但是會返回被操作值的舊值。

原子值

Go語言的標準庫代碼包sync/atomic中有一個叫做Value的原子值,它是一個結構體類型,用於存儲需要原子讀寫的值,結構體如下

// Value提供原子加載並存儲一致類型的值。
// Value的零值從Load返回nil。
//調用Store後,不得復制值。
//首次使用後不得復制值。
type Value struct {
   v interface{}
}

可以看出結構體內是一個 v interface{},也就是說 該Value原子值可以保存任何類型的需要原子讀寫的值。

使用方式如下:

var Atomicvalue  atomic.Value

該類型有兩個公開的指針方法

//原子的讀取原子值實例中存儲的值,返回一個 interface{} 類型的值,且不接受任何參數。
//若未曾通過store方法存儲值之前,會返回nil
func (v *Value) Load() (x interface{})

//原子的在原子實例中存儲一個值,接收一個 interface{} 類型(不能為nil)的參數,且不會返回任何值
func (v *Value) Store(x interface{})

一旦原子值實例存儲瞭某個類型的值,那麼之後Store存儲的值就必須是與該類型一致,否則就會引發panic。

嚴格來講,atomic.Value類型的變量一旦被聲明,就不應該被復制到其他地方。比如:作為源值賦值給其他變量,作為參數傳遞給函數,作為結果值從函數返回,作為元素值通過通道傳遞,這些都會造成值的復制。

但是atomic.Value類型的指針類型變量就不會存在這個問題,原因是對結構體的復制不但會生成該值的副本,還會生成其中字段的副本,這樣那麼並發引發的值變化都與原值沒關系瞭。

看下面這個小示例

func main() {
   var Atomicvalue  atomic.Value
   Atomicvalue.Store([]int{1,2,3,4,5})
   anotherStore(Atomicvalue)
   fmt.Println("main: ",Atomicvalue)
}

func anotherStore(Atomicvalue atomic.Value)  {
   Atomicvalue.Store([]int{6,7,8,9,10})
   fmt.Println("anotherStore: ",Atomicvalue)
}
---output---
anotherStore:  {[6 7 8 9 10]}
main:  {[1 2 3 4 5]}

原子操作與互斥鎖的區別

互斥鎖是一種數據結構,使你可以執行一系列互斥操作。而原子操作是互斥的單個操作,這意味著沒有其他線程可以打斷它。那麼就Go語言裡atomic包裡的原子操作和sync包提供的同步鎖有什麼不同呢?

首先atomic操作的優勢是更輕量,比如CAS可以在不形成臨界區和創建互斥量的情況下完成並發安全的值替換操作。這可以大大的減少同步對程序性能的損耗。

原子操作也有劣勢。還是以CAS操作為例,使用CAS操作的做法趨於樂觀,總是假設被操作值未曾被改變(即與舊值相等),並一旦確認這個假設的真實性就立即進行值替換,那麼在被操作值被頻繁變更的情況下,CAS操作並不那麼容易成功。而使用互斥鎖的做法則趨於悲觀,我們總假設會有並發的操作要修改被操作的值,並使用鎖將相關操作放入臨界區中加以保護。

所以總結下來原子操作與互斥鎖的區別有:

互斥鎖是一種數據結構,用來讓一個線程執行程序的關鍵部分,完成互斥的多個操作。
原子操作是針對某個值的單個互斥操作。
可以把互斥鎖理解為悲觀鎖,共享資源每次隻給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程。
atomic包提供瞭底層的原子性內存原語,這對於同步算法的實現很有用。這些函數一定要非常小心地使用,使用不當反而會增加系統資源的開銷,對於應用層來說,最好使用通道或sync包中提供的功能來完成同步操作。

針對atomic包的觀點在Google的郵件組裡也有很多討論,其中一個結論解釋是:

應避免使用該包裝。或者,閱讀C ++ 11標準的“原子操作”一章;如果您瞭解如何在C ++中安全地使用這些操作,那麼你才能有安全地使用Go的 sync/atomic包的能力。

到此這篇關於Go語言原子操作及互斥鎖的區別 的文章就介紹到這瞭,更多相關Go語言原子操作內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: