一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

腳本之家,腳本語言編程技術及教程分享平臺!
分類導航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服務器之家 - 腳本之家 - Golang - Go 的 Atomic.Value 為什么不加鎖也能保證數據線程安全?

Go 的 Atomic.Value 為什么不加鎖也能保證數據線程安全?

2021-11-12 22:42網管叨bi叨網管 Golang

本文由淺入深的介紹了atomic.Value的使用姿勢,以及內部實現。讓大家不僅知其然,還能知其所以然。

Go 的 Atomic.Value 為什么不加鎖也能保證數據線程安全?

有些朋友可能沒有注意過,在 Go(甚至是大部分語言)中,一條普通的賦值語句其實不是一個原子操作。例如,在32位機器上寫int64類型的變量就會有中間狀態,因為它會被拆成兩次寫操作(匯編的MOV指令)——寫低 32 位和寫高 32 位,如下圖所示:

Go 的 Atomic.Value 為什么不加鎖也能保證數據線程安全?

32機器上對int64進行賦值

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

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

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

之前我在文章Golang 五種原子性操作的用法詳解里,詳細介紹過它的用法,下面我們先來快速回顧一下atomic.Value的使用方式

atomic.Value的使用方式

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

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

下面是一個簡單的例子演示atomic.Value的用法。

  1. type Rectangle struct {
  2. length int
  3. width int
  4. }
  5. var rect atomic.Value
  6. func update(width, length int) {
  7. rectLocal := new(Rectangle)
  8. rectLocal.width = width
  9. rectLocal.length = length
  10. rect.Store(rectLocal)
  11. }
  12. func main() {
  13. wg := sync.WaitGroup{}
  14. wg.Add(10)
  15. // 10 個協程并發更新
  16. for i := 0; i < 10; i++ {
  17. go func() {
  18. defer wg.Done()
  19. update(i, i+5)
  20. }()
  21. }
  22. wg.Wait()
  23. _r := rect.Load().(*Rectangle)
  24. fmt.Printf("rect.width=%d\nrect.length=%d\n", _r.width, _r.length)
  25. }

你也可以試試,不用atomic.Value,直接給Rectange類型的指針變量賦值,對比一下兩者結果的區別。

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

atomic.Value的內部實現

atomic.Value被設計用來存儲任意類型的數據,所以它內部的字段是一個interface{}類型。

  1. type Value struct {
  2. v interface{}
  3. }

除了Value外,atomic包內部定義了一個ifaceWords類型,這其實是interface{}的內部表示 (runtime.eface),它的作用是將interface{}類型分解,得到其原始類型(typ)和真正的值(data)。

  1. // ifaceWords is interface{} internal representation.
  2. type ifaceWords struct {
  3. typ unsafe.Pointer
  4. data unsafe.Pointer
  5. }

寫入線程安全的保證

在介紹寫入之前,我們先來看一下 Go 語言內部的unsafe.Pointer類型。

unsafe.Pointer

出于安全考慮,Go 語言并不支持直接操作內存,但它的標準庫中又提供一種不安全(不保證向后兼容性) 的指針類型unsafe.Pointer,讓程序可以靈活的操作內存。

unsafe.Pointer的特別之處在于,它可以繞過 Go 語言類型系統的檢查,與任意的指針類型互相轉換。也就是說,如果兩種類型具有相同的內存結構(layout),我們可以將unsafe.Pointer當做橋梁,讓這兩種類型的指針相互轉換,從而實現同一份內存擁有兩種不同的解讀方式。

比如說,[]byte和string其實內部的存儲結構都是一樣的,他們在運行時類型分別表示為reflect.SliceHeader和reflect.StringHeader

  1. type SliceHeader struct {
  2. Data uintptr
  3. Len int
  4. Cap int
  5. }
  6. type StringHeader struct {
  7. Data uintptr
  8. Len int
  9. }

但 Go 語言的類型系統禁止他倆互換。如果借助unsafe.Pointer,我們就可以實現在零拷貝的情況下,將[]byte數組直接轉換成string類型。

  1. bytes := []byte{104, 101, 108, 108, 111}
  2. p := unsafe.Pointer(&bytes) //將 *[]byte 指針強制轉換成unsafe.Pointer
  3. str := *(*string)(p) //將 unsafe.Pointer再轉換成string類型的指針,再將這個指針的值當做string類型取出來
  4. fmt.Println(str) //輸出 "hello"

