前面幾篇文章介紹了Golang中互斥鎖、讀寫鎖、條件變量,雖然它們可以很好地協調對共享資源的通路,但并不能保證原子操作。
原子操作
原子操作是指一系列操作要麼全部執行成功,要麼全部執行失敗,不會有中間狀态。
鎖無法保證原子性是因為:
- 在鎖保護的臨界區代碼執行期間,其他協程無法通路該代碼段,但是它們可以通路其他資源,可能會導緻原子操作失敗;
- 鎖雖然能做到隻讓一個goroutine執行臨界區代碼,不被其他goroutine打擾,不過仍然可能被系統中斷(因為goroutine都是統一被runtime排程的,runtime會頻繁切換一個goroutine的運作狀态)
可以看出原子操作的粒度更細,它對單個變量的通路進行了原子化保證,在操作完成之前會阻塞其他并發操作。能夠保證原子性執行的隻有原子操作,原子操作在執行過程中是不允許被中斷的。在計算機底層,原子性是由CPU支援的,是以絕對有效。Golang中的原子操作是基于作業系統和CPU的,具體功能由标準庫中的sync/atomic包提供。
sync/atomic
sync/atomic包提供的原子操作有Add、Load、Store、Swap和CompareAndSwap,這些函數支援的資料類型有int32、int64、uint32、uint64、uintptr和unsafe包中的Pointer,不過,沒有提供針對unsafe.Pointer的Add方法。具體方法如下:
- AddInt32/AddInt64/AddUint32/AddUint64/AddUintptr: 原子地将指定的值加到一個位址中的值上。
- CompareAndSwapInt32/CompareAndSwapInt64/CompareAndSwapUint32/CompareAndSwapUint64/CompareAndSwapUintptr/CompareAndSwapPointer: 原子地比較一個指定類型位址中的值,如果該值和參數old比對,就在那個位址處存儲參數new。
- SwapInt32/SwapInt64/SwapUint32/SwapUint64/SwapUintptr/SwapPointer: 原子地将值存儲在指定位址處,并傳回此位址的舊值。
- LoadInt32/LoadInt64/LoadUint32/LoadUint64/LoadUintptr/LoadPointer: 原子地傳回指定位址中的值。
- StoreInt32/StoreInt64/StoreUint32/StoreUint64/StoreUintptr/StorePointer: 原子地将指定值存儲到指定類型位址中。
此外,sync/atomic包還提供一個名稱為Value的類型,可以被用來存儲任意類型的值,結構如下:
type Value struct {
v any
}
使用方法和示例
使用原子操作可以用于計算需要在多個goroutine之間共享的計數器。例如,計算線上使用者數量、任務完成情況等等。
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var counter int64
done := make(chan bool)
for i := 0; i < 100; i++ {
go func() {
atomic.AddInt64(&counter, 1)
done <- true
}()
}
for i := 0; i < 100; i++ {
<-done
}
fmt.Println(counter)
}
首先聲明了一個int64類型的計數器變量counter,使用AddInt64原子操作對其進行遞增。通過使用AddInt64,確定了每個goroutine對其值的修改操作都能夠安全進行。
再看一個自旋鎖的示例:
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
sign := make(chan struct{}, 2)
var num int32
go func() {
defer func() {
sign <- struct{}{}
}()
for {
// 定時增大num值
time.Sleep(time.Millisecond * 500)
newNum := atomic.AddInt32(&num, 2)
fmt.Printf("num目前值為: %d\n", newNum)
// 滿足條件後退出
if newNum == 10 {
break
}
}
}()
go func() {
// 定時檢查num值,等于10則歸零
defer func() {
sign <- struct{}{}
}()
for {
if atomic.CompareAndSwapInt32(&num, 10, 0) {
fmt.Println("已将num歸零")
break
}
time.Sleep(time.Millisecond * 500)
}
}()
<-sign
<-sign
}