Go語(yǔ)言atomic.Value如何不加鎖保證數(shù)據(jù)線程安全?
引言
很多人可能沒(méi)有注意過(guò),在 Go(甚至是大部分語(yǔ)言)中,一條普通的賦值語(yǔ)句其實(shí)不是一個(gè)原子操作。例如,在32位機(jī)器上寫(xiě)int64類型的變量就會(huì)有中間狀態(tài),它會(huì)被拆成兩次寫(xiě)操作(匯編的MOV指令)——寫(xiě)低 32 位和寫(xiě)高 32 位。32機(jī)器上對(duì)int64進(jìn)行賦值
如果一個(gè)線程剛寫(xiě)完低32位,還沒(méi)來(lái)得及寫(xiě)高32位時(shí),另一個(gè)線程讀取了這個(gè)變量,那它得到的就是一個(gè)毫無(wú)邏輯的中間變量,這很有可能使我們的程序出現(xiàn)Bug。
這還只是一個(gè)基礎(chǔ)類型,如果我們對(duì)一個(gè)結(jié)構(gòu)體進(jìn)行賦值,那它出現(xiàn)并發(fā)問(wèn)題的概率就更高了。很可能寫(xiě)線程剛寫(xiě)完一小半的字段,讀線程就來(lái)讀取這個(gè)變量,那么就只能讀到僅修改了一部分的值。這顯然破壞了變量的完整性,讀出來(lái)的值也是完全錯(cuò)誤的。
面對(duì)這種多線程下變量的讀寫(xiě)問(wèn)題,Go給出的解決方案是atomic.Value,它使得我們可以不依賴于不保證兼容性的unsafe.Pointer類型,同時(shí)又能將任意數(shù)據(jù)類型的讀寫(xiě)操作封裝成原子性操作。
atomic.Value的使用方式
atomic.Value類型對(duì)外提供了兩個(gè)讀寫(xiě)方法:
v.Store(c)- 寫(xiě)操作,將原始的變量c存放到一個(gè)atomic.Value類型的v里。c := v.Load()- 讀操作,從內(nèi)存中線程安全的v中讀取上一步存放的內(nè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(i int) {
defer wg.Done()
update(i, i+5)
}(i)
}
wg.Wait()
r := rect.Load().(*Rectangle)
fmt.Printf("rect.width=%d\nrect.length=%d\n", r.width, r.length)
}你可能會(huì)好奇,為什么atomic.Value在不加鎖的情況下就提供了讀寫(xiě)變量的線程安全保證,接下來(lái)我們就一起看看其內(nèi)部實(shí)現(xiàn)。
atomic.Value的內(nèi)部實(shí)現(xiàn)
atomic.Value被設(shè)計(jì)用來(lái)存儲(chǔ)任意類型的數(shù)據(jù),所以它內(nèi)部的字段是一個(gè)interface{}類型。
// 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 interface{}
}除了Value外,atomic包內(nèi)部定義了一個(gè)ifaceWords類型,這其實(shí)是interface{}的內(nèi)部表示 (runtime.eface),它的作用是將interface{}類型分解,得到其原始類型(typ)和真正的值(data)。
// ifaceWords is interface{} internal representation.
type ifaceWords struct {
typ unsafe.Pointer
data unsafe.Pointer
}寫(xiě)入線程安全的保證
直接來(lái)看代碼
// Store sets the value of the Value to x.
// All calls to Store for a given Value must use values of the same concrete type.
// Store of an inconsistent type panics, as does Store(nil).
func (v *Value) Store(val interface{}) {
if val == nil {
panic("sync/atomic: store of nil value into Value")
}
// 通過(guò)unsafe.Pointer將現(xiàn)有的(v)和要寫(xiě)入的值(val) 分別轉(zhuǎn)成ifaceWords類型。
// 這樣我們下一步就可以得到這兩個(gè)interface{}的原始類型(typ)和真正的值(data)。
vp := (*ifaceWords)(unsafe.Pointer(v))
vlp := (*ifaceWords)(unsafe.Pointer(&val))
for {
typ := LoadPointer(&vp.typ)
if typ == nil {
// Attempt to start first store.
// Disable preemption so that other goroutines can use
// active spin wait to wait for completion; and so that
// GC does not see the fake type accidentally.
runtime_procPin()
if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
runtime_procUnpin()
continue
}
// Complete first store.
StorePointer(&vp.data, vlp.data)
StorePointer(&vp.typ, vlp.typ)
runtime_procUnpin()
return
}
if uintptr(typ) == ^uintptr(0) {
// First store in progress. Wait.
// Since we disable preemption around the first store,
// we can wait with active spinning.
continue
}
// First store completed. Check type and overwrite data.
if typ != vlp.typ {
panic("sync/atomic: store of inconsistently typed value into Value")
}
StorePointer(&vp.data, vlp.data)
return
}
}大概的邏輯:
- 開(kāi)始就是一個(gè)無(wú)限 for 循環(huán)。配合
CompareAndSwap使用,可以達(dá)到樂(lè)觀鎖的效果。 - 通過(guò)
LoadPointer這個(gè)原子操作拿到當(dāng)前Value中存儲(chǔ)的類型。下面根據(jù)這個(gè)類型的不同,分3種情況處理。
第一次寫(xiě)入
一個(gè)
atomic.Value實(shí)例被初始化后,它的typ和data字段會(huì)被設(shè)置為指針的零值 nil,所以先判斷如果typ是否為nil,如果是那就證明這個(gè)Value實(shí)例還未被寫(xiě)入過(guò)數(shù)據(jù)。那之后就是一段初始寫(xiě)入的操作:runtime_procPin()這是runtime中的一段函數(shù),一方面它禁止了調(diào)度器對(duì)當(dāng)前 goroutine 的搶占(preemption),使得它在執(zhí)行當(dāng)前邏輯的時(shí)候不被其他goroutine打斷,以便可以盡快地完成工作。另一方面,在禁止搶占期間,GC 線程也無(wú)法被啟用,這樣可以防止 GC 線程看到一個(gè)莫名其妙的指向^uintptr(0)的類型(這是賦值過(guò)程中的中間狀態(tài))。1)使用
CAS操作,先嘗試將typ設(shè)置為^uintptr(0)這個(gè)中間狀態(tài)。如果失敗,則證明已經(jīng)有別的線程搶先完成了賦值操作,那它就解除搶占鎖,然后重新回到 for 循環(huán)第一步。2)如果設(shè)置成功,那證明當(dāng)前線程搶到了這個(gè)"樂(lè)觀鎖”,它可以安全的把
v設(shè)為傳入的新值了。注意,這里是先寫(xiě)data字段,然后再寫(xiě)typ字段。因?yàn)槲覀兪且?code>typ字段的值作為寫(xiě)入完成與否的判斷依據(jù)的第一次寫(xiě)入還未完成
如果看到
typ字段還是^uintptr(0)這個(gè)中間類型,證明剛剛的第一次寫(xiě)入還沒(méi)有完成,所以它會(huì)繼續(xù)循環(huán),一直等到第一次寫(xiě)入完成。第一次寫(xiě)入已完成
首先檢查上一次寫(xiě)入的類型與這一次要寫(xiě)入的類型是否一致,如果不一致則拋出異常。反之,則直接把這一次要寫(xiě)入的值寫(xiě)入到
data字段。
這個(gè)邏輯的主要思想就是,為了完成多個(gè)字段的原子性寫(xiě)入,我們可以抓住其中的一個(gè)字段,以它的狀態(tài)來(lái)標(biāo)志整個(gè)原子寫(xiě)入的狀態(tài)。
讀?。↙oad)操作
先上代碼:
// Load returns the value set by the most recent Store.
// It returns nil if there has been no call to Store for this Value.
func (v *Value) Load() (val interface{}) {
vp := (*ifaceWords)(unsafe.Pointer(v))
typ := LoadPointer(&vp.typ)
if typ == nil || uintptr(typ) == ^uintptr(0) {
// First store not yet completed.
return nil
}
data := LoadPointer(&vp.data)
vlp := (*ifaceWords)(unsafe.Pointer(&val))
vlp.typ = typ
vlp.data = data
return
}讀取相對(duì)就簡(jiǎn)單很多了,它有兩個(gè)分支:
- 如果當(dāng)前的
typ是 nil 或者^uintptr(0),那就證明第一次寫(xiě)入還沒(méi)有開(kāi)始,或者還沒(méi)完成,那就直接返回 nil (不對(duì)外暴露中間狀態(tài))。 - 否則,根據(jù)當(dāng)前看到的
typ和data構(gòu)造出一個(gè)新的interface{}返回出去。
總結(jié)
本文由淺入深的介紹了atomic.Value的使用姿勢(shì),以及內(nèi)部實(shí)現(xiàn)。另外,原子操作由底層硬件支持,對(duì)于一個(gè)變量更新的保護(hù),原子操作通常會(huì)更有效率,并且更能利用計(jì)算機(jī)多核的優(yōu)勢(shì),如果要更新的是一個(gè)復(fù)合對(duì)象,則應(yīng)當(dāng)使用atomic.Value封裝好的實(shí)現(xiàn)。
而我們做并發(fā)同步控制常用到的Mutex鎖,則是由操作系統(tǒng)的調(diào)度器實(shí)現(xiàn),鎖應(yīng)當(dāng)用來(lái)保護(hù)一段邏輯。
以上就是Go語(yǔ)言atomic.Value如何不加鎖保證數(shù)據(jù)線程安全?的詳細(xì)內(nèi)容,更多關(guān)于Go語(yǔ)言atomic.Value如何不加鎖保證數(shù)據(jù)線程安全?的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言程序開(kāi)發(fā)gRPC服務(wù)
這篇文章主要為大家介紹了Go語(yǔ)言程序開(kāi)發(fā)gRPC服務(wù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Go語(yǔ)言數(shù)據(jù)結(jié)構(gòu)之二叉樹(shù)可視化詳解
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言數(shù)據(jù)結(jié)構(gòu)中二叉樹(shù)可視化的方法詳解,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-09-09
Golang命令行進(jìn)行debug調(diào)試操作
今天小編就為大家分享一篇關(guān)于,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-04-04

