Golang sync/atomic包——原子操作
阿新 • • 發佈:2022-01-18
1、概述
1.1 基本概念
原子性:一個或多個操作在CPU的執行過程中不被中斷的特性,稱為原子性。這些操作對外表現成一個不可分割的整體,他們要麼都執行,要麼都不執行,外界不會看到他們只執行到一半的狀態。
原子操作:進行過程中不能被中斷的操作,原子操作由底層硬體支援,而鎖則是由作業系統提供的API實現,若實現相同的功能,前者通常會更有效率
Golang 中的原子操作:sync/atomic包
能夠進行原子操作的型別:int32, int64, uint32, uint64, uintptr, unsafe.Pointer
五種操作函式:增或減、比較並交換、載入、儲存、交換
原子操作比鎖更為高效。
1.2 原子操作 vs 鎖
- 加鎖比較耗時,需要上下文切換。即使是goroutine也需要上下文切換
- 只針對基本型別,可使用原子操作保證執行緒安全
- 原子操作在使用者態完成,效能比互斥鎖要高
- 原子操作步驟簡單,不需要加鎖-操作-解鎖
1.3 五種操作
- 增或減 (Add)
- 比較並交換 (CAS, Compare & Swap)
- 載入 (Load)
- 儲存 (Store)
- 交換 (Swap)
1.4 最小案例
package main import ( "sync" "fmt" ) var count int func add(wg *sync.WaitGroup) { defer wg.Done() count++ } func main() { wg := sync.WaitGroup{} wg.Add(1000) for i := 0; i < 1000; i++ { go add(&wg) } wg.Wait() fmt.Println(count) }
count
不會等於1000,因為count++
這一步實際是三個操作:
- 從記憶體讀取
count
- CPU更新
count = count + 1
- 寫入
count
到記憶體
因此就會出現多個goroutine讀取到相同的數值,然後更新同樣的數值到記憶體,導致最終結果比預期少。
2、sync/atomic包使用
Go語言提供的原子操作都是非入侵式的,由標準庫中sync/aotomic
中的眾多函式代表
atomic包中支援六種型別
int32
uint32
int64
uint64
uintptr
unsafe.Pointer
對於每一種型別,提供了五類原子操作:
LoadXXX(addr)
: 原子性的獲取*addr
return *addr
StoreXXX(addr, val)
: 原子性的將val
的值儲存到*addr
,等價於:addr = val
AddXXX(addr, delta)
: 原子性的將delta
的值新增到*addr
並返回新值(unsafe.Pointer
不支援),等價於:*addr += delta return *addr
SwapXXX(addr, new) old
: 原子性的將new
的值儲存到*addr
並返回舊值,等價於:old = *addr *addr = new return old
CompareAndSwapXXX(addr, old, new) bool
: 原子性的比較*addr
和old
,如果相同則將new
賦值給*addr
並返回true
,等價於:if *addr == old { *addr = new return true } return false
因此第一部分的案例可以修改如下,即可通過
// 修改方式1 func add(wg *sync.WaitGroup) { defer wg.Done() atomic.AddInt32(&count, 1) } // 修改方式2 func add(wg *sync.WaitGroup) { defer wg.Done() for { if atomic.CompareAndSwapInt32(&count, count, count+1) { break } } }
參考:https://juejin.cn/post/6844904053042839560
參考:https://blog.csdn.net/elihe2011/article/details/109157797