欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go語言atomic.Value如何不加鎖保證數(shù)據(jù)線程安全?

 更新時間:2023年05月12日 09:47:00   作者:心如花木向陽生  
這篇文章主要介紹了Go語言atomic.Value如何不加鎖保證數(shù)據(jù)線程安全詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

很多人可能沒有注意過,在 Go(甚至是大部分語言)中,一條普通的賦值語句其實不是一個原子操作。例如,在32位機器上寫int64類型的變量就會有中間狀態(tài),它會被拆成兩次寫操作(匯編的MOV指令)——寫低 32 位和寫高 32 位。32機器上對int64進行賦值

如果一個線程剛寫完低32位,還沒來得及寫高32位時,另一個線程讀取了這個變量,那它得到的就是一個毫無邏輯的中間變量,這很有可能使我們的程序出現(xiàn)Bug。

這還只是一個基礎(chǔ)類型,如果我們對一個結(jié)構(gòu)體進行賦值,那它出現(xiàn)并發(fā)問題的概率就更高了。很可能寫線程剛寫完一小半的字段,讀線程就來讀取這個變量,那么就只能讀到僅修改了一部分的值。這顯然破壞了變量的完整性,讀出來的值也是完全錯誤的。

面對這種多線程下變量的讀寫問題,Go給出的解決方案是atomic.Value,它使得我們可以不依賴于不保證兼容性的unsafe.Pointer類型,同時又能將任意數(shù)據(jù)類型的讀寫操作封裝成原子性操作。

atomic.Value的使用方式

atomic.Value類型對外提供了兩個讀寫方法:

  • v.Store(c) - 寫操作,將原始的變量c存放到一個atomic.Value類型的v里。
  • c := v.Load() - 讀操作,從內(nèi)存中線程安全的v中讀取上一步存放的內(nèi)容。

下面是一個簡單的例子演示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 個協(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)
}

你可能會好奇,為什么atomic.Value在不加鎖的情況下就提供了讀寫變量的線程安全保證,接下來我們就一起看看其內(nèi)部實現(xiàn)。

atomic.Value的內(nèi)部實現(xiàn)

atomic.Value被設(shè)計用來存儲任意類型的數(shù)據(jù),所以它內(nèi)部的字段是一個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)部定義了一個ifaceWords類型,這其實是interface{}的內(nèi)部表示 (runtime.eface),它的作用是將interface{}類型分解,得到其原始類型(typ)和真正的值(data)。

// ifaceWords is interface{} internal representation.
type ifaceWords struct {
	typ  unsafe.Pointer
	data unsafe.Pointer
}

寫入線程安全的保證

直接來看代碼

// 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")
	}
    // 通過unsafe.Pointer將現(xiàn)有的(v)和要寫入的值(val) 分別轉(zhuǎn)成ifaceWords類型。
    // 這樣我們下一步就可以得到這兩個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
	}
}

大概的邏輯:

  • 開始就是一個無限 for 循環(huán)。配合CompareAndSwap使用,可以達到樂觀鎖的效果。
  • 通過LoadPointer這個原子操作拿到當前Value中存儲的類型。下面根據(jù)這個類型的不同,分3種情況處理。
  • 第一次寫入

    一個atomic.Value實例被初始化后,它的typdata字段會被設(shè)置為指針的零值 nil,所以先判斷如果typ是否為nil,如果是那就證明這個Value實例還未被寫入過數(shù)據(jù)。那之后就是一段初始寫入的操作:

  • runtime_procPin()這是runtime中的一段函數(shù),一方面它禁止了調(diào)度器對當前 goroutine 的搶占(preemption),使得它在執(zhí)行當前邏輯的時候不被其他goroutine打斷,以便可以盡快地完成工作。另一方面,在禁止搶占期間,GC 線程也無法被啟用,這樣可以防止 GC 線程看到一個莫名其妙的指向^uintptr(0)的類型(這是賦值過程中的中間狀態(tài))。

    1)使用CAS操作,先嘗試將typ設(shè)置為^uintptr(0)這個中間狀態(tài)。如果失敗,則證明已經(jīng)有別的線程搶先完成了賦值操作,那它就解除搶占鎖,然后重新回到 for 循環(huán)第一步。

    2)如果設(shè)置成功,那證明當前線程搶到了這個"樂觀鎖”,它可以安全的把v設(shè)為傳入的新值了。注意,這里是先寫data字段,然后再寫typ字段。因為我們是以typ字段的值作為寫入完成與否的判斷依據(jù)的

  • 第一次寫入還未完成

    如果看到typ字段還是^uintptr(0)這個中間類型,證明剛剛的第一次寫入還沒有完成,所以它會繼續(xù)循環(huán),一直等到第一次寫入完成。

  • 第一次寫入已完成

    首先檢查上一次寫入的類型與這一次要寫入的類型是否一致,如果不一致則拋出異常。反之,則直接把這一次要寫入的值寫入到data字段。

