Go 1.9 sync.Map揭秘
Go 1.9 sync.Map揭秘
目錄?[?]
在Go 1.6之前, 內(nèi)置的map類型是部分goroutine安全的,并發(fā)的讀沒(méi)有問(wèn)題,并發(fā)的寫可能有問(wèn)題。自go 1.6之后, 并發(fā)地讀寫map會(huì)報(bào)錯(cuò),這在一些知名的開(kāi)源庫(kù)中都存在這個(gè)問(wèn)題,所以go 1.9之前的解決方案是額外綁定一個(gè)鎖,封裝成一個(gè)新的struct或者單獨(dú)使用鎖都可以。
本文帶你深入到sync.Map的具體實(shí)現(xiàn)中,看看為了增加一個(gè)功能,代碼是如何變的復(fù)雜的,以及作者在實(shí)現(xiàn)sync.Map的一些思想。
有并發(fā)問(wèn)題的map
官方的faq已經(jīng)提到內(nèi)建的map不是線程(goroutine)安全的。
首先,讓我們看一段并發(fā)讀寫的代碼,下列程序中一個(gè)goroutine一直讀,一個(gè)goroutine一只寫同一個(gè)鍵值,即即使讀寫的鍵不相同,而且map也沒(méi)有"擴(kuò)容"等操作,代碼還是會(huì)報(bào)錯(cuò)。
| 12345678910111213141516171819 | package mainfunc main() { m := make(map[int]int) go func() { for { _ = m[1] } }() go func() { for { m[2] = 2 } }() select {}} |
錯(cuò)誤信息是:?fatal error: concurrent map read and map write。
如果你查看Go的源代碼:?hashmap_fast.go#L118,會(huì)看到讀的時(shí)候會(huì)檢查hashWriting標(biāo)志, 如果有這個(gè)標(biāo)志,就會(huì)報(bào)并發(fā)錯(cuò)誤。
寫的時(shí)候會(huì)設(shè)置這個(gè)標(biāo)志:?hashmap.go#L542
| 1 | h.flags |= hashWriting |
hashmap.go#L628設(shè)置完之后會(huì)取消這個(gè)標(biāo)記。
當(dāng)然,代碼中還有好幾處并發(fā)讀寫的檢查, 比如寫的時(shí)候也會(huì)檢查是不是有并發(fā)的寫,刪除鍵的時(shí)候類似寫,遍歷的時(shí)候并發(fā)讀寫問(wèn)題等。
有時(shí)候,map的并發(fā)問(wèn)題不是那么容易被發(fā)現(xiàn), 你可以利用-race參數(shù)來(lái)檢查。
Go 1.9之前的解決方案
但是,很多時(shí)候,我們會(huì)并發(fā)地使用map對(duì)象,尤其是在一定規(guī)模的項(xiàng)目中,map總會(huì)保存goroutine共享的數(shù)據(jù)。在Go官方blog的Go maps in action一文中,提供了一種簡(jiǎn)便的解決方案。
| 1234 | var counter = struct{ sync.RWMutex m map[string]int}{m: make(map[string]int)} |
它使用嵌入struct為map增加一個(gè)讀寫鎖。
讀數(shù)據(jù)的時(shí)候很方便的加鎖:
| 1234 | counter.RLock()n := counter.m["some_key"]counter.RUnlock()fmt.Println("some_key:", n) |
寫數(shù)據(jù)的時(shí)候:
| 123 | counter.Lock()counter.m["some_key"]++counter.Unlock() |
sync.Map
可以說(shuō),上面的解決方案相當(dāng)簡(jiǎn)潔,并且利用讀寫鎖而不是Mutex可以進(jìn)一步減少讀寫的時(shí)候因?yàn)殒i帶來(lái)的性能。
但是,它在一些場(chǎng)景下也有問(wèn)題,如果熟悉Java的同學(xué),可以對(duì)比一下java的ConcurrentHashMap的實(shí)現(xiàn),在map的數(shù)據(jù)非常大的情況下,一把鎖會(huì)導(dǎo)致大并發(fā)的客戶端共爭(zhēng)一把鎖,Java的解決方案是shard, 內(nèi)部使用多個(gè)鎖,每個(gè)區(qū)間共享一把鎖,這樣減少了數(shù)據(jù)共享一把鎖帶來(lái)的性能影響,orcaman提供了這個(gè)思路的一個(gè)實(shí)現(xiàn):?concurrent-map,他也詢問(wèn)了Go相關(guān)的開(kāi)發(fā)人員是否在Go中也實(shí)現(xiàn)這種方案,由于實(shí)現(xiàn)的復(fù)雜性,答案是Yes, we considered it.,但是除非有特別的性能提升和應(yīng)用場(chǎng)景,否則沒(méi)有進(jìn)一步的開(kāi)發(fā)消息。
那么,在Go 1.9中sync.Map是怎么實(shí)現(xiàn)的呢?它是如何解決并發(fā)提升性能的呢?
sync.Map的實(shí)現(xiàn)有幾個(gè)優(yōu)化點(diǎn),這里先列出來(lái),我們后面慢慢分析。
下面我們介紹sync.Map的重點(diǎn)代碼,以便理解它的實(shí)現(xiàn)思想。
首先,我們看一下sync.Map的數(shù)據(jù)結(jié)構(gòu):
| 123456789101112131415161718 | type Map struct { // 當(dāng)涉及到dirty數(shù)據(jù)的操作的時(shí)候,需要使用這個(gè)鎖 mu Mutex // 一個(gè)只讀的數(shù)據(jù)結(jié)構(gòu),因?yàn)橹蛔x,所以不會(huì)有讀寫沖突。 // 所以從這個(gè)數(shù)據(jù)中讀取總是安全的。 // 實(shí)際上,實(shí)際也會(huì)更新這個(gè)數(shù)據(jù)的entries,如果entry是未刪除的(unexpunged), 并不需要加鎖。如果entry已經(jīng)被刪除了,需要加鎖,以便更新dirty數(shù)據(jù)。 read atomic.Value // readOnly // dirty數(shù)據(jù)包含當(dāng)前的map包含的entries,它包含最新的entries(包括read中未刪除的數(shù)據(jù),雖有冗余,但是提升dirty字段為read的時(shí)候非常快,不用一個(gè)一個(gè)的復(fù)制,而是直接將這個(gè)數(shù)據(jù)結(jié)構(gòu)作為read字段的一部分),有些數(shù)據(jù)還可能沒(méi)有移動(dòng)到read字段中。 // 對(duì)于dirty的操作哦需要加鎖,因?yàn)閷?duì)它的操作可能會(huì)有讀寫競(jìng)爭(zhēng)。 // 當(dāng)dirty為空的時(shí)候, 比如初始化或者剛提升完,下一次的寫操作會(huì)復(fù)制read字段中未刪除的數(shù)據(jù)到這個(gè)數(shù)據(jù)中。 dirty map[interface{}]*entry // 當(dāng)從Map中讀取entry的時(shí)候,如果read中不包含這個(gè)entry,會(huì)嘗試從dirty中讀取,這個(gè)時(shí)候會(huì)將misses加一, // 當(dāng)misses累積到 dirty的長(zhǎng)度的時(shí)候, 就會(huì)將dirty提升為read,避免從dirty中miss太多次。因?yàn)椴僮鱠irty需要加鎖。 misses int} |
它的數(shù)據(jù)結(jié)構(gòu)很簡(jiǎn)單,值包含四個(gè)字段:read、mu、dirty、misses。
它使用了冗余的數(shù)據(jù)結(jié)構(gòu)read、dirty。dirty中會(huì)包含read中為刪除的entries,新增加的entries會(huì)加入到dirty中。
read的數(shù)據(jù)結(jié)構(gòu)是:
| 1234 | type readOnly struct { m map[interface{}]*entry amended bool // 如果Map.dirty有些數(shù)據(jù)不在中的時(shí)候,這個(gè)值為true} |
amended指明Map.dirty中有readOnly.m未包含的數(shù)據(jù),所以如果從Map.read找不到數(shù)據(jù)的話,還要進(jìn)一步到Map.dirty中查找。
對(duì)Map.read的修改是通過(guò)原子操作進(jìn)行的。
雖然read和dirty有冗余數(shù)據(jù),但這些數(shù)據(jù)是通過(guò)指針指向同一個(gè)數(shù)據(jù),所以盡管Map的value會(huì)很大,但是冗余的空間占用還是有限的。
readOnly.m和Map.dirty存儲(chǔ)的值類型是*entry,它包含一個(gè)指針p, 指向用戶存儲(chǔ)的value值。
| 123 | type entry struct { p unsafe.Pointer // *interface{}} |
p有三種值:
- nil: entry已被刪除了,并且m.dirty為nil
- expunged: entry已被刪除了,并且m.dirty不為nil,而且這個(gè)entry不存在于m.dirty中
- 其它: entry是一個(gè)正常的值
以上是sync.Map的數(shù)據(jù)結(jié)構(gòu),下面我們重點(diǎn)看看Load、Store、Delete、Range這四個(gè)方法,其它輔助方法可以參考這四個(gè)方法來(lái)理解。
Load
加載方法,也就是提供一個(gè)鍵key,查找對(duì)應(yīng)的值value,如果不存在,通過(guò)ok反映:
| 123456789101112131415161718192021222324252627 | func (m *Map) Load(key interface{}) (value interface{}, ok bool) { // 1.首先從m.read中得到只讀readOnly,從它的map中查找,不需要加鎖 read, _ := m.read.Load().(readOnly) e, ok := read.m[key] // 2. 如果沒(méi)找到,并且m.dirty中有新數(shù)據(jù),需要從m.dirty查找,這個(gè)時(shí)候需要加鎖 if !ok && read.amended { m.mu.Lock() // 雙檢查,避免加鎖的時(shí)候m.dirty提升為m.read,這個(gè)時(shí)候m.read可能被替換了。 read, _ = m.read.Load().(readOnly) e, ok = read.m[key] // 如果m.read中還是不存在,并且m.dirty中有新數(shù)據(jù) if !ok && read.amended { // 從m.dirty查找 e, ok = m.dirty[key] // 不管m.dirty中存不存在,都將misses計(jì)數(shù)加一 // missLocked()中滿足條件后就會(huì)提升m.dirty m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } return e.load()} |
這里有兩個(gè)值的關(guān)注的地方。一個(gè)是首先從m.read中加載,不存在的情況下,并且m.dirty中有新數(shù)據(jù),加鎖,然后從m.dirty中加載。
二是這里使用了雙檢查的處理,因?yàn)樵谙旅娴膬蓚€(gè)語(yǔ)句中,這兩行語(yǔ)句并不是一個(gè)原子操作。
| 12 | if !ok && read.amended { m.mu.Lock() |
雖然第一句執(zhí)行的時(shí)候條件滿足,但是在加鎖之前,m.dirty可能被提升為m.read,所以加鎖后還得再檢查m.read,后續(xù)的方法中都使用了這個(gè)方法。
雙檢查的技術(shù)Java程序員非常熟悉了,單例模式的實(shí)現(xiàn)之一就是利用雙檢查的技術(shù)。
可以看到,如果我們查詢的鍵值正好存在于m.read中,無(wú)須加鎖,直接返回,理論上性能優(yōu)異。即使不存在于m.read中,經(jīng)過(guò)miss幾次之后,m.dirty會(huì)被提升為m.read,又會(huì)從m.read中查找。所以對(duì)于更新/增加較少,加載存在的key很多的case,性能基本和無(wú)鎖的map類似。
下面看看m.dirty是如何被提升的。?missLocked方法中可能會(huì)將m.dirty提升。
| 123456789 | func (m *Map) missLocked() { m.misses++ if m.misses < len(m.dirty) { return } m.read.Store(readOnly{m: m.dirty}) m.dirty = nil m.misses = 0} |
上面的最后三行代碼就是提升m.dirty的,很簡(jiǎn)單的將m.dirty作為readOnly的m字段,原子更新m.read。提升后m.dirty、m.misses重置, 并且m.read.amended為false。
Store
這個(gè)方法是更新或者新增一個(gè)entry。
| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253 | func (m *Map) Store(key, value interface{}) { // 如果m.read存在這個(gè)鍵,并且這個(gè)entry沒(méi)有被標(biāo)記刪除,嘗試直接存儲(chǔ)。 // 因?yàn)閙.dirty也指向這個(gè)entry,所以m.dirty也保持最新的entry。 read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e.tryStore(&value) { return } // 如果`m.read`不存在或者已經(jīng)被標(biāo)記刪除 m.mu.Lock() read, _ = m.read.Load().(readOnly) if e, ok := read.m[key]; ok { if e.unexpungeLocked() { //標(biāo)記成未被刪除 m.dirty[key] = e //m.dirty中不存在這個(gè)鍵,所以加如m.dirty } e.storeLocked(&value) //更新 } else if e, ok := m.dirty[key]; ok { // m.dirty存在這個(gè)鍵,更新 e.storeLocked(&value) } else { //新鍵值 if !read.amended { //m.dirty中沒(méi)有新的數(shù)據(jù),往m.dirty中增加第一個(gè)新鍵 m.dirtyLocked() //從m.read中復(fù)制未刪除的數(shù)據(jù) m.read.Store(readOnly{m: read.m, amended: true}) } m.dirty[key] = newEntry(value) //將這個(gè)entry加入到m.dirty中 } m.mu.Unlock()}func (m *Map) dirtyLocked() { if m.dirty != nil { return } read, _ := m.read.Load().(readOnly) m.dirty = make(map[interface{}]*entry, len(read.m)) for k, e := range read.m { if !e.tryExpungeLocked() { m.dirty[k] = e } }}func (e *entry) tryExpungeLocked() (isExpunged bool) { p := atomic.LoadPointer(&e.p) for p == nil { // 將已經(jīng)刪除標(biāo)記為nil的數(shù)據(jù)標(biāo)記為expunged if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { return true } p = atomic.LoadPointer(&e.p) } return p == expunged} |
你可以看到,以上操作都是先從操作m.read開(kāi)始的,不滿足條件再加鎖,然后操作m.dirty。
Store可能會(huì)在某種情況下(初始化或者m.dirty剛被提升后)從m.read中復(fù)制數(shù)據(jù),如果這個(gè)時(shí)候m.read中數(shù)據(jù)量非常大,可能會(huì)影響性能。
Delete
刪除一個(gè)鍵值。
| 12345678910111213141516 | func (m *Map) Delete(key interface{}) { read, _ := m.read.Load().(readOnly) e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() read, _ = m.read.Load().(readOnly) e, ok = read.m[key] if !ok && read.amended { delete(m.dirty, key) } m.mu.Unlock() } if ok { e.delete() }} |
同樣,刪除操作還是從m.read中開(kāi)始, 如果這個(gè)entry不存在于m.read中,并且m.dirty中有新數(shù)據(jù),則加鎖嘗試從m.dirty中刪除。
注意,還是要雙檢查的。 從m.dirty中直接刪除即可,就當(dāng)它沒(méi)存在過(guò),但是如果是從m.read中刪除,并不會(huì)直接刪除,而是打標(biāo)記:
| 12345678910111213 | func (e *entry) delete() (hadValue bool) { for { p := atomic.LoadPointer(&e.p) // 已標(biāo)記為刪除 if p == nil || p == expunged { return false } // 原子操作,e.p標(biāo)記為nil if atomic.CompareAndSwapPointer(&e.p, p, nil) { return true } }} |
Range
因?yàn)閒or ... range map是內(nèi)建的語(yǔ)言特性,所以沒(méi)有辦法使用for range遍歷sync.Map, 但是可以使用它的Range方法,通過(guò)回調(diào)的方式遍歷。
| 12345678910111213141516171819202122232425262728 | func (m *Map) Range(f func(key, value interface{}) bool) { read, _ := m.read.Load().(readOnly) // 如果m.dirty中有新數(shù)據(jù),則提升m.dirty,然后在遍歷 if read.amended { //提升m.dirty m.mu.Lock() read, _ = m.read.Load().(readOnly) //雙檢查 if read.amended { read = readOnly{m: m.dirty} m.read.Store(read) m.dirty = nil m.misses = 0 } m.mu.Unlock() } // 遍歷, for range是安全的 for k, e := range read.m { v, ok := e.load() if !ok { continue } if !f(k, v) { break } }} |
Range方法調(diào)用前可能會(huì)做一個(gè)m.dirty的提升,不過(guò)提升m.dirty不是一個(gè)耗時(shí)的操作。
sync.Map的性能
Go 1.9源代碼中提供了性能的測(cè)試:?map_bench_test.go、map_reference_test.go
我也基于這些代碼修改了一下,得到下面的測(cè)試數(shù)據(jù),相比較以前的解決方案,性能多少回有些提升,如果你特別關(guān)注性能,可以考慮sync.Map。
| 12345678910111213141516 | BenchmarkHitAll/*sync.RWMutexMap-4 20000000 83.8 ns/opBenchmarkHitAll/*sync.Map-4 30000000 59.9 ns/opBenchmarkHitAll_WithoutPrompting/*sync.RWMutexMap-4 20000000 96.9 ns/opBenchmarkHitAll_WithoutPrompting/*sync.Map-4 20000000 64.1 ns/opBenchmarkHitNone/*sync.RWMutexMap-4 20000000 79.1 ns/opBenchmarkHitNone/*sync.Map-4 30000000 43.3 ns/opBenchmarkHit_WithoutPrompting/*sync.RWMutexMap-4 20000000 81.5 ns/opBenchmarkHit_WithoutPrompting/*sync.Map-4 30000000 44.0 ns/opBenchmarkUpdate/*sync.RWMutexMap-4 5000000 328 ns/opBenchmarkUpdate/*sync.Map-4 10000000 146 ns/opBenchmarkUpdate_WithoutPrompting/*sync.RWMutexMap-4 5000000 336 ns/opBenchmarkUpdate_WithoutPrompting/*sync.Map-4 5000000 324 ns/opBenchmarkDelete/*sync.RWMutexMap-4 10000000 155 ns/opBenchmarkDelete/*sync.Map-4 30000000 55.0 ns/opBenchmarkDelete_WithoutPrompting/*sync.RWMutexMap-4 10000000 173 ns/opBenchmarkDelete_WithoutPrompting/*sync.Map-4 10000000 147 ns/op |
其它
sync.Map沒(méi)有Len方法,并且目前沒(méi)有跡象要加上 (issue#20680),所以如果想得到當(dāng)前Map中有效的entries的數(shù)量,需要使用Range方法遍歷一次, 比較X疼。
LoadOrStore方法如果提供的key存在,則返回已存在的值(Load),否則保存提供的鍵值(Store)。
來(lái)源:?http://colobu.com/2017/07/11/dive-into-sync-Map/
轉(zhuǎn)載于:https://www.cnblogs.com/zhangboyu/p/7456687.html
總結(jié)
以上是生活随笔為你收集整理的Go 1.9 sync.Map揭秘的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 39、平衡二叉树
- 下一篇: Spring中获取Session的方法汇