<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為什么 Go map 和 slice 是非線性安全的?

          共 6295字,需瀏覽 13分鐘

           ·

          2021-07-21 16:27

          大家好,我是煎魚(yú)。

          初入 Go 語(yǔ)言的大門(mén),有不少的小伙伴會(huì)快速的 3 天精通 Go,5 天上手項(xiàng)目,14 天上線業(yè)務(wù)迭代,21 天排查、定位問(wèn)題,順帶捎個(gè)反省報(bào)告。

          其中最常見(jiàn)的初級(jí)錯(cuò)誤,Go 面試較最?lèi)?ài)問(wèn)的問(wèn)題之一:

          為什么在 Go 語(yǔ)言里,map 和 slice 不支持并發(fā)讀寫(xiě),也就是是非線性安全的,為什么不支持?

          見(jiàn)招拆招后,緊接著就會(huì)開(kāi)始討論如何讓他們倆 ”冤家“ 支持并發(fā)讀寫(xiě)?

          今天我們這篇文章就來(lái)理一理,了解其前因后果,一起吸魚(yú)學(xué)懂 Go 語(yǔ)言。

          非線性安全的例子

          slice

          我們使用多個(gè) goroutine 對(duì)類(lèi)型為 slice 的變量進(jìn)行操作,看看結(jié)果會(huì)變的怎么樣。

          如下:

          func main() {
           var s []string
           for i := 0; i < 9999; i++ {
            go func() {
             s = append(s, "腦子進(jìn)煎魚(yú)了")
            }()
           }

           fmt.Printf("進(jìn)了 %d 只煎魚(yú)"len(s))
          }

          輸出結(jié)果:

          // 第一次執(zhí)行
          進(jìn)了 5790 只煎魚(yú)
          // 第二次執(zhí)行
          進(jìn)了 7370 只煎魚(yú)
          // 第三次執(zhí)行
          進(jìn)了 6792 只煎魚(yú)

          你會(huì)發(fā)現(xiàn)無(wú)論你執(zhí)行多少次,每次輸出的值大概率都不會(huì)一樣。也就是追加進(jìn) slice 的值,出現(xiàn)了覆蓋的情況。

          因此在循環(huán)中所追加的數(shù)量,與最終的值并不相等。且這種情況,是不會(huì)報(bào)錯(cuò)的,是一個(gè)出現(xiàn)率不算高的隱式問(wèn)題。

          這個(gè)產(chǎn)生的主要原因是程序邏輯本身就有問(wèn)題,同時(shí)讀取到相同索引位,自然也就會(huì)產(chǎn)生覆蓋的寫(xiě)入了。

          map

          同樣針對(duì) map 也如法炮制一下。重復(fù)針對(duì)類(lèi)型為 map 的變量進(jìn)行寫(xiě)入。

          如下:

          func main() {
           s := make(map[string]string)
           for i := 0; i < 99; i++ {
            go func() {
             s["煎魚(yú)"] = "吸魚(yú)"
            }()
           }

           fmt.Printf("進(jìn)了 %d 只煎魚(yú)"len(s))
          }

          輸出結(jié)果:

          fatal error: concurrent map writes

          goroutine 18 [running]:
          runtime.throw(0x10cb861, 0x15)
                  /usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1117 +0x72 fp=0xc00002e738 sp=0xc00002e708 pc=0x1032472
          runtime.mapassign_faststr(0x10b3360, 0xc0000a2180, 0x10c91da, 0x6, 0x0)
                  /usr/local/Cellar/go/1.16.2/libexec/src/runtime/map_faststr.go:211 +0x3f1 fp=0xc00002e7a0 sp=0xc00002e738 pc=0x1011a71
          main.main.func1(0xc0000a2180)
                  /Users/eddycjy/go-application/awesomeProject/main.go:9 +0x4c fp=0xc00002e7d8 sp=0xc00002e7a0 pc=0x10a474c
          runtime.goexit()
                  /usr/local/Cellar/go/1.16.2/libexec/src/runtime/asm_amd64.s:1371 +0x1 fp=0xc00002e7e0 sp=0xc00002e7d8 pc=0x1063fe1
          created by main.main
                  /Users/eddycjy/go-application/awesomeProject/main.go:8 +0x55

          好家伙,程序運(yùn)行會(huì)直接報(bào)錯(cuò)。并且是 Go 源碼調(diào)用 throw 方法所導(dǎo)致的致命錯(cuò)誤,也就是說(shuō) Go 進(jìn)程會(huì)中斷。

          不得不說(shuō),這個(gè)并發(fā)寫(xiě) map 導(dǎo)致的 fatal error: concurrent map writes 錯(cuò)誤提示。我有一個(gè)朋友,已經(jīng)看過(guò)少說(shuō)幾十次了,不同組,不同人...

          是個(gè)日經(jīng)的隱式問(wèn)題。

          如何支持并發(fā)讀寫(xiě)

          對(duì) map 上鎖

          實(shí)際上我們?nèi)匀淮嬖诓l(fā)讀寫(xiě) map 的訴求(程序邏輯決定),因?yàn)?Go 語(yǔ)言中的 goroutine 實(shí)在是太方便了。

          像是一般寫(xiě)爬蟲(chóng)任務(wù)時(shí),基本會(huì)用到多個(gè) goroutine,獲取到數(shù)據(jù)后再寫(xiě)入到 map 或者 slice 中去。

          Go 官方在 Go maps in action 中提供了一種簡(jiǎn)單又便利的方式來(lái)實(shí)現(xiàn):

          var counter = struct{
              sync.RWMutex
              m map[string]int
          }{m: make(map[string]int)}

          這條語(yǔ)句聲明了一個(gè)變量,它是一個(gè)匿名結(jié)構(gòu)(struct)體,包含一個(gè)原生和一個(gè)嵌入讀寫(xiě)鎖 sync.RWMutex。

          要想從變量中中讀出數(shù)據(jù),則調(diào)用讀鎖:

          counter.RLock()
          n := counter.m["煎魚(yú)"]
          counter.RUnlock()
          fmt.Println("煎魚(yú):", n)

          要往變量中寫(xiě)數(shù)據(jù),則調(diào)用寫(xiě)鎖:

          counter.Lock()
          counter.m["煎魚(yú)"]++
          counter.Unlock()

          這就是一個(gè)最常見(jiàn)的 Map 支持并發(fā)讀寫(xiě)的方式了。

          sync.Map

          前言

          雖然有了 Map+Mutex 的極簡(jiǎn)方案,但是也仍然存在一定問(wèn)題。那就是在 map 的數(shù)據(jù)量非常大時(shí),只有一把鎖(Mutex)就非??膳铝?,一把鎖會(huì)導(dǎo)致大量的爭(zhēng)奪鎖,導(dǎo)致各種沖突和性能低下。

          常見(jiàn)的解決方案是分片化,將一個(gè)大 map 分成多個(gè)區(qū)間,各區(qū)間使用多個(gè)鎖,這樣子鎖的粒度就大大降低了。不過(guò)該方案實(shí)現(xiàn)起來(lái)很復(fù)雜,很容易出錯(cuò)。因此 Go 團(tuán)隊(duì)到比較為止暫無(wú)推薦,而是采取了其他方案。

          該方案就是在 Go1.9 起支持的 sync.Map,其支持并發(fā)讀寫(xiě) map,起到一個(gè)補(bǔ)充的作用。

          具體介紹

          Go 語(yǔ)言的 sync.Map 支持并發(fā)讀寫(xiě) map,采取了 “空間換時(shí)間” 的機(jī)制,冗余了兩個(gè)數(shù)據(jù)結(jié)構(gòu),分別是:read 和 dirty,減少加鎖對(duì)性能的影響:

          type Map struct {
           mu Mutex
           read atomic.Value // readOnly
           dirty map[interface{}]*entry
           misses int
          }

          其是專(zhuān)門(mén)為 append-only 場(chǎng)景設(shè)計(jì)的,也就是適合讀多寫(xiě)少的場(chǎng)景。這是他的優(yōu)點(diǎn)之一。

          若出現(xiàn)寫(xiě)多/并發(fā)多的場(chǎng)景,會(huì)導(dǎo)致 read map 緩存失效,需要加鎖,沖突變多,性能急劇下降。這是他的重大缺點(diǎn)。

          提供了以下常用方法:

          func (m *Map) Delete(key interface{})
          func (m *Map) Load(key interface{}) (value interface{}, ok bool)
          func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)
          func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
          func (m *Map) Range(f func(key, value interface{}) bool)
          func (m *Map) Store(key, value interface{})
          • Delete:刪除某一個(gè)鍵的值。
          • Load:返回存儲(chǔ)在 map 中的鍵的值,如果沒(méi)有值,則返回 nil。ok 結(jié)果表示是否在 map 中找到了值。
          • LoadAndDelete:刪除一個(gè)鍵的值,如果有的話返回之前的值。
          • LoadOrStore:如果存在的話,則返回鍵的現(xiàn)有值。否則,它存儲(chǔ)并返回給定的值。如果值被加載,加載的結(jié)果為 true,如果被存儲(chǔ),則為 false。
          • Range:遞歸調(diào)用,對(duì) map 中存在的每個(gè)鍵和值依次調(diào)用閉包函數(shù) f。如果 f 返回 false 就停止迭代。
          • Store:存儲(chǔ)并設(shè)置一個(gè)鍵的值。

          實(shí)際運(yùn)行例子如下:

          var m sync.Map

          func main() {
           //寫(xiě)入
           data := []string{"煎魚(yú)""咸魚(yú)""烤魚(yú)""蒸魚(yú)"}
           for i := 0; i < 4; i++ {
            go func(i int) {
             m.Store(i, data[i])
            }(i)
           }
           time.Sleep(time.Second)

           //讀取
           v, ok := m.Load(0)
           fmt.Printf("Load: %v, %v\n", v, ok)

           //刪除
           m.Delete(1)

           //讀或?qū)?/span>
           v, ok = m.LoadOrStore(1"吸魚(yú)")
           fmt.Printf("LoadOrStore: %v, %v\n", v, ok)

           //遍歷
           m.Range(func(key, value interface{}) bool {
            fmt.Printf("Range: %v, %v\n", key, value)
            return true
           })
          }

          輸出結(jié)果:

          Load: 煎魚(yú), true
          LoadOrStore: 吸魚(yú), false
          Range: 0, 煎魚(yú)
          Range: 1, 吸魚(yú)
          Range: 3, 蒸魚(yú)
          Range: 2, 烤魚(yú)

          為什么不支持

          Go Slice 的話,主要還是索引位覆寫(xiě)問(wèn)題,這個(gè)就不需要糾結(jié)了,勢(shì)必是程序邏輯在編寫(xiě)上有明顯缺陷,自行改之就好。

          但 Go map 就不大一樣了,很多人以為是默認(rèn)支持的,一個(gè)不小心就翻車(chē),這么的常見(jiàn)。那憑什么 Go 官方還不支持,難不成太復(fù)雜了,性能太差了,到底是為什么?

          原因如下(via @go faq):

          • 典型使用場(chǎng)景:map 的典型使用場(chǎng)景是不需要從多個(gè) goroutine 中進(jìn)行安全訪問(wèn)。
          • 非典型場(chǎng)景(需要原子操作):map 可能是一些更大的數(shù)據(jù)結(jié)構(gòu)或已經(jīng)同步的計(jì)算的一部分。
          • 性能場(chǎng)景考慮:若是只是為少數(shù)程序增加安全性,導(dǎo)致 map 所有的操作都要處理 mutex,將會(huì)降低大多數(shù)程序的性能。

          匯總來(lái)講,就是 Go 官方在經(jīng)過(guò)了長(zhǎng)時(shí)間的討論后,認(rèn)為 Go map 更應(yīng)適配典型使用場(chǎng)景,而不是為了小部分情況,導(dǎo)致大部分程序付出代價(jià)(性能),決定了不支持。

          總結(jié)

          在今天這篇文章中,我們針對(duì) Go 語(yǔ)言中的 map 和 slice 進(jìn)行了基本的介紹,也對(duì)不支持并發(fā)讀者的場(chǎng)景進(jìn)行了模擬展示。

          同時(shí)也針對(duì)業(yè)內(nèi)常見(jiàn)的支持并發(fā)讀寫(xiě)的方式進(jìn)行了講述,最后分析了不支持的原因,讓我們對(duì)整個(gè)前因后果有了一個(gè)完整的了解。

          不知道你在日常是否有遇到過(guò) Go 語(yǔ)言中非線性安全的問(wèn)題呢,歡迎你在評(píng)論區(qū)留言和大家一起交流!


          關(guān)注煎魚(yú),吸取他的知識(shí) ??



          你好,我是煎魚(yú)。高一折騰過(guò)前端,參加過(guò)國(guó)賽拿了獎(jiǎng),大學(xué)搞過(guò) PHP?,F(xiàn)在整 Go,在公司負(fù)責(zé)微服務(wù)架構(gòu)等相關(guān)工作推進(jìn)和研發(fā)。

          從大學(xué)開(kāi)始靠自己賺生活費(fèi)和學(xué)費(fèi),到出版 Go 暢銷(xiāo)書(shū)《Go 語(yǔ)言編程之旅》,再到獲得 GOP(Go 領(lǐng)域最有觀點(diǎn)專(zhuān)家)榮譽(yù),點(diǎn)擊藍(lán)字查看我的出書(shū)之路。

          日常分享高質(zhì)量文章,輸出 Go 面試、工作經(jīng)驗(yàn)、架構(gòu)設(shè)計(jì),加微信拉讀者交流群,記得點(diǎn)贊!

          瀏覽 51
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人免费视频 国产免费麻豆。 | 亚洲电影无码在线观看视频 | 亚洲精品99久久精品爆乳 | 免费黄v在线播放 | 国产又粗又硬视频 |