這個邏輯的主要思想就是,為了完成多個字段的原子性寫入,我們可以抓住其中的一個字段,以它的狀態(tài)來標志整個原子寫入的狀態(tài)。

讀取(Load)操作

先上代碼:

// 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
}

讀取相對就簡單很多了,它有兩個分支:

  • 如果當前的typ是 nil 或者^uintptr(0),那就證明第一次寫入還沒有開始,或者還沒完成,那就直接返回 nil (不對外暴露中間狀態(tài))。
  • 否則,根據(jù)當前看到的typdata構(gòu)造出一個新的interface{}返回出去。

總結(jié)

本文由淺入深的介紹了atomic.Value的使用姿勢,以及內(nèi)部實現(xiàn)。另外,原子操作由底層硬件支持,對于一個變量更新的保護,原子操作通常會更有效率,并且更能利用計算機多核的優(yōu)勢,如果要更新的是一個復(fù)合對象,則應(yīng)當使用atomic.Value封裝好的實現(xiàn)。

而我們做并發(fā)同步控制常用到的Mutex鎖,則是由操作系統(tǒng)的調(diào)度器實現(xiàn),鎖應(yīng)當用來保護一段邏輯。

以上就是Go語言atomic.Value如何不加鎖保證數(shù)據(jù)線程安全?的詳細內(nèi)容,更多關(guān)于Go語言atomic.Value如何不加鎖保證數(shù)據(jù)線程安全?的資料請關(guān)注腳本之家其它相關(guān)文章!

您可能感興趣的文章:

相關(guān)文章

  • Go讀寫鎖操作方法示例詳解

    Go讀寫鎖操作方法示例詳解

    這篇文章主要為大家介紹了Go讀寫鎖方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-07-07
  • 一文了解Go 并發(fā)與并行

    一文了解Go 并發(fā)與并行

    并發(fā)性和并行性是是兩個既有聯(lián)系又有所區(qū)別的概念,本文主要介紹了Go并發(fā)與并行,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-05-05
  • 淺析Go中原子操作的重要性與使用

    淺析Go中原子操作的重要性與使用

    這篇文章主要帶大家一起探索?Go?中原子操作的概念,了解為什么它們是重要的,以及如何有效地使用它們,文中的示例代碼講解詳細,需要的可以了解下
    2023-11-11
  • golang?gorm更新日志執(zhí)行SQL示例詳解

    golang?gorm更新日志執(zhí)行SQL示例詳解

    這篇文章主要為大家介紹了golang?gorm更新日志執(zhí)行SQL示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪
    2022-04-04
  • Go1.21新增slices包的用法詳解

    Go1.21新增slices包的用法詳解

    Go?1.21新增的?slices?包提供了很多和切片相關(guān)的函數(shù),可以用于任何類型的切片,這篇文章主要來和大家介紹一下slices包中相關(guān)函數(shù)的用法,需要的可以參考一下
    2023-08-08
  • Go語言程序開發(fā)gRPC服務(wù)

    Go語言程序開發(fā)gRPC服務(wù)

    這篇文章主要為大家介紹了Go語言程序開發(fā)gRPC服務(wù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-06-06
  • Go語言數(shù)據(jù)結(jié)構(gòu)之二叉樹可視化詳解

    Go語言數(shù)據(jù)結(jié)構(gòu)之二叉樹可視化詳解

    這篇文章主要為大家詳細介紹了Go語言數(shù)據(jù)結(jié)構(gòu)中二叉樹可視化的方法詳解,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2022-09-09
  • Golang命令行進行debug調(diào)試操作

    Golang命令行進行debug調(diào)試操作

    今天小編就為大家分享一篇關(guān)于,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-04-04
  • golang中defer的使用規(guī)則詳解

    golang中defer的使用規(guī)則詳解

    大家應(yīng)該都知道在golang當中,defer代碼塊會在函數(shù)調(diào)用鏈表中增加一個函數(shù)調(diào)用。下面這篇文章主要給大家介紹了關(guān)于golang中defer的使用規(guī)則,文中介紹的非常詳細,對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。
    2017-07-07
  • 一文詳解Go語言中切片的底層原理

    一文詳解Go語言中切片的底層原理

    在Go語言中,切片作為一種引用類型數(shù)據(jù),相對數(shù)組而言是一種動態(tài)長度的數(shù)據(jù)類型,使用的場景也是非常多,所以本文主要來和大家聊聊切片的底層原理,需要的可以參考一下
    2023-06-06

最新評論