Go 原子操作sync.atomic


 

sync.atomic

atomic 提供的原子操作能夠確保任一時刻只有一個goroutine對幾種簡單的類型進行原子操作進行操作,善用atomic能夠避免程序中出現大量的鎖操作。
這些類型包括int32,int64,uint32,uint64,uintptr,unsafe.Pointer,共6個。
這些函數的原子操作共有5種:增或減,比較並交換、載入、存儲和交換它們提供了不同的功能。

atomic常見操作有:

  • 增減
  • 載入
  • 比較並交換
  • 交換
  • 存儲

 

增減

原子增或減即可實現對被操作值的增大或減少。因此該操作只能操作數值類型。
被用於進行增或減的原子操作都是以“Add”為前綴,並后面跟針對具體類型的名稱。
atomic 包中提供了如下以Add為前綴的增減操作:
- func AddInt32(addr *int32, delta int32) (new int32)
- func AddInt64(addr *int64, delta int64) (new int64)
- func AddUint32(addr *uint32, delta uint32) (new uint32)
- func AddUint64(addr *uint64, delta uint64) (new uint64)
- func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

例子:

package main
import (
    "fmt"
    "sync/atomic"
    "time"
)

func main() {
   var opts int64 = 0

   for i := 0; i < 50; i++ { 
       // 注意第一個參數必須是地址
       atomic.AddInt64(&opts, 3) //加操作
       //atomic.AddInt64(&opts, -1) 減操作
       time.Sleep(time.Millisecond)
   }
   time.Sleep(time.Second)

   fmt.Println("opts: ", atomic.LoadInt64(&opts))
}

由於atomic.AddUint32函數和atomic.AddUint64函數的第二個參數的類型分別是uint32和uint64,所以我們無法通過傳遞一個負的數值來減小被操作值。那么,這是不是就意味着我們無法原子的減小uint32或uint64類型的值了呢?幸好,不是這樣。Go語言為我們提供了一個可以迂回的達到此目的辦法。
如果我們想原子的把uint32類型的變量ui32的值增加NN(NN代表了一個負整數),那么我們可以這樣調用atomic.AddUint32函數:

atomic.AddUint32(&ui32, ^uint32(-NN-1))

對於uint64類型的值來說也是這樣。調用表達式

atomic.AddUint64(&ui64, ^uint64(-NN-1))

表示原子的把uint64類型的變量ui64的值增加NN(或者說減小-NN)。
之所以這種方式可以奏效,是因為它利用了二進制補碼的特性。我們知道,一個負整數的補碼可以通過對它按位(除了符號位之外)求反碼並加一得到。我們還知道,一個負整數可以由對它的絕對值減一並求補碼后得到的數值的二進制表示來代表。例如,如果NN是一個int類型的變量且其值為-35,那么表達式

uint32(int32(NN))

^uint32(-NN-1)

的結果值就都會是11111111111111111111111111011101。由此,我們使用^uint32(-NN-1)和^uint64(-NN-1)來分別表示uint32類型和uint64類型的NN就順理成章了。這樣,我們就可以合理的繞過uint32類型和uint64類型對值的限制了。
以上是官方提供一種通用解決方案。

載入

atomic 包中提供了如下以Load為前綴的增減操作:

- func LoadInt32(addr *int32) (val int32)

- func LoadInt64(addr *int64) (val int64)

- func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

- func LoadUint32(addr *uint32) (val uint32)

- func LoadUint64(addr *uint64) (val uint64)

- func LoadUintptr(addr *uintptr) (val uintptr)

載入操作能夠保證原子的讀變量的值,當讀取的時候,任何其他CPU操作都無法對該變量進行讀寫,其實現機制受到底層硬件的支持。
例子:

v := atomic.LoadInt32(&value)

函數atomic.LoadInt32接受一個*int32類型的指針值,並會返回該指針值指向的那個值。

比較並交換

並交換操作的英文稱謂——Compare And Swap,簡稱CAS。在sync/atomic包中,這類原子操作由名稱以“CompareAndSwap”為前綴的若干個函數代表。
該操作簡稱 CAS(Compare And Swap)。 這類操作的前綴為 CompareAndSwap :

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

- func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)

- func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

- func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)

- func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)

- func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)

該操作在進行交換前首先確保變量的值未被更改,即仍然保持參數 old 所記錄的值,滿足此前提下才進行交換操作。CAS的做法類似操作數據庫時常見的樂觀鎖機制。

需要注意的是,當有大量的goroutine 對變量進行讀寫操作時,可能導致CAS操作無法成功,這時可以利用for循環多次嘗試。

例子:

CompareAndSwapInt64函數接受三個參數。第一個參數的值應該是指向被操作值的指針值。該值的類型即為*int64。后兩個參數的類型都是int64類型。它們的值應該分別代表被操作值的舊值和新值。CompareAndSwapInt64函數在被調用之后會先判斷參數addr指向的被操作值與參數old的值是否相等。僅當此判斷得到肯定的結果之后,該函數才會用參數new代表的新值替換掉原先的舊值。否則,后面的替換操作就會被忽略。這正是“比較並交換”這個短語的由來。CompareAndSwapInt64函數的結果swapped被用來表示是否進行了值的替換操作。

var value int64

func atomicAddOp(tmp int64) {
for {
       oldValue := value
       if atomic.CompareAndSwapInt64(&value, oldValue, oldValue+tmp) {
           return
       }
   }
}

與鎖相比,CAS操作有明顯的不同。它總是假設被操作值未曾被改變(即與舊值相等),並一旦確認這個假設的真實性就立即進行值替換。而使用鎖則是更加謹慎的做法。我們總是先假設會有並發的操作要修改被操作值,並使用鎖將相關操作放入臨界區中加以保護。我們可以說,使用鎖的做法趨於悲觀,而CAS操作的做法則更加樂觀。

CAS操作的優勢是,可以在不形成臨界區和創建互斥量的情況下完成並發安全的值替換操作。這可以大大的減少同步對程序性能的損耗。當然,CAS操作也有劣勢。在被操作值被頻繁變更的情況下,CAS操作並不那么容易成功。

請記住,當想並發安全的更新一些類型(更具體的講是,前文所述的那6個類型)的值的時候,我們總是應該優先選擇CAS操作。

交換

此類操作的前綴為 Swap:

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

- func SwapInt64(addr *int64, new int64) (old int64)

- func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

- func SwapUint32(addr *uint32, new uint32) (old uint32)

- func SwapUint64(addr *uint64, new uint64) (old uint64)

- func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)

相對於CAS,明顯此類操作更為暴力直接,並不管變量的舊值是否被改變,直接賦予新值然后返回背替換的值。

 

存儲

此類操作的前綴為 Store:

- func StoreInt32(addr *int32, val int32)

- func StoreInt64(addr *int64, val int64)

- func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

- func StoreUint32(addr *uint32, val uint32)

- func StoreUint64(addr *uint64, val uint64)

- func StoreUintptr(addr *uintptr, val uintptr)

此類操作確保了寫變量的原子性,避免其他操作讀到了修改變量過程中的臟數據。

 

 

筆記來源:

《GO並發編程實戰》—— 原子操作

Go 原子操作


免責聲明!

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



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