atomic
原子操作
原子操作即是進行過程中不能被中斷的操作,針對某個值的原子操作在被進行的過程中,CPU絕不會再去進行其他的針對該值的操作。為了實現這樣的嚴謹性,原子操作僅會由一個獨立的CPU指令代表和完成。原子操作是無鎖的,常常直接通過CPU指令直接實現。 事實上,其它同步技術的實現常常依賴於原子操作。
具體的原子操作在不同的操作系統中實現是不同的。比如在Intel的CPU架構機器上,主要是使用總線鎖的方式實現的。 大致的意思就是當一個CPU需要操作一個內存塊的時候,向總線發送一個LOCK信號,所有CPU收到這個信號后就不對這個內存塊進行操作了。 等待操作的CPU執行完操作后,發送UNLOCK信號,才結束。 在AMD的CPU架構機器上就是使用MESI一致性協議的方式來保證原子操作。 所以我們在看atomic源碼的時候,我們看到它針對不同的操作系統有不同匯編語言文件。
Go中原子操作的支持
Go語言的sync/atomic
提供了對原子操作的支持,用於同步訪問整數和指針。
- Go語言提供的原子操作都是非入侵式的
- 原子操作支持的類型包括
int32、int64、uint32、uint64、uintptr、unsafe.Pointer
。
競爭條件是由於異步的訪問共享資源,並試圖同時讀寫該資源而導致的,使用互斥鎖和通道的思路都是在線程獲得到訪問權后阻塞其他線程對共享內存的訪問,而使用原子操作解決數據競爭問題則是利用了其不可被打斷的特性。
CompareAndSwap(CAS)
go中的Cas操作,是借用了CPU提供的原子性指令來實現。CAS操作修改共享變量時候不需要對共享變量加鎖,而是通過類似樂觀鎖的方式進行檢查,本質還是不斷的占用CPU 資源換取加鎖帶來的開銷(比如上下文切換開銷)。
原子操作中的CAS(Compare And Swap),在sync/atomic
包中,這類原子操作由名稱以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)
CompareAndSwap
函數會先判斷參數addr指向的操作值與參數old的值是否相等,僅當此判斷得到的結果是true之后,才會用參數new代表的新值替換掉原先的舊值,否則操作就會被忽略。
查看下源碼,這幾個代碼差不多,以CompareAndSwapUint32
為例子,golang主要還是依賴匯編來來實現的原子操作,不同的CPU架構是有對應不同的.s匯編文件的。
/usr/local/go/src/sync/atomic/asm.s
TEXT ·CompareAndSwapUint32(SB),NOSPLIT,$0
JMP runtime∕internal∕atomic·Cas(SB)
看下匯編的Cas
// bool Casp1(void **val, void *old, void *new)
// Atomically:
// if(*val == old){
// *val = new;
// return 1;
// } else
// return 0;
TEXT runtime∕internal∕atomic·Casp1(SB), NOSPLIT, $0-25
// 首先將 ptr 的值放入 BX
MOVQ ptr+0(FP), BX
// 將假設的舊值放入 AX
MOVQ old+8(FP), AX
// 需要比較的新值放入到CX
MOVQ new+16(FP), CX
LOCK
CMPXCHGQ CX, 0(BX)
SETEQ ret+24(FP)
RET
MOV 指令有有好幾種后綴 MOVB MOVW MOVL MOVQ 分別對應的是 1 字節 、2 字節 、4 字節、8 字節
TEXT runtime∕internal∕atomic·Cas(SB),NOSPLIT,$0-17
,$0-17
表示的意思是這個TEXT block
運行的時候,需要開辟的棧幀大小是0,而17 = 8 + 4 + 4 + 1 = sizeof(pointer of int32) + sizeof(int32) + sizeof(int32) + sizeof(bool)
(返回值是 bool ,占據 1 個字節)
FP
,是偽寄存器(pseudo) ,里邊存的是 Frame Pointer
, FP
配合偏移 可以指向函數調用參數或者臨時變量
MOVQ ptr+0(FP) BX
這一句話是指把函數的第一個參數ptr+0(FP)
移動到BX
寄存器中
MOVQ代
表移動的是8個字節,Q 代表64bit
,參數的引用是 參數名稱+偏移(FP),可以看到這里名稱用了ptr
,並不是val
,變量名對匯編不會有什么影響,但是語法上是必須帶上的,可讀性也會更好些。
LOCK
並不是指令,而是一個指令的前綴(instruction prefix)
,是用來修飾CMPXCHGL CX,0(BX)
的
The LOCK prefix ensures that the CPU has exclusive ownership of the appropriate cache line for the duration of the operation, and provides certain additional ordering guarantees. This may be achieved by asserting a bus lock, but the CPU will avoid this where possible. If the bus is locked then it is only for the duration of the locked instruction
CMPXCHGL
有兩個操作數,CX 和 0(BX)
,0(BX)
代表的是val
的地址。
CMPXCHGL
指令做的事情,首先會把0(BX)
里的值和AX
寄存器里存的值做比較,如果一樣的話會把CX
里邊存的值保存到0(BX)
這塊地址里 (雖然這條指令里並沒有出現AX
,但是還是用到了,匯編里還是有不少這樣的情況)
SETEQ
會在AX
和CX
相等的時候把1寫進 ret+16(FP)
(否則寫 0)
看下如何使用
func main() {
var a, b int32 = 13, 13
var c int32 = 9
res := atomic.CompareAndSwapInt32(&a, b, c)
fmt.Println("swapped:", res)
fmt.Println("替換的值:", c)
fmt.Println("替換之后a的值:", a)
}
查看下輸出
swapped: true
替換的值: 9
替換之后a的值: 9
a
值和b
值作比較,當a
和b
相等時,會用c
的值替換掉a
的值
我們使用的mutex
互斥鎖類似悲觀鎖,總是假設會有並發的操作要修改被操作的值,所以使用鎖將相關操作放入到臨界區加以保存。而CAS操作做法趨於樂觀鎖,總是假設被操作的值未曾改變(即與舊值相等),並一旦確認這個假設的真實性就立即進行值替換。在被操作值被頻繁變更的情況下,CAS
操作並不那么容易成功所以需要不斷進行嘗試,直到成功為止。
舉個栗子
func main() {
fmt.Println("======old value=======")
fmt.Println(value)
addValue(10)
fmt.Println("======New value=======")
fmt.Println(value)
}
//不斷地嘗試原子地更新value的值,直到操作成功為止
func addValue(delta int32) {
for {
v := value
if atomic.CompareAndSwapInt32(&value, v, v+delta) {
break
}
}
}
Swap(交換)
上面的CompareAndSwap
系列的函數需要比較后再進行交換,也有不需要進行比較就進行交換的原子操作。
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)
幾個差不多,來看下SwapInt32
的源碼,也是通過匯編來實現的
/usr/local/go/src/sync/atomic/asm.s
TEXT ·SwapUint32(SB),NOSPLIT,$0
JMP runtime∕internal∕atomic·Xchg(SB)
看下匯編的Xchg
TEXT runtime∕internal∕atomic·Xchg(SB), NOSPLIT, $0-20
MOVQ ptr+0(FP), BX
MOVL new+8(FP), AX
// 原子操作, 把_value的值和newValue交換, 且返回_value原來的值
XCHGL AX, 0(BX)
MOVL AX, ret+16(FP)
RET
舉個栗子
func main() {
var a, b int32 = 13, 12
old := atomic.SwapInt32(&a, b)
fmt.Println("old的值:", old)
fmt.Println("替換之后a的值", a)
}
查看下輸出
old的值: 13
替換之后a的值 12
Add(增加或減少)
對一個數值進行增加或者減少的行為也需要保證是原子的,它對應於atomic包的函數就是
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)
舉個栗子
func main() {
var a int32 = 13
addValue := atomic.AddInt32(&a, 1)
fmt.Println("增加之后:", addValue)
delValue := atomic.AddInt32(&a, -4)
fmt.Println("減少之后:", delValue)
}
查看下輸出
增加之后: 14
減少之后: 10
Load(原子讀取)
當我們要讀取一個變量的時候,很有可能這個變量正在被寫入,這個時候,我們就很有可能讀取到寫到一半的數據。 所以讀取操作是需要一個原子行為的。
在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)
Store(原子寫入)
讀取是有原子性的操作的,同樣寫入atomic包也提供了相關的操作包。
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)
原子操作與互斥鎖的區別
首先atomic操作的優勢是更輕量,比如CAS可以在不形成臨界區和創建互斥量的情況下完成並發安全的值替換操作。這可以大大的減少同步對程序性能的損耗。
原子操作也有劣勢。還是以CAS操作為例,使用CAS操作的做法趨於樂觀,總是假設被操作值未曾被改變(即與舊值相等),並一旦確認這個假設的真實性就立即進行值替換,那么在被操作值被頻繁變更的情況下,CAS操作並不那么容易成功。而使用互斥鎖的做法則趨於悲觀,我們總假設會有並發的操作要修改被操作的值,並使用鎖將相關操作放入臨界區中加以保護。
下面是幾點區別:
- 互斥鎖是一種數據結構,用來讓一個線程執行程序的關鍵部分,完成互斥的多個操作
- 原子操作是無鎖的,常常直接通過CPU指令直接實現
- 原子操作中的cas趨於樂觀鎖,CAS操作並不那么容易成功,需要判斷,然后嘗試處理
- 可以把互斥鎖理解為悲觀鎖,共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程
atomic
包提供了底層的原子性內存原語,這對於同步算法的實現很有用。這些函數一定要非常小心地使用,使用不當反而會增加系統資源的開銷,對於應用層來說,最好使用通道或sync包中提供的功能來完成同步操作。
針對atomic
包的觀點在Google的郵件組里也有很多討論,其中一個結論解釋是:
應避免使用該包裝。或者,閱讀C ++ 11標准的“原子操作”一章;如果您了解如何在C ++中安全地使用這些操作,那么你才能有安全地使用Go的sync/atomic包的能力。
atomic.Value
此類型的值相當於一個容器,可以被用來“原子地"存儲(Store)和加載(Load)任意類型的值。當然這個類型也是原子性的。
有了atomic.Value
這個類型,這樣用戶就可以在不依賴Go
內部類型unsafe.Pointer
的情況下使用到atomic提供的原子操作。
分析下源碼
// A Value must not be copied after first use.
type Value struct {
v interface{}
}
里面主要是包含了兩個方法
-
v.Store(c)
- 寫操作,將原始的變量c存放到一個atomic.Value
類型的v里。 -
c = v.Load()
- 讀操作,從線程安全的v中讀取上一步存放的內容。
Load
// ifaceWords is interface{} internal representation.
type ifaceWords struct {
// 類型
typ unsafe.Pointer
// 數據
data unsafe.Pointer
}
// 如果沒Store將返回nil
func (v *Value) Load() (x interface{}) {
// 獲得 interface 結構的指針
vp := (*ifaceWords)(unsafe.Pointer(v))
// 獲取類型
typ := LoadPointer(&vp.typ)
// 判斷,第一次寫入還沒有開始,或者還沒完成,返回nil
if typ == nil || uintptr(typ) == ^uintptr(0) {
// First store not yet completed.
return nil
}
// 獲得存儲值的實際數據
data := LoadPointer(&vp.data)
// 將復制得到的 typ 和 data 給到 x
xp := (*ifaceWords)(unsafe.Pointer(&x))
xp.typ = typ
xp.data = data
return
}
1、Load中也是借助於atomic.LoadPointer
來實現的;
2、使用了Go
運行時類型系統中的interface{}
這一類型本質上由 兩段內容組成,一個是類型typ
區域,另一個是實際數據data
區域;
3、保證與原子性,加入了一個判斷:
-
typ為nil表示還沒有寫入值
-
uintptr(typ) == ^uintptr(0)
表示有第一次寫入還沒有完成
Store
// 如果兩次Store的類型不同將會panic
// 如果寫入nil,也會panic
func (v *Value) Store(x interface{}) {
// value不能為nil
if x == nil {
panic("sync/atomic: store of nil value into Value")
}
// Value存儲的指針
vp := (*ifaceWords)(unsafe.Pointer(v))
// 寫入value的目標指針x
xp := (*ifaceWords)(unsafe.Pointer(&x))
for {
typ := LoadPointer(&vp.typ)
// 第一次Store
if typ == nil {
// 禁止搶占當前 Goroutine 來確保存儲順利完成
runtime_procPin()
// 如果typ為nil,設置一個標志位,宣告正在有人操作此值
if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
// 如果沒有成功,取消不可搶占,下次再試
runtime_procUnpin()
continue
}
// 如果標志位設置成功,說明其他人都不會向 interface{} 中寫入數據
// 這點細品很巧妙,先寫數據,在寫類型,應該類型設置了不可寫入的表示位
// 寫入數據
StorePointer(&vp.data, xp.data)
// 寫入類型
StorePointer(&vp.typ, xp.typ)
// 存儲成功,取消不可搶占,,直接返回
runtime_procUnpin()
return
}
// 已經有值寫入了,或者有正在寫入的Goroutine
// 有其他 Goroutine 正在對 v 進行寫操作
if uintptr(typ) == ^uintptr(0) {
continue
}
// 如果本次存入的類型與前次存儲的類型不同
if typ != xp.typ {
panic("sync/atomic: store of inconsistently typed value into Value")
}
// 類型已經寫入,直接保存數據
StorePointer(&vp.data, xp.data)
return
}
}
梳理下流程:
1、首先判斷類型如果為nil直接panic;
2、然后通過有個for循環來連續判斷是否可以進行值的寫入;
3、如果是typ == nil
表示是第一次寫入,然后給type設置一個標識位,來表示有goroutine正在寫入;
4、然后寫入值,退出;
5、如果type不為nil,但是等於標識位,表示有正在寫入的goroutine,然后繼續循環;
6、最后type不為nil,並且不等於標識位,並且和value里面的type類型一樣,寫入內容,然后退出。
注意:其中使用了runtime_procPin()
方法,它可以將一個goroutine
死死占用當前使用的P(P-M-G中的processor)
,不允許其它goroutine/M
搶占,這樣就能保證存儲順利完成,不必擔心競爭的問題。釋放pin的方法是runtime_procUnpin
。

總結
1、atomic中的操作是原子性的;
2、原子操作由底層硬件支持,而鎖則由操作系統的調度器實現。鎖應當用來保護一段邏輯,對於一個變量更新的保護,原子操作通常會更有效率,並且更能利用計算機多核的優勢,如果要更新的是一個復合對象,則應當使用atomic.Value
封裝好的實現。
3、atomic中的代碼,主要還是依賴匯編來來實現的原子操作。
參考
【Go並發編程之美-CAS操作】https://zhuanlan.zhihu.com/p/56733484
【sync/atomic - 原子操作】https://docs.kilvn.com/The-Golang-Standard-Library-by-Example/chapter16/16.02.html
【Go語言的原子操作和互斥鎖的區別】https://studygolang.com/articles/29240
【Package atomic】https://go-zh.org/pkg/sync/atomic/
【Go 語言標准庫中 atomic.Value 的前世今生】https://blog.betacat.io/post/golang-atomic-value-exploration/
【原子操作】https://golang.design/under-the-hood/zh-cn/part4lib/ch15sync/atomic/
【關於Go語言中的go:linkname】https://blog.csdn.net/IT_DREAM_ER/article/details/103590944
【原子操作使用】https://www.kancloud.cn/digest/batu-go/153537
【Go源碼解析之atomic】https://amazingao.com/posts/2020/11/go-src/sync/atomic/
【Plan 9 匯編語言】https://golang.design/under-the-hood/zh-cn/part1basic/ch01basic/asm/