golang 原子操作


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

1. 什么是原子操作

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

2.goalng 中的原子操作類型

  int32、int64、uint32、uint64、uintptr和unsafe.Pointer類型,共6個

3.golang 中有哪些原子操作

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

4.詳解

   1. 增或減
被用於進行增或減的原子操作(以下簡稱原子增/減操作)的函數名稱都以“Add”為前綴,並后跟針對的具體類型的名稱。例如,實現針對uint32類型的原子增/減操作的函數的名稱為AddUint32。事實上,sync/atomic包中的所有函數的命名都遵循此規則。

  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函數之外,還有CompareAndSwapInt64、CompareAndSwapPointer、CompareAndSwapUint32、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.LoadInt64、atomic.LoadPointer、atomic.LoadUint32、atomic.LoadUint64和atomic.LoadUintptr。

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

 

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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM