golang 原子操做

原文: http://ifeve.com/go-concurrency-atomic/golang

1. 什麼是原子操做安全

  咱們已經知道,原子操做便是進行過程當中不能被中斷的操做。也就是說,針對某個值的原子操做在被進行的過程中,CPU毫不會再去進行其它的針對該值的操做。不管這些其它的操做是否爲原子操做都會是這樣。爲了實現這樣的嚴謹性,原子操做僅會由一個獨立的CPU指令表明和完成。只有這樣纔可以在併發環境下保證原子操做的絕對安全。
Go語言提供的原子操做都是非侵入式的。它們由標準庫代碼包sync/atomic中的衆多函數表明。咱們能夠經過調用這些函數對幾種簡單的類型的值進行原子操做。架構

2.goalng 中的原子操做類型併發

  int3二、int6四、uint3二、uint6四、uintptr和unsafe.Pointer類型,共6個app

3.golang 中有哪些原子操做函數

  有5種,即:增或減、比較並交換、載入、存儲和交換。性能

4.詳解ui

   1. 增或減
被用於進行增或減的原子操做(如下簡稱原子增/減操做)的函數名稱都以「Add」爲前綴,並後跟針對的具體類型的名稱。例如,實現針對uint32類型的原子增/減操做的函數的名稱爲AddUint32。事實上,sync/atomic包中的全部函數的命名都遵循此規則。atom

  2. 比較並交換
有些讀者可能很熟悉比較並交換操做的英文稱謂——Compare And Swap,簡稱CAS。在sync/atomic包中,這類原子操做由名稱以「CompareAndSwap」爲前綴的若干個函數表明。
咱們依然以針對int32類型值的函數爲例。該函數名爲CompareAndSwapInt32。其聲明以下:指針

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

能夠看到,CompareAndSwapInt32函數接受三個參數。第一個參數的值應該是指向被操做值的指針值。該值的類型即爲*int32。後兩個參數的類型都是int32類型。它們的值應該分別表明被操做值的舊值和新值。CompareAndSwapInt32函數在被調用以後會先判斷參數addr指向的被操做值與參數old的值是否相等。僅當此判斷獲得確定的結果以後,該函數纔會用參數new表明的新值替換掉原先的舊值。不然,後面的替換操做就會被忽略。這正是「比較並交換」這個短語的由來。CompareAndSwapInt32函數的結果swapped被用來表示是否進行了值的替換操做。
與咱們前面講到的鎖相比,CAS操做有明顯的不一樣。它老是假設被操做值不曾被改變(即與舊值相等),並一旦確認這個假設的真實性就當即進行值替換。而使用鎖則是更加謹慎的作法。咱們老是先假設會有併發的操做要修改被操做值,並使用鎖將相關操做放入臨界區中加以保護。咱們能夠說,使用鎖的作法趨於悲觀,而CAS操做的作法則更加樂觀。
CAS操做的優點是,能夠在不造成臨界區和建立互斥量的狀況下完成併發安全的值替換操做。這能夠大大的減小同步對程序性能的損耗。固然,CAS操做也有劣勢。在被操做值被頻繁變動的狀況下,CAS操做並不那麼容易成功。有些時候,咱們可能不得不利用for循環以進行屢次嘗試。示例以下:

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

  能夠看到,爲了保證CAS操做的成功完成,咱們僅在CompareAndSwapInt32函數的結果值爲true時纔會退出循環。這種作法與自旋鎖的自旋行爲類似。addValue函數會不斷的嘗試原子的更新value的值,直到這一操做成功爲止。操做失敗的原因總會是value的舊值已不與v的值相等了。若是value的值會被併發的修改的話,那麼發生這種狀況是很正常的。
CAS操做雖然不會讓某個Goroutine阻塞在某條語句上,可是仍可能會使流程的執行暫時停滯。不過,這種停滯的時間大都極其短暫。
請記住,當想併發安全的更新一些類型(更具體的講是,前文所述的那6個類型)的值的時候,咱們老是應該優先選擇CAS操做。
與此對應,被用來進行原子的CAS操做的函數共有6個。除了咱們已經講過的CompareAndSwapInt32函數以外,還有CompareAndSwapInt6四、CompareAndSwapPointer、CompareAndSwapUint3二、CompareAndSwapUint64 和CompareAndSwapUintptr函數。這些函數的結果聲明列表與CompareAndSwapInt32函數的徹底一致。而它們的參數聲明列表與後者也很是相似。雖然其中的那三個參數的類型不一樣,但其遵循的規則是一致的,即:第二個和第三個參數的類型均爲與第一個參數的類型(即某個指針類型)緊密相關的那個類型。例如,若是第一個參數的類型爲*unsafe.Pointer,那麼後兩個參數的類型就必定是unsafe.Pointer。這也是由這三個參數的含義決定的。

  

3. 載入
在前面示例的for循環中,咱們使用語句v := value爲變量v賦值。可是,要注意,其中的讀取value的值的操做並非併發安全的。在該讀取操做被進行的過程當中,其它的對此值的讀寫操做是能夠被同時進行的。它們並不會受到任何限制。
在第7章的第1節的最後,咱們舉過這樣一個例子:在32位計算架構的計算機上寫入一個64位的整數。若是在這個寫操做未完成的時候有一個讀操做被併發的進行了,那麼這個讀操做極可能會讀取到一個只被修改了一半的數據。這種結果是至關糟糕的。
爲了原子的讀取某個值,sync/atomic代碼包一樣爲咱們提供了一系列的函數。這些函數的名稱都以「Load」爲前綴,意爲載入。咱們依然以針對int32類型值的那個函數爲例。
咱們下面利用LoadInt32函數對上一個示例稍做修改:制。

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

  函數atomic.LoadInt32接受一個*int32類型的指針值,並會返回該指針值指向的那個值。在該示例中,咱們使用調用表達式atomic.LoadInt32(&value)替換掉了標識符value。替換後,那條賦值語句的含義就變爲:原子的讀取變量value的值並把它賦給變量v。有了「原子的」這個形容詞就意味着,在這裏讀取value的值的同時,當前計算機中的任何CPU都不會進行其它的針對此值的讀或寫操做。這樣的約束是受到底層硬件的支持的。
注意,雖然咱們在這裏使用atomic.LoadInt32函數原子的載入value的值,可是其後面的CAS操做仍然是有必要的。由於,那條賦值語句和if語句並不會被原子的執行。在它們被執行期間,CPU仍然可能進行其它的針對value的值的讀或寫操做。也就是說,value的值仍然有可能被併發的改變。
與atomic.LoadInt32函數的功能相似的函數有atomic.LoadInt6四、atomic.LoadPointer、atomic.LoadUint3二、atomic.LoadUint64和atomic.LoadUintptr。

  4. 存儲
與讀取操做相對應的是寫入操做。而sync/atomic包也提供了與原子的值載入函數相對應的原子的值存儲函數。這些函數的名稱均以「Store」爲前綴。

 

  5. 交換在sync/atomic代碼包中還存在着一類函數。它們的功能與前文所講的CAS操做和原子載入操做都有些相似。這樣的功能能夠被稱爲原子交換操做。這類函數的名稱都以「Swap」爲前綴。

相關文章
相關標籤/搜索