您好,登錄后才能下訂單哦!
本篇內(nèi)容主要講解“如何理解Go Map和Slice屬于非線性安全”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“如何理解Go Map和Slice屬于非線性安全”吧!
slice
我們使用多個(gè) goroutine 對(duì)類型為 slice 的變量進(jìn)行操作,看看結(jié)果會(huì)變的怎么樣。
如下:
func main() { var s []string for i := 0; i < 9999; i++ { go func() { s = append(s, "腦子進(jìn)煎魚了") }() } fmt.Printf("進(jìn)了 %d 只煎魚", len(s)) }
輸出結(jié)果:
// 第一次執(zhí)行 進(jìn)了 5790 只煎魚 // 第二次執(zhí)行 進(jìn)了 7370 只煎魚 // 第三次執(zhí)行 進(jìn)了 6792 只煎魚
你會(huì)發(fā)現(xiàn)無論你執(zhí)行多少次,每次輸出的值大概率都不會(huì)一樣。也就是追加進(jìn) slice 的值,出現(xiàn)了覆蓋的情況。
因此在循環(huán)中所追加的數(shù)量,與最終的值并不相等。且這種情況,是不會(huì)報(bào)錯(cuò)的,是一個(gè)出現(xiàn)率不算高的隱式問題。
這個(gè)產(chǎn)生的主要原因是程序邏輯本身就有問題,同時(shí)讀取到相同索引位,自然也就會(huì)產(chǎn)生覆蓋的寫入了。
map
同樣針對(duì) map 也如法炮制一下。重復(fù)針對(duì)類型為 map 的變量進(jìn)行寫入。
如下:
func main() { s := make(map[string]string) for i := 0; i < 99; i++ { go func() { s["煎魚"] = "吸魚" }() } fmt.Printf("進(jìn)了 %d 只煎魚", 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ò)誤,也就是說 Go 進(jìn)程會(huì)中斷。
不得不說,這個(gè)并發(fā)寫 map 導(dǎo)致的 fatal error: concurrent map writes 錯(cuò)誤提示。我有一個(gè)朋友,已經(jīng)看過少說幾十次了,不同組,不同人...
是個(gè)日經(jīng)的隱式問題。
對(duì) map 上鎖
實(shí)際上我們?nèi)匀淮嬖诓l(fā)讀寫 map 的訴求(程序邏輯決定),因?yàn)?Go 語言中的 goroutine 實(shí)在是太方便了。
像是一般寫爬蟲任務(wù)時(shí),基本會(huì)用到多個(gè) goroutine,獲取到數(shù)據(jù)后再寫入到 map 或者 slice 中去。
Go 官方在 Go maps in action 中提供了一種簡(jiǎn)單又便利的方式來實(shí)現(xiàn):
var counter = struct{ sync.RWMutex m map[string]int }{m: make(map[string]int)}
這條語句聲明了一個(gè)變量,它是一個(gè)匿名結(jié)構(gòu)(struct)體,包含一個(gè)原生和一個(gè)嵌入讀寫鎖 sync.RWMutex。
要想從變量中中讀出數(shù)據(jù),則調(diào)用讀鎖:
counter.RLock() n := counter.m["煎魚"] counter.RUnlock() fmt.Println("煎魚:", n)
要往變量中寫數(shù)據(jù),則調(diào)用寫鎖:
counter.Lock() counter.m["煎魚"]++ counter.Unlock()
這就是一個(gè)最常見的 Map 支持并發(fā)讀寫的方式了。
前言
雖然有了 Map+Mutex 的極簡(jiǎn)方案,但是也仍然存在一定問題。那就是在 map 的數(shù)據(jù)量非常大時(shí),只有一把鎖(Mutex)就非??膳铝耍话焰i會(huì)導(dǎo)致大量的爭(zhēng)奪鎖,導(dǎo)致各種沖突和性能低下。
常見的解決方案是分片化,將一個(gè)大 map 分成多個(gè)區(qū)間,各區(qū)間使用多個(gè)鎖,這樣子鎖的粒度就大大降低了。不過該方案實(shí)現(xiàn)起來很復(fù)雜,很容易出錯(cuò)。因此 Go 團(tuán)隊(duì)到比較為止暫無推薦,而是采取了其他方案。
該方案就是在 Go1.9 起支持的 sync.Map,其支持并發(fā)讀寫 map,起到一個(gè)補(bǔ)充的作用。
具體介紹
Go 語言的 sync.Map 支持并發(fā)讀寫 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 }
其是專門為 append-only 場(chǎng)景設(shè)計(jì)的,也就是適合讀多寫少的場(chǎng)景。這是他的優(yōu)點(diǎn)之一。
若出現(xiàn)寫多/并發(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 中的鍵的值,如果沒有值,則返回 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() { //寫入 data := []string{"煎魚", "咸魚", "烤魚", "蒸魚"} 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ū)?nbsp; v, ok = m.LoadOrStore(1, "吸魚") 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: 煎魚, true LoadOrStore: 吸魚, false Range: 0, 煎魚 Range: 1, 吸魚 Range: 3, 蒸魚 Range: 2, 烤魚
Go Slice 的話,主要還是索引位覆寫問題,這個(gè)就不需要糾結(jié)了,勢(shì)必是程序邏輯在編寫上有明顯缺陷,自行改之就好。
但 Go map 就不大一樣了,很多人以為是默認(rèn)支持的,一個(gè)不小心就翻車,這么的常見。那憑什么 Go 官方還不支持,難不成太復(fù)雜了,性能太差了,到底是為什么?
原因如下(via @go faq):
典型使用場(chǎng)景:map 的典型使用場(chǎng)景是不需要從多個(gè) goroutine 中進(jìn)行安全訪問。
非典型場(chǎng)景(需要原子操作):map 可能是一些更大的數(shù)據(jù)結(jié)構(gòu)或已經(jīng)同步的計(jì)算的一部分。
性能場(chǎng)景考慮:若是只是為少數(shù)程序增加安全性,導(dǎo)致 map 所有的操作都要處理 mutex,將會(huì)降低大多數(shù)程序的性能。
匯總來講,就是 Go 官方在經(jīng)過了長(zhǎng)時(shí)間的討論后,認(rèn)為 Go map 更應(yīng)適配典型使用場(chǎng)景,而不是為了小部分情況,導(dǎo)致大部分程序付出代價(jià)(性能),決定了不支持。
到此,相信大家對(duì)“如何理解Go Map和Slice屬于非線性安全”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。