知道了unsafe.Pointer的作用,我們可以直接來看代碼了:

  1. func (v *Value) Store(x interface{}) {
  2. if x == nil {
  3. panic("sync/atomic: store of nil value into Value")
  4. }
  5. vp := (*ifaceWords)(unsafe.Pointer(v)) // Old value
  6. xp := (*ifaceWords)(unsafe.Pointer(&x)) // New value
  7. for {
  8. typ := LoadPointer(&vp.typ)
  9. if typ == nil {
  10. // Attempt to start first store.
  11. // Disable preemption so that other goroutines can use
  12. // active spin wait to wait for completion; and so that
  13. // GC does not see the fake type accidentally.
  14. runtime_procPin()
  15. if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
  16. runtime_procUnpin()
  17. continue
  18. }
  19. // Complete first store.
  20. StorePointer(&vp.data, xp.data)
  21. StorePointer(&vp.typ, xp.typ)
  22. runtime_procUnpin()
  23. return
  24. }
  25. if uintptr(typ) == ^uintptr(0) {
  26. // First store in progress. Wait.
  27. // Since we disable preemption around the first store,
  28. // we can wait with active spinning.
  29. continue
  30. }
  31. // First store completed. Check type and overwrite data.
  32. if typ != xp.typ {
  33. panic("sync/atomic: store of inconsistently typed value into Value")
  34. }
  35. StorePointer(&vp.data, xp.data)
  36. return
  37. }
  38. }

大概的邏輯:

  • 通過unsafe.Pointer將現有的和要寫入的值分別轉成ifaceWords類型,這樣我們下一步就可以得到這兩個interface{}的原始類型(typ)和真正的值(data)。
  • 開始就是一個無限 for 循環。配合CompareAndSwap使用,可以達到樂觀鎖的效果。
  • 通過LoadPointer這個原子操作拿到當前Value中存儲的類型。下面根據這個類型的不同,分3種情況處理。

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

  • runtime_procPin()這是runtime中的一段函數,一方面它禁止了調度器對當前 goroutine 的搶占(preemption),使得它在執行當前邏輯的時候不被打斷,以便可以盡快地完成工作,因為別人一直在等待它。另一方面,在禁止搶占期間,GC 線程也無法被啟用,這樣可以防止 GC 線程看到一個莫名其妙的指向^uintptr(0)的類型(這是賦值過程中的中間狀態)。
  • 使用CAS操作,先嘗試將typ設置為^uintptr(0)這個中間狀態。如果失敗,則證明已經有別的線程搶先完成了賦值操作,那它就解除搶占鎖,然后重新回到 for 循環第一步。
  • 如果設置成功,那證明當前線程搶到了這個"樂觀鎖”,它可以安全的把v設為傳入的新值了。注意,這里是先寫data字段,然后再寫typ字段。因為我們是以typ字段的值作為寫入完成與否的判斷依據的。

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

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

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

讀取(Load)操作

先上代碼:

  1. func (v *Value) Load() (x interface{}) {
  2. vp := (*ifaceWords)(unsafe.Pointer(v))
  3. typ := LoadPointer(&vp.typ)
  4. if typ == nil || uintptr(typ) == ^uintptr(0) {
  5. // First store not yet completed.
  6. return nil
  7. }
  8. data := LoadPointer(&vp.data)
  9. xp := (*ifaceWords)(unsafe.Pointer(&x))
  10. xp.typ = typ
  11. xp.data = data
  12. return
  13. }

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

如果當前的typ是 nil 或者^uintptr(0),那就證明第一次寫入還沒有開始,或者還沒完成,那就直接返回 nil (不對外暴露中間狀態)。

否則,根據當前看到的typ和data構造出一個新的interface{}返回出去。

總結

本文由淺入深的介紹了atomic.Value的使用姿勢,以及內部實現。讓大家不僅知其然,還能知其所以然。

另外,原子操作由底層硬件支持,對于一個變量更新的保護,原子操作通常會更有效率,并且更能利用計算機多核的優勢,如果要更新的是一個復合對象,則應當使用atomic.Value封裝好的實現。

而我們做并發同步控制常用到的Mutex鎖,則是由操作系統的調度器實現,鎖應當用來保護一段邏輯。

原文鏈接:https://mp.weixin.qq.com/s/GFJO03DFNy8O3HcMeEhT6g

延伸 · 閱讀

精彩推薦
  • Golanggolang json.Marshal 特殊html字符被轉義的解決方法

    golang json.Marshal 特殊html字符被轉義的解決方法

    今天小編就為大家分享一篇golang json.Marshal 特殊html字符被轉義的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧 ...

    李浩的life12792020-05-27
  • Golanggolang的httpserver優雅重啟方法詳解

    golang的httpserver優雅重啟方法詳解

    這篇文章主要給大家介紹了關于golang的httpserver優雅重啟的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,...

    helight2992020-05-14
  • GolangGolang通脈之數據類型詳情

    Golang通脈之數據類型詳情

    這篇文章主要介紹了Golang通脈之數據類型,在編程語言中標識符就是定義的具有某種意義的詞,比如變量名、常量名、函數名等等,Go語言中標識符允許由...

    4272021-11-24
  • Golanggo日志系統logrus顯示文件和行號的操作

    go日志系統logrus顯示文件和行號的操作

    這篇文章主要介紹了go日志系統logrus顯示文件和行號的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧...

    SmallQinYan12302021-02-02
  • GolangGolang中Bit數組的實現方式

    Golang中Bit數組的實現方式

    這篇文章主要介紹了Golang中Bit數組的實現方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧...

    天易獨尊11682021-06-09
  • Golanggolang如何使用struct的tag屬性的詳細介紹

    golang如何使用struct的tag屬性的詳細介紹

    這篇文章主要介紹了golang如何使用struct的tag屬性的詳細介紹,從例子說起,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看...

    Go語言中文網11352020-05-21
  • Golanggo語言制作端口掃描器

    go語言制作端口掃描器

    本文給大家分享的是使用go語言編寫的TCP端口掃描器,可以選擇IP范圍,掃描的端口,以及多線程,有需要的小伙伴可以參考下。 ...

    腳本之家3642020-04-25
  • Golanggolang 通過ssh代理連接mysql的操作

    golang 通過ssh代理連接mysql的操作

    這篇文章主要介紹了golang 通過ssh代理連接mysql的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧...

    a165861639710342021-03-08
主站蜘蛛池模板: 九九影院午夜理论片无码 | 国产精品露脸国语对白河北 | 91传媒制片厂果冻有限公司 | 色先锋av资源中文字幕 | 国产精品久久久久久久久 | 近亲乱中文字幕 | 非洲黑人女bbwxxxx | 太紧太深了受不了黑人 | 国产区最新| 久久青青草原 | 国产尤物精品视频 | 万域之王动漫在线观看全集免费播放 | 久久精品一卡二卡三卡四卡视频版 | 免费午夜剧场 | 日韩一级精品视频在线观看 | 国产精品久久久精品视频 | boobsmilking流奶水野战 | 色综合中文字幕在线亚洲 | 国产新疆成人a一片在线观看 | 超级乱淫伦短篇小说做车 | 日本xxxⅹ69xxxx护士 | 四虎精品成人a在线观看 | 久青草国产在线观看视频 | 国产一区二区视频在线 | 二次元美女脱裤子让男人桶爽 | 1024国产基地永久免费 | 欧美日韩高清观看一区二区 | 超级乱淫伦小说1女多男 | 日韩久久综合 | 天天视频国产精品 | 亚洲精品久久久成人 | 果冻传媒天美传媒乌鸦传媒 | 国内精品伊人久久大香线焦 | 日韩中文字幕网站 | 亚洲香蕉视频 | 欧美亚洲高清日韩成人 | 九九精品99久久久香蕉 | 操一操影院 | 无套大战白嫩乌克兰美女 | 国产精品极品美女自在线 | 熟睡中的麻麻大白屁股小说 |