sync/atomic定義
官方文件地址:https://pkg.go.dev/sync/atomi...
Go語言標準庫中的sync/atomic
包提供了偏底層的原子記憶體原語(atomic memory primitives),用於實現同步演算法,其本質是將底層CPU提供的原子操作指令封裝成了Go函式。
使用sync/atomic
提供的原子操作可以確保在任意時刻只有一個goroutine對變數進行操作,避免併發衝突。
使用sync/atomic
需要特別小心,Go官方建議只有在一些偏底層的應用場景裡才去使用sync/atomic
,其它場景建議使用channel
或者sync
包裡的鎖。
Share memory by communicating; don't communicate by sharing memory.
sync/atomic
提供了5種型別的原子操作和1個Value
型別。
5種型別的原子操作
- swap操作:
SwapXXX
- compare-and-swap操作:
CompareAndSwapXXX
- add操作:
AddXXX
- load操作:
LoadXXX
- store操作:
StoreXXX
這幾種型別的原子操作只支援幾個基本的資料型別。
add操作的Addxxx
函式只支援int32
, int64
, uint32
, uint64
, uintptr
這5種基本資料型別。
其它型別的操作函式只支援int32
, int64
, uint32
, uint64
, uintptr
, unsafe.Pointer
這6種基本資料型別。
Value型別
由於上面5種型別的原子操作只支援幾種基本的資料型別,因此為了擴大原子操作的使用範圍,Go團隊在1.4版本的sync/atomic
包中引入了一個新的型別Value
。Value
型別可以用來讀取(Load)和修改(Store)任意型別的值。
Go 1.4版本的Value
型別只有Load
和Store
2個方法,Go 1.17版本又給Value
型別新增了CompareAndSwap
和Swap
這2個新方法。
sync/atomic實踐
swap操作
swap操作支援int32
, int64
, uint32
, uint64
, uintptr
, unsafe.Pointer
這6種基本資料型別,對應有6個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)
swap操作實現的功能是把addr
指標指向的記憶體裡的值替換為新值new
,然後返回舊值old
,是如下虛擬碼的原子實現:
old = *addr
*addr = new
return old
我們拿SwapInt32
舉個例子:
// swap.go
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var newValue int32 = 200
var dst int32 = 100
// 把dst的值替換為newValue
old := atomic.SwapInt32(&dst, newValue)
// 列印結果
fmt.Println("old value: ", old, " new value:", dst)
}
上面程式的執行結果如下:
old value: 100 new value: 200
compare-and-swap操作
compare-and-swap(CAS)操作支援int32
, int64
, uint32
, uint64
, uintptr
, unsafe.Pointer
這6種基本資料型別,對應有6個compare-and-swap操作函式。
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)
compare-and-swap操作實現的功能是先比較addr
指標指向的記憶體裡的值是否為舊值old
相等。
- 如果相等,就把
addr
指標指向的記憶體裡的值替換為新值new
,並返回true
,表示操作成功。 - 如果不相等,直接返回
false
,表示操作失敗。
compare-and-swap操作是如下虛擬碼的原子實現:
if *addr == old {
*addr = new
return true
}
return false
我們拿CompareAndSwapInt32
舉個例子:
// compare-and-swap.go
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var dst int32 = 100
oldValue := atomic.LoadInt32(&dst)
var newValue int32 = 200
// 先比較dst的值和oldValue的值,如果相等,就把dst的值替換為newValue
swapped := atomic.CompareAndSwapInt32(&dst, oldValue, newValue)
// 列印結果
fmt.Printf("old value: %d, swapped value: %d, swapped success: %v\n", oldValue, dst, swapped)
}
上面程式的執行結果如下:
old value: 100, swapped value: 200, swapped success: true
add操作
add操作支援int32
, int64
, uint32
, uint64
, uintptr
這5種基本資料型別,對應有5個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)
add操作實現的功能是把addr
指標指向的記憶體裡的值和delta
做加法,然後返回新值,是如下虛擬碼的原子實現:
*addr += delta
return *addr
我們拿AddInt32
舉個例子:
// add.go
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var wg sync.WaitGroup
// 多個goroutine併發讀寫sum,有併發衝突,最終計算得到的sum值是不準確的
func test1() {
var sum int32 = 0
N := 100
wg.Add(N)
for i := 0; i < N; i++ {
go func(i int32) {
sum += i
wg.Done()
}(int32(i))
}
wg.Wait()
fmt.Println("func test1, sum=", sum)
}
// 使用原子操作計算sum,沒有併發衝突,最終計算得到sum的值是準確的
func test2() {
var sum int32 = 0
N := 100
wg.Add(N)
for i := 0; i < N; i++ {
go func(i int32) {
atomic.AddInt32(&sum, i)
wg.Done()
}(int32(i))
}
wg.Wait()
fmt.Println("func test2, sum=", sum)
}
func main() {
test1()
test2()
}
上面程式的執行結果如下:
func test1, sum= 4857
func test2, sum= 4950
注意:對於test1函式,你本地執行得到的結果可能和我的不一樣,這個值並不是一個固定值。
load操作
load操作支援int32
, int64
, uint32
, uint64
, uintptr
, unsafe.Pointer
這6種基本資料型別,對應有6個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)
load操作實現的功能是返回addr
指標指向的記憶體裡的值,是如下虛擬碼的原子實現:
return *addr
我們拿LoadInt32
舉個例子:
// load.go
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var sum int32 = 100
result := atomic.LoadInt32(&sum)
fmt.Println("result=", result)
}
上面程式的執行結果如下:
result= 100
store操作
store操作支援int32
, int64
, uint32
, uint64
, uintptr
, unsafe.Pointer
這6種基本資料型別,對應有6個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)
store操作實現的功能是把addr
指標指向的記憶體裡的值修改為val
,是如下虛擬碼的原子實現:
*addr = val
我們拿StoreInt32
舉個例子:
// store.go
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var sum int32 = 100
var newValue int32 = 200
// 將sum的值修改為newValue
atomic.StoreInt32(&sum, newValue)
// 讀取修改後的sum值
result := atomic.LoadInt32(&sum)
// 列印結果
fmt.Println("result=", result)
}
上面程式的執行結果如下:
result= 200
Value型別
Go標準庫裡的sync/atomic
包提供了Value
型別,可以用來併發讀取和修改任何型別的值。
Value
型別的定義如下:
// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {
v any
}
Value
型別有4個方法:CompareAndSwap
, Load
, Store
, Swap
,定義如下:
func (v *Value) CompareAndSwap(old, new any) (swapped bool)
func (v *Value) Load() (val any)
func (v *Value) Store(val any)
func (v *Value) Swap(new any) (old any)
原始碼實現:https://cs.opensource.google/...
下面是一個具體的示例:對map[string][string]
型別做併發讀寫,為了避免加鎖,使用value
型別來讀取和修改map[string][string]
。
package main
import (
"sync/atomic"
"time"
)
func loadConfig() map[string]string {
// 從資料庫或者檔案系統中讀取配置資訊,然後以map的形式存放在記憶體裡
return make(map[string]string)
}
func requests() chan int {
// 將從外界中接收到的請求放入到channel裡
return make(chan int)
}
func main() {
// config變數用來存放該服務的配置資訊
var config atomic.Value
// 初始化時從別的地方載入配置檔案,並存到config變數裡
config.Store(loadConfig())
go func() {
// 每10秒鐘定時拉取最新的配置資訊,並且更新到config變數裡
for {
time.Sleep(10 * time.Second)
// 對應於賦值操作 config = loadConfig()
config.Store(loadConfig())
}
}()
// 建立協程,每個工作協程都會根據它所讀取到的最新的配置資訊來處理請求
for i := 0; i < 10; i++ {
go func() {
for r := range requests() {
// 對應於取值操作 c := config
// 由於Load()返回的是一個interface{}型別,所以我們要先強制轉換一下
c := config.Load().(map[string]string)
// 這裡是根據配置資訊處理請求的邏輯...
_, _ = r, c
}
}()
}
}
總結和注意事項
- 原子操作由底層CPU的原子操作指令支援。
- 5種原子操作和
Value
型別的官方文件地址:https://pkg.go.dev/sync/atomi... - CAS操作會有ABA問題
- 對於386處理器架構,64-bit原子操作函式使用了奔騰MMX或更新處理器型號才支援的CPU指令。對於非Linux的ARM處理器架構,64-bit原子操作函式使用了ARMv6k core或更新處理器型號才支援的CPU指令。對於ARM, 386和32-bit MIPS處理器架構,原子操作的呼叫者要對進行原子訪問的64bit字(word)按照64-bit進行記憶體對齊。變數或者分配的結構體、陣列和切片的第1個字可以認為是64-bit對齊的。(這塊涉及到記憶體對齊,後面抽個專題詳解)
開源地址
文章和示例程式碼開源在GitHub: Go語言初級、中級和高階教程。
公眾號:coding進階。關注公眾號可以獲取最新Go面試題和技術棧。
個人網站:Jincheng's Blog。
知乎:無忌。