Golang 五種原子性操作的用法詳解
本文我們詳細聊一下 Go
語言的原子操作的用法,啥是原子操作呢?顧名思義,原子操作就是具備原子性的操作... 是不是感覺說了跟沒說一樣,原子性的解釋如下:
一個或者多個操作在 CPU 執(zhí)行的過程中不被中斷的特性,稱為 原子性(atomicity) 。這些操作對外表現(xiàn)成一個不可分割的整體,他們要么都執(zhí)行,要么都不執(zhí)行,外界不會看到他們只執(zhí)行到一半的狀態(tài)。
CPU
執(zhí)行一系列操作時不可能不發(fā)生中斷,但如果我們在執(zhí)行多個操作時,能讓他們的 中間狀態(tài)對外不可見 ,那我們就可以宣稱他們擁有了"不可分割”的原子性。
類似的解釋我們在數(shù)據(jù)庫事務的 ACID
概念里也聽過。
Go 語言提供了哪些原子操作
Go
語言通過內(nèi)置包 sync/atomic
提供了對原子操作的支持,其提供的原子操作有以下幾大類:
- AddXXXType
- int32
- int64
- uint32
- uint64
- uintptr
- XXXType
LoadXXXType
,支持的類型除了基礎類型外還支持 Pointer
,也就是支持載入任何類型的指針。Store
開頭,支持的類型跟載入操作支持的那些一樣。
- CAS
- Go
- CAS
交換,這個簡單粗暴一些,不比較直接交換,這個操作很少會用。
互斥鎖跟原子操作的區(qū)別
平日里,在并發(fā)編程里,Go語言 sync
包里的同步原語 Mutex
是我們經(jīng)常用來保證并發(fā)安全的,那么他跟 atomic
包里的這些操作有啥區(qū)別呢?在我看來他們在使用目的和底層實現(xiàn)上都不一樣:
-
使用目的:互斥鎖是用來保護一段邏輯,原子操作用于對一個變量的更新保護。
- 底層實現(xiàn):
Mutex
由 操作系統(tǒng) 的調(diào)度器實現(xiàn),而atomic
包中的原子操作則由 底層硬件指令 直接提供支持,這些指令在執(zhí)行的過程中是不允許中斷的,因此原子操作可以在lock-free
的情況下保證并發(fā)安全,并且它的性能也能做到隨CPU
個數(shù)的增多而線性擴展。
對于一個變量更新的保護,原子操作通常會更有效率,并且更能利用計算機多核的優(yōu)勢。
比如下面這個,使用互斥鎖的并發(fā)計數(shù)器程序:
- func mutexAdd() {
- var a int32 = 0
- var wg sync.WaitGroup
- var mu sync.Mutex
- start := time.Now()
- for i := 0; i < 100000000; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- mu.Lock()
- a += 1
- mu.Unlock()
- }()
- }
- wg.Wait()
- timeSpends := time.Now().Sub(start).Nanoseconds()
- fmt.Printf("use mutex a is %d, spend time: %v\n", a, timeSpends)
- }
把 Mutex
改成用方法 atomic.AddInt32(&a, 1)
調(diào)用,在不加鎖的情況下仍然能確保對變量遞增的并發(fā)安全。
- func AtomicAdd() {
- var a int32 = 0
- var wg sync.WaitGroup
- start := time.Now()
- for i := 0; i < 1000000; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- atomic.AddInt32(&a, 1)
- }()
- }
- wg.Wait()
- timeSpends := time.Now().Sub(start).Nanoseconds()
- fmt.Printf("use atomic a is %d, spend time: %v\n", atomic.LoadInt32(&a), timeSpends)
- }
可以在本地運行以上這兩段代碼,可以觀察到計數(shù)器的結(jié)果都最后都是 1000000
,都是線程安全的。
需要注意的是,所有原子操作方法的被操作數(shù)形參必須是指針類型,通過指針變量可以獲取被操作數(shù)在內(nèi)存中的地址,從而施加特殊的CPU指令,確保同一時間只有一個goroutine能夠進行操作。
上面的例子除了增加操作外我們還演示了載入操作,接下來我們來看一下 CAS
操作。
比較并交換
該操作簡稱 CAS
(Compare And Swap)。這類操作的前綴為 CompareAndSwap
:
- func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
- func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
該操作在 進行交換前首先確保被操作數(shù)的值未被更改,即仍然保存著參數(shù) old
所記錄的值,滿足此前提條件下才進行交換操作 。 CAS
的做法類似操作數(shù)據(jù)庫時常見的樂觀鎖機制。
需要注意的是,當有大量的goroutine 對變量進行讀寫操作時,可能導致 CAS
操作無法成功,這時可以利用 for
循環(huán)多次嘗試。
上面我只列出了比較典型的 int32
和 unsafe.Pointer
類型的 CAS
方法,主要是想說除了讀數(shù)值類型進行比較交換,還支持對指針進行比較交換。
unsafe.Pointer提供了繞過Go語言指針類型限制的方法,unsafe指的并不是說不安全,而是說官方并不保證向后兼容。
- // 定義一個struct類型P
- type P struct{ x, y, z int }
- // 執(zhí)行類型P的指針
- var pP *P
- func main() {
- // 定義一個執(zhí)行unsafe.Pointer值的指針變量
- var unsafe1 = (*unsafe.Pointer)(unsafe.Pointer(&pP))
- // Old pointer
- var sy P
- // 為了演示效果先將unsafe1設置成Old Pointer
- px := atomic.SwapPointer(
- unsafe1, unsafe.Pointer(&sy))
- // 執(zhí)行CAS操作,交換成功,結(jié)果返回true
- y := atomic.CompareAndSwapPointer(
- unsafe1, unsafe.Pointer(&sy), px)
- fmt.Println(y)
- }
上面的示例并不是在并發(fā)環(huán)境下進行的 CAS
,只是為了演示效果,先把被操作數(shù)設置成了 Old Pointer
。
其實 Mutex
的底層實現(xiàn)也是依賴原子操作中的 CAS
實現(xiàn)的,原子操作的 atomic
包相當于是 sync
包里的那些同步原語的實現(xiàn)依賴。
比如互斥鎖 Mutex
的結(jié)構(gòu)里有一個 state
字段,其是表示鎖狀態(tài)的狀態(tài)位。
- type Mutex struct {
- state int32
- sema uint32
- }
為了方便理解,我們在這里將它的狀態(tài)定義為0和1,0代表目前該鎖空閑,1代表已被加鎖,以下是 sync.Mutex
中 Lock
方法的部分實現(xiàn)代碼。
- func (m *Mutex) Lock() {
- // Fast path: grab unlocked mutex.
- if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
- if race.Enabled {
- race.Acquire(unsafe.Pointer(m))
- }
- return
- }
- // Slow path (outlined so that the fast path can be inlined)
- m.lockSlow()
- }
在 atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked)
中, m.state
代表鎖的狀態(tài),通過 CAS
方法,判斷鎖此時的狀態(tài)是否空閑( m.state==0
),是,則對其加鎖( mutexLocked
常量的值為1)。
atomic.Value保證任意值的讀寫安全
atomic
包里提供了一套 Store
開頭的方法,用來保證各種類型變量的并發(fā)寫安全,避免其他操作讀到了修改變量過程中的臟數(shù)據(jù)。
- func StoreInt32(addr *int32, val int32)
- func StoreInt64(addr *int64, val int64)
- func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
- ...
這些操作方法的定義與上面介紹的那些操作的方法類似,我就不再演示怎么使用這些方法了。
值得一提的是如果你想要并發(fā)安全的設置一個結(jié)構(gòu)體的多個字段,除了把結(jié)構(gòu)體轉(zhuǎn)換為指針,通過 StorePointer
設置外,還可以使用 atomic
包后來引入的 atomic.Value
,它在底層為我們完成了從具體指針類型到 unsafe.Pointer
之間的轉(zhuǎn)換。
有了 atomic.Value
后,它使得我們可以不依賴于不保證兼容性的 unsafe.Pointer
類型,同時又能將任意數(shù)據(jù)類型的讀寫操作封裝成原子性操作(中間狀態(tài)對外不可見)。
atomic.Value
類型對外暴露了兩個方法:
- v.Store(c)
- c
- atomic.Value
- v
c := v.Load()
- 讀操作,從線程安全的 v
中讀取上一步存放的內(nèi)容。1.17 版本我看還增加了 Swap
和 CompareAndSwap
方法。
簡潔的接口使得它的使用也很簡單,只需將需要做并發(fā)保護的變量讀取和賦值操作用 Load()
和 Store()
代替就行了。
由于 Load()
返回的是一個 interface{}
類型,所以在使用前我們記得要先轉(zhuǎn)換成具體類型的值,再使用。下面是一個簡單的例子演示 atomic.Value
的用法。
- type Rectangle struct {
- length int
- width int
- }
- var rect atomic.Value
- func update(width, length int) {
- rectLocal := new(Rectangle)
- rectLocal.width = width
- rectLocal.length = length
- rect.Store(rectLocal)
- }
- func main() {
- wg := sync.WaitGroup{}
- wg.Add(10)
- // 10 個協(xié)程并發(fā)更新
- for i := 0; i < 10; i++ {
- go func() {
- defer wg.Done()
- update(i, i+5)
- }()
- }
- wg.Wait()
- _r := rect.Load().(*Rectangle)
- fmt.Printf("rect.width=%d\nrect.length=%d\n", _r.width, _r.length)
- }
你也可以試試,不用 atomic.Value
,直接給 Rectange
類型的指針變量賦值,看看在并發(fā)條件下,兩個字段的值是不是能跟預期的一樣變成10和15。
總結(jié)
本文詳細介紹了Go語言原子操作 atomic
包中會被高頻使用的操作的使用場景和用法,當然我并沒有羅列 atomic
包里所有操作的用法,主要是考慮到有的用到的地方實在不多,或者是已經(jīng)被更好的方式替代,還有就是覺得確實沒必要,看完本文的內(nèi)容相信你已經(jīng)完全具備自行探索 atomic
包的能力了。
再強調(diào)一遍,原子操作由 底層硬件 支持,而鎖則由操作系統(tǒng)的 調(diào)度器 實現(xiàn)。鎖應當用來保護一段邏輯,對于一個變量更新的保護,原子操作通常會更有效率,并且更能利用計算機多核的優(yōu)勢,如果要更新的是一個復合對象,則應當使用 atomic.Value
封裝好的實現(xiàn)。
給網(wǎng)管個星標第一時間吸我的知識 :point_up_2: