詳解Golang五種原子性操作的用法
本文我們?cè)敿?xì)聊一下Go語(yǔ)言的原子操作的用法,啥是原子操作呢?顧名思義,原子操作就是具備原子性的操作... 是不是感覺(jué)說(shuō)了跟沒(méi)說(shuō)一樣,原子性的解釋如下:
一個(gè)或者多個(gè)操作在 CPU 執(zhí)行的過(guò)程中不被中斷的特性,稱(chēng)為原子性(atomicity) 。這些操作對(duì)外表現(xiàn)成一個(gè)不可分割的整體,他們要么都執(zhí)行,要么都不執(zhí)行,外界不會(huì)看到他們只執(zhí)行到一半的狀態(tài)。
CPU執(zhí)行一系列操作時(shí)不可能不發(fā)生中斷,但如果我們?cè)趫?zhí)行多個(gè)操作時(shí),能讓他們的中間狀態(tài)對(duì)外不可見(jiàn),那我們就可以宣稱(chēng)他們擁有了"不可分割”的原子性。
類(lèi)似的解釋我們?cè)跀?shù)據(jù)庫(kù)事務(wù)的ACID概念里也聽(tīng)過(guò),只不過(guò)這里保障原子性的執(zhí)行體是CPU。
Go 語(yǔ)言提供了哪些原子操作
Go語(yǔ)言通過(guò)內(nèi)置包sync/atomic提供了對(duì)原子操作的支持,其提供的原子操作有以下幾大類(lèi):
- 增減,操作方法的命名方式為AddXXXType,保證對(duì)操作數(shù)進(jìn)行原子的增減,支持的類(lèi)型為int32、int64、uint32、uint64、uintptr,使用時(shí)以實(shí)際類(lèi)型替換前面我說(shuō)的XXXType就是對(duì)應(yīng)的操作方法。
- 載入,保證了讀取到操作數(shù)前沒(méi)有其他任務(wù)對(duì)它進(jìn)行變更,操作方法的命名方式為L(zhǎng)oadXXXType,支持的類(lèi)型除了基礎(chǔ)類(lèi)型外還支持Pointer,也就是支持載入任何類(lèi)型的指針。
- 存儲(chǔ),有載入了就必然有存儲(chǔ)操作,這類(lèi)操作的方法名以Store開(kāi)頭,支持的類(lèi)型跟載入操作支持的那些一樣。
- 比較并交換,也就是CAS (Compare And Swap),像Go的很多并發(fā)原語(yǔ)實(shí)現(xiàn)就是依賴(lài)的CAS操作,同樣是支持上面列的那些類(lèi)型。
- 交換,這個(gè)簡(jiǎn)單粗暴一些,不比較直接交換,這個(gè)操作很少會(huì)用。
互斥鎖跟原子操作的區(qū)別
平日里,在并發(fā)編程里,Go語(yǔ)言sync包里的同步原語(yǔ)Mutex是我們經(jīng)常用來(lái)保證并發(fā)安全的,那么他跟atomic包里的這些操作有啥區(qū)別呢?在我看來(lái)他們?cè)谑褂媚康暮偷讓訉?shí)現(xiàn)上都不一樣:
- 使用目的:互斥鎖是用來(lái)保護(hù)一段邏輯,原子操作用于對(duì)一個(gè)變量的更新保護(hù)。
- 底層實(shí)現(xiàn):Mutex由操作系統(tǒng)的調(diào)度器實(shí)現(xiàn),而atomic包中的原子操作則由底層硬件指令直接提供支持,這些指令在執(zhí)行的過(guò)程中是不允許中斷的,因此原子操作可以在lock-free的情況下保證并發(fā)安全,并且它的性能也能做到隨CPU個(gè)數(shù)的增多而線(xiàn)性擴(kuò)展。
對(duì)于一個(gè)變量更新的保護(hù),原子操作通常會(huì)更有效率,并且更能利用計(jì)算機(jī)多核的優(yōu)勢(shì)。
比如下面這個(gè),使用互斥鎖的并發(fā)計(jì)數(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)用,在不加鎖的情況下仍然能確保對(duì)變量遞增的并發(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)
}
可以在本地運(yùn)行以上這兩段代碼,可以觀(guān)察到計(jì)數(shù)器的結(jié)果都最后都是1000000,都是線(xiàn)程安全的。
需要注意的是,所有原子操作方法的被操作數(shù)形參必須是指針類(lèi)型,通過(guò)指針變量可以獲取被操作數(shù)在內(nèi)存中的地址,從而施加特殊的CPU指令,確保同一時(shí)間只有一個(gè)goroutine能夠進(jìn)行操作。
上面的例子除了增加操作外我們還演示了載入操作,接下來(lái)我們來(lái)看一下CAS操作。
比較并交換
該操作簡(jiǎn)稱(chēng)CAS (Compare And Swap)。 這類(lèi)操作的前綴為 CompareAndSwap :
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
該操作在進(jìn)行交換前首先確保被操作數(shù)的值未被更改,即仍然保存著參數(shù) old 所記錄的值,滿(mǎn)足此前提條件下才進(jìn)行交換操作。CAS的做法類(lèi)似操作數(shù)據(jù)庫(kù)時(shí)常見(jiàn)的樂(lè)觀(guān)鎖機(jī)制。
需要注意的是,當(dāng)有大量的goroutine 對(duì)變量進(jìn)行讀寫(xiě)操作時(shí),可能導(dǎo)致CAS操作無(wú)法成功,這時(shí)可以利用for循環(huán)多次嘗試。
上面我只列出了比較典型的int32和unsafe.Pointer類(lèi)型的CAS方法,主要是想說(shuō)除了讀數(shù)值類(lèi)型進(jìn)行比較交換,還支持對(duì)指針進(jìn)行比較交換。
unsafe.Pointer提供了繞過(guò)Go語(yǔ)言指針類(lèi)型限制的方法,unsafe指的并不是說(shuō)不安全,而是說(shuō)官方并不保證向后兼容。
// 定義一個(gè)struct類(lèi)型P
type P struct{ x, y, z int }
// 執(zhí)行類(lèi)型P的指針
var pP *P
func main() {
// 定義一個(gè)執(zhí)行unsafe.Pointer值的指針變量
var unsafe1 = (*unsafe.Pointer)(unsafe.Pointer(&pP))
// Old pointer
var sy P
// 為了演示效果先將unsafe1設(shè)置成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)境下進(jìn)行的CAS,只是為了演示效果,先把被操作數(shù)設(shè)置成了Old Pointer。
其實(shí)Mutex的底層實(shí)現(xiàn)也是依賴(lài)原子操作中的CAS實(shí)現(xiàn)的,原子操作的atomic包相當(dāng)于是sync包里的那些同步原語(yǔ)的實(shí)現(xiàn)依賴(lài)。
比如互斥鎖Mutex的結(jié)構(gòu)里有一個(gè)state字段,其是表示鎖狀態(tài)的狀態(tài)位。
type Mutex struct {
state int32
sema uint32
}
為了方便理解,我們?cè)谶@里將它的狀態(tài)定義為0和1,0代表目前該鎖空閑,1代表已被加鎖,以下是sync.Mutex中Lock方法的部分實(shí)現(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),通過(guò)CAS方法,判斷鎖此時(shí)的狀態(tài)是否空閑(m.state==0),是,則對(duì)其加鎖(mutexLocked常量的值為1)。
atomic.Value保證任意值的讀寫(xiě)安全
atomic包里提供了一套Store開(kāi)頭的方法,用來(lái)保證各種類(lèi)型變量的并發(fā)寫(xiě)安全,避免其他操作讀到了修改變量過(guò)程中的臟數(shù)據(jù)。
func StoreInt32(addr *int32, val int32) func StoreInt64(addr *int64, val int64) func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) ...
這些操作方法的定義與上面介紹的那些操作的方法類(lèi)似,我就不再演示怎么使用這些方法了。
值得一提的是如果你想要并發(fā)安全的設(shè)置一個(gè)結(jié)構(gòu)體的多個(gè)字段,除了把結(jié)構(gòu)體轉(zhuǎn)換為指針,通過(guò)StorePointer設(shè)置外,還可以使用atomic包后來(lái)引入的atomic.Value,它在底層為我們完成了從具體指針類(lèi)型到unsafe.Pointer之間的轉(zhuǎn)換。
有了atomic.Value后,它使得我們可以不依賴(lài)于不保證兼容性的unsafe.Pointer類(lèi)型,同時(shí)又能將任意數(shù)據(jù)類(lèi)型的讀寫(xiě)操作封裝成原子性操作(中間狀態(tài)對(duì)外不可見(jiàn))。
atomic.Value類(lèi)型對(duì)外暴露了兩個(gè)方法:
- v.Store(c) - 寫(xiě)操作,將原始的變量c存放到一個(gè)atomic.Value類(lèi)型的v里。
- c := v.Load() - 讀操作,從線(xiàn)程安全的v中讀取上一步存放的內(nèi)容。
1.17 版本我看還增加了Swap和CompareAndSwap方法。
簡(jiǎn)潔的接口使得它的使用也很簡(jiǎn)單,只需將需要做并發(fā)保護(hù)的變量讀取和賦值操作用Load()和Store()代替就行了。
由于Load()返回的是一個(gè)interface{}類(lèi)型,所以在使用前我們記得要先轉(zhuǎn)換成具體類(lèi)型的值,再使用。下面是一個(gè)簡(jiǎ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 個(gè)協(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類(lèi)型的指針變量賦值,看看在并發(fā)條件下,兩個(gè)字段的值是不是能跟預(yù)期的一樣變成10和15。
總結(jié)
本文詳細(xì)介紹了Go語(yǔ)言原子操作atomic包中會(huì)被高頻使用的操作的使用場(chǎng)景和用法,當(dāng)然我并沒(méi)有羅列atomic包里所有操作的用法,主要是考慮到有的用到的地方實(shí)在不多,或者是已經(jīng)被更好的方式替代,還有就是覺(jué)得確實(shí)沒(méi)必要,看完本文的內(nèi)容相信你已經(jīng)完全具備自行探索atomic包的能力了。
再?gòu)?qiáng)調(diào)一遍,原子操作由底層硬件支持,而鎖則由操作系統(tǒng)的調(diào)度器實(shí)現(xiàn)。鎖應(yīng)當(dāng)用來(lái)保護(hù)一段邏輯,對(duì)于一個(gè)變量更新的保護(hù),原子操作通常會(huì)更有效率,并且更能利用計(jì)算機(jī)多核的優(yōu)勢(shì),如果要更新的是一個(gè)復(fù)合對(duì)象,則應(yīng)當(dāng)使用atomic.Value封裝好的實(shí)現(xiàn)。
到此這篇關(guān)于詳解Golang五種原子性操作的用法的文章就介紹到這了,更多相關(guān)詳解Golang五種原子性操作的用法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang仿ps實(shí)現(xiàn)獲取Linux進(jìn)程信息
這篇文章主要為大家學(xué)習(xí)介紹了Golang如何仿ps實(shí)現(xiàn)獲取Linux進(jìn)程信息,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-07-07
golang結(jié)構(gòu)化日志slog的用法簡(jiǎn)介
日志是任何軟件的重要組成部分,Go?提供了一個(gè)內(nèi)置日志包(slog),在本文中,小編將簡(jiǎn)單介紹一下slog包的功能以及如何在?Go?應(yīng)用程序中使用它,感興趣的可以了解下2023-09-09
詳解Go?flag實(shí)現(xiàn)二級(jí)子命令的方法
這篇文章主要介紹了Go?flag?詳解,實(shí)現(xiàn)二級(jí)子命令,本文就探討一下?Go?語(yǔ)言中如何寫(xiě)一個(gè)擁有類(lèi)似特性的命令行程序,需要的朋友可以參考下2022-07-07
Go語(yǔ)言{}大括號(hào)的特殊用法實(shí)例探究
這篇文章主要為大家介紹了Go語(yǔ)言{}大括號(hào)的特殊用法實(shí)例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
Bililive-go 實(shí)現(xiàn)直播自動(dòng)監(jiān)控錄制功能
最近有直播錄制的需求,但是自己手動(dòng)錄制太麻煩繁瑣,于是用了開(kāi)源項(xiàng)目Bililive-go進(jìn)行全自動(dòng)監(jiān)控錄制,對(duì)Bililive-go 直播自動(dòng)監(jiān)控錄制實(shí)現(xiàn)思路感興趣的朋友,一起看看吧2024-03-03
Go語(yǔ)言fmt.Sprintf格式化輸出的語(yǔ)法與實(shí)例
Go 可以使用 fmt.Sprintf 來(lái)格式化字符串,下面這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言fmt.Sprintf格式化輸出的語(yǔ)法與實(shí)例,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
Mac下Vs code配置Go語(yǔ)言環(huán)境的詳細(xì)過(guò)程
這篇文章給大家介紹Mac下Vs code配置Go語(yǔ)言環(huán)境的詳細(xì)過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-07-07

