c++ map 多线程同时更新值 崩溃_深入理解并发安全的 sync.Map
golang中內置了map關鍵字,但是它是非線程安全的。從go 1.9開始,標準庫加入了sync.Map,提供用于并發安全的map。
普通map的并發問題
map的并發讀寫代碼
func main() { m := make(map[int]int) go func() { for { _ = m[1] // 讀 } }() go func() { for { m[2] = 2 // 寫 } }() select {} // 維持主goroutine}以上是一段并發讀寫map的代碼, 其中一個goroutine一直讀,另外一個goroutine一直寫。即使讀寫的map鍵不相同,且不存在"擴容"等操作,代碼還是會報錯。
fatal?error:?concurrent?map?read?and?map?write鎖+map
那普通map有沒有能力實現并發安全呢?答案是肯定的。通過給map額外綁定一個鎖(sync.Mutex或sync.RWMutex),封裝成一個新的struct,實現并發安全。
定義帶有鎖的對象M
type M struct { sync.RWMutex Map map[int]int}執行并發讀寫
func main() { m := M{Map: make(map[int]int)} go func() { for { m.RLock() v := m.Map[2] // 讀??????fmt.Println(v) m.RUnlock() } }() go func() { for i := 1; i > 0; i++ { m.Lock() m.Map[2] = i // 寫 m.Unlock() } }() select {}}在讀goroutine讀數據時,通過讀鎖鎖定,在寫goroutine寫數據時,寫鎖鎖定,程序就能并發安全的運行,運行結果示意如下。
...112311241125...sync.Map
既然通過加鎖的方式就能解決map的并發問題,實現方式簡潔,并且利用讀寫鎖而不是Mutex可以進一步減少讀寫的時候因為鎖而帶來的性能損耗。那么為什么還會有sync.Map的出現?
當map的數據量非常大時,會引發并發的大量goroutine爭奪同一把鎖,這種現象將直接導致性能的急劇下降。在java中有類似于map的hashMap,它同樣是并發不安全,但是JDK提供了線程安全的ConcurrentHashMap,它在面對上述場景時,其核心解決方案是鎖分段技術,即內部使用多個鎖,每個區間共享一把鎖,當多線程訪問map中的不同數據段的數據時,線程間就不會存在鎖競爭,從而提高了并發訪問效率。那sync.Map采取的是什么策略來提升并發性能的呢?
sync.Map的源碼結構(基于1.14.1)
type Map struct {??//?此鎖是為了保護Map中的dirty數據??mu?Mutex??//?用來存讀的數據,只讀類型,不會造成讀寫沖突??read?atomic.Value // readOnly??//?dirty包含最新的寫入數據(entry),在寫的同時,將read中未刪除的數據拷貝到dirty中??//?因為是go中內置的普通map類型,且涉及寫操作,所以需要通過mu加鎖??dirty?map[interface{}]*entry??//?當讀數據時,該字段不在read中,嘗試從dirty中讀取,不管是否在dirty中讀取到數據,misses+1??//?當累計到len(dirty)時,會將dirty拷貝到read中,并將dirty清空,以此提升讀性能。 misses int}(左右滑動查看完整代碼圖片)
在sync.Map中用到了兩個冗余數據結構read、dirty。其中read的類型為atomic.Value,它會通過atomic.Value的Load方法將其斷言為readOnly對象。
read,?_?:=?m.read.Load().(readOnly)?//?m為sync.Map因此,read的真實類型即是readOnly,其數據類型如下。
type readOnly struct {??// read 中的go內置map類型,但是它不需要鎖。??m???????map[interface{}]*entry??//?當sync.Map.diry中的包含了某些不在m中的key時,amended的值為true.??amended?bool}(左右滑動查看完整代碼圖片)
amended屬性的作用是指明dirty中是否有readOnly.m中未包含的數據,因此當對sync.Map的讀操作在read中找不到數據時,將進一步到dirty中查找。
readOnly.m和Map.dirty中map存儲的值類型是*entry,它包含一個指針p,指向用戶存儲的value值。
type?entry?struct?{ p unsafe.Pointer // *interface{}}entry.p的值有三種類型:
nil:entry已經被刪除,且m.dirty為nil
expunged:entry被刪除,m.dirty不為nil,但entry不存在m.dirty中
其他:entry有效,記錄在m.read中,若dirty不為空,也會記錄在dirty中。
雖然read和dirty存在冗余數據,但是這些數據entry是通過指針指向的,因此,盡管Map的value可能會很大,但是空間存儲還是足夠的。
以上是sync.Map的數據結構,下面著重看看它的四個方法實現:Load、Store、Delete和Range。
Load
加載方法,通過提供的鍵key,查找對應的值value。
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {??//?首先從m.read中通過Load方法得到readOnly read, _ := m.read.Load().(readOnly)??//?從read中的map中查找key e, ok := read.m[key]??//?如果在read中沒有找到,且表明有新數據在diry中(read.amended為true)??//?那么,就需要在dirty中查找,這時需要加鎖。 if !ok && read.amended { m.mu.Lock()????//?雙重檢查:避免在本次加鎖的時候,有其他goroutine正好將Map中的dirty數據復制到了read中。????//?能發生上述可能的原因是以下兩行代碼語句,并不是原子操作。? // if !ok && read.amended { // m.mu.Lock() // }????//?而Map.read其并發安全性的保障就在于它的修改是通過原子操作進行的。????//?因此需要再檢查一次read. read, _ = m.read.Load().(readOnly) e, ok = read.m[key]????//?如果m.read中key還是不存在,且dirty中有新數據,則檢查dirty中的數據。 if !ok && read.amended { e, ok = m.dirty[key]??????//?不管是否從dirty中得到了數據,都會將misses的計數+1 m.missLocked() } m.mu.Unlock() } if !ok { return nil, false }??//?通過Map的load方法,將entry.p加載為對應指針,再返回指針指向的值 return e.load()}(左右滑動查看完整代碼圖片)
Map.missLocked函數是保證sync.Map性能的重要函數,它的目的是將存在有鎖的dirty中的數據,轉移到只讀線程安全的read中去。
func (m *Map) missLocked() { m.misses++ // 計數+1 if m.misses < len(m.dirty) { return }??m.read.Store(readOnly{m:?m.dirty})?//?將dirty數據復制到read中去 m.dirty = nil // dirty清空 m.misses = 0 // misses重置為0}(左右滑動查看完整代碼圖片)
Store
該方法更新或新增鍵值對key-value。
func (m *Map) Store(key, value interface{}) {??//?如果m.read中存在該鍵,且該鍵沒有被標記刪除(expunged)??//?則嘗試直接存儲(見entry的tryStore方法)??//?注意:?如果m.dirty中也有該鍵(key對應的entry),由于都是通過指針指向,所有m.dirty中也會保持最新entry值。 read, _ := m.read.Load().(readOnly) if e, ok := read.m[key]; ok && e.tryStore(&value) { return??}??//?如果不滿足上述條件,即m.read不存在或者已經被標記刪除 m.mu.Lock() read, _ = m.read.Load().(readOnly)??if?e,?ok?:=?read.m[key];?ok?{?//?如果read中有該鍵????if?e.unexpungeLocked()?{?//?判斷entry是否被標記刪除??????//?如果entry被標記刪除,則將entry添加進m.dirty中 m.dirty[key] = e }????//?更新entry指向value地址 e.storeLocked(&value)??}?else?if?e,?ok?:=?m.dirty[key];?ok?{?//dirty中有該鍵:更新 e.storeLocked(&value)??}?else?{?// dirty和read中均無該鍵:新增????if?!read.amended?{?//?表明dirty中沒有新數據,在dirty中增加第一個新鍵??????m.dirtyLocked()?//?從m.read中復制未刪除的數據到dirty中 m.read.Store(readOnly{m: read.m, amended: true}) }????m.dirty[key]?=?newEntry(value)?//?將entry增加到dirty中 } m.mu.Unlock()}(左右滑動查看完整代碼圖片)
Store的每次操作都是先從read開始,當不滿足條件時,才加鎖操作dirty。但是由于存在從read中復制數據的情況(例如dirty剛復制完數據給m.read,又來了一個新鍵),當m.read中數據量很大時,可能對性能造成影響。
Delete
刪除某鍵值。
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() }}//?如果read中有該鍵,則從read中刪除,其刪除方式是通過原子操作func (e *entry) delete() (hadValue bool) { for { p := atomic.LoadPointer(&e.p)????//?如果p指針為空,或者被標記清除 if p == nil || p == expunged { return false } // 通過原子操作,將e.p標記為nil. if atomic.CompareAndSwapPointer(&e.p, p, nil) { return true } }}(左右滑動查看完整代碼圖片)
Delete中的邏輯和Store邏輯相似,都是從read開始,如果這個key(也即是entry)不在read中,且dirty中有新數據,則加鎖從dirty中刪除。注意,和Load與Store方法一樣,也是需要雙檢查。
Range
想要遍歷sync.Map,不能通過for range的形式,因此,它自身提供了Range方法,通過回調的方式遍歷。
func?(m?*Map)?Range(f?func(key,?value?interface{})?bool)?{ read, _ := m.read.Load().(readOnly)??//?判斷dirty中是否有新的數據??if?read.amended?{ m.mu.Lock()????//?雙檢查 read, _ = m.read.Load().(readOnly) if read.amended {??????//?將dirty中的數據復制到read中 read = readOnly{m: m.dirty} m.read.Store(read) m.dirty = nil m.misses = 0 } m.mu.Unlock() }??//?遍歷已經整合過dirty的read for k, e := range read.m { v, ok := e.load() if !ok { continue } if !f(k, v) { break } }}(左右滑動查看完整代碼圖片)
sync.Map的優化總結
1. 空間換時間:通過兩個冗余的數據結構(read、write),減小鎖對性能的影響。
2. 讀操作使用read,避免讀寫沖突。
3. 動態調整:通過misses值,避免dirty數據過多。
4. 雙檢查機制:避免在非原子操作時產生數據錯誤。
5. 延遲刪除機制:刪除一個鍵值只是先打標記,只有等提升dirty(復制到read中,并清空自身)時才清理刪除的數據。
6. 優先從read中讀、改和刪除,因為對read的操作不用加鎖,大大提升性能。
sync.Map的使用例子
func?main()?{ var sm sync.Map??//?注意:同一個sync.Map,和map不一樣,每個item的key或value可以和其他的數據類型不一樣??//?只要滿足key能hash即可 sm.Store(1, "a") sm.Store("b", 2) sm.Store("c", 3)??//?和map獲取key值類似??if?v,?ok?:=?sm.Load("b");?ok?{ fmt.Println(v) }?//?刪除某個key的鍵值對 sm.Delete(1)??//?參數fun中的參數是遍歷獲得的key和value,返回一個bool值??//?返回true時,繼續遍歷??// 返回false,遍歷結束 sm.Range(func(key, value interface{}) bool { fmt.Println(key,value) return true })}(左右滑動查看完整代碼圖片)
輸出
2b 2c?3sync.Map的性能
在Go源碼$GOROOT/src/sync中,提供了測試代碼。
map_reference_test.go:? 定義了測試用的mapInterface接口,sync.Map、RwMutexMap和DeepCopyMap對象實現該接口方法。
map_test.go: 三個對象的方法測試代碼。
map_bench_test.go:? 三個對象的benchmark性能對比測試代碼。
在小菜刀的機器上,運行性能測試結果如下。
$ go test -bench=LoadMostlyHits -benchmemBenchmarkLoadMostlyHits/*sync_test.DeepCopyMap-8 80252629 13.5 ns/op 7 B/op 0 allocs/opBenchmarkLoadMostlyHits/*sync_test.RWMutexMap-8 23025050 51.8 ns/op 7 B/op 0 allocs/opBenchmarkLoadMostlyHits/*sync.Map-8 67718686 14.9 ns/op 7 B/op 0 allocs/op$ go test -bench=LoadMostlyMisses -benchmemBenchmarkLoadMostlyMisses/*sync_test.DeepCopyMap-8 128480215 11.2 ns/op 7 B/op 0 allocs/opBenchmarkLoadMostlyMisses/*sync_test.RWMutexMap-8 23989224 47.4 ns/op 7 B/op 0 allocs/opBenchmarkLoadMostlyMisses/*sync.Map-8 132403878 9.30 ns/op 7 B/op 0 allocs/op$ go test -bench=LoadOrStoreBalanced -benchmemBenchmarkLoadOrStoreBalanced/*sync_test.RWMutexMap-8 3909409 553 ns/op 99 B/op 2 allocs/opBenchmarkLoadOrStoreBalanced/*sync.Map-8 3574923 368 ns/op 97 B/op 3 allocs/op$ go test -bench=LoadOrStoreUnique -benchmemBenchmarkLoadOrStoreUnique/*sync_test.RWMutexMap-8 2053806 647 ns/op 174 B/op 2 allocs/opBenchmarkLoadOrStoreUnique/*sync.Map-8 2456720 577 ns/op 140 B/op 4 allocs/op$ go test -bench=LoadOrStoreCollision -benchmemBenchmarkLoadOrStoreCollision/*sync_test.DeepCopyMap-8 153679003 8.18 ns/op 0 B/op 0 allocs/opBenchmarkLoadOrStoreCollision/*sync_test.RWMutexMap-8 13718534 87.9 ns/op 0 B/op 0 allocs/opBenchmarkLoadOrStoreCollision/*sync.Map-8 175620835 7.08 ns/op 0 B/op 0 allocs/op$ go test -bench=Range -benchmemBenchmarkRange/*sync_test.DeepCopyMap-8 416906 2947 ns/op 0 B/op 0 allocs/opBenchmarkRange/*sync_test.RWMutexMap-8 22784 52370 ns/op 16384 B/op 1 allocs/opBenchmarkRange/*sync.Map-8 369955 3194 ns/op 0 B/op 0 allocs/op$ go test -bench=AdversarialAlloc -benchmemBenchmarkAdversarialAlloc/*sync_test.DeepCopyMap-8 1875109 646 ns/op 539 B/op 1 allocs/opBenchmarkAdversarialAlloc/*sync_test.RWMutexMap-8 19454866 61.6 ns/op 8 B/op 1 allocs/opBenchmarkAdversarialAlloc/*sync.Map-8 3712470 320 ns/op 51 B/op 1 allocs/op$ go test -bench=AdversarialDelete -benchmemBenchmarkAdversarialDelete/*sync_test.DeepCopyMap-8 6749067 215 ns/op 168 B/op 1 allocs/opBenchmarkAdversarialDelete/*sync_test.RWMutexMap-8 16046545 76.9 ns/op 25 B/op 1 allocs/opBenchmarkAdversarialDelete/*sync.Map-8 18678104 64.2 ns/op 18 B/op 1 allocs/op(左右滑動查看完整代碼圖片)
如何選擇Map
從性能測試結果可以看出,sync.Map并不是為了代替鎖+map的組合。它的設計,是為了在某些并發場景下,相對前者能有較小的性能損耗。
源碼文檔中($GOROOT/src/sync/map.go)已經給出了sync.Map的合適場景。
//?The?Map?type?is?specialized.?Most?code?should?use?a?plain?Go?map?instead,// with separate locking or coordination, for better type safety and to make it// easier to maintain other invariants along with the map content.//// The Map type is optimized for two common use cases: (1) when the entry for a given// key is only ever written once but read many times, as in caches that only grow,// or (2) when multiple goroutines read, write, and overwrite entries for disjoint// sets of keys. In these two cases, use of a Map may significantly reduce lock// contention compared to a Go map paired with a separate Mutex or RWMutex.(左右滑動查看完整代碼圖片)
兩種情況應該選擇sync.Map
key值一次寫入,多次讀取(即寫少讀多場景)。
多個goroutine的讀取、寫入和覆蓋在不相交的key集。
推薦閱讀
并發安全的 map:sync.Map源碼分析
喜歡本文的朋友,歡迎關注“Go語言中文網”:
Go語言中文網啟用微信學習交流群,歡迎加微信:274768166,投稿亦歡迎
總結
以上是生活随笔為你收集整理的c++ map 多线程同时更新值 崩溃_深入理解并发安全的 sync.Map的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 北京环球影城未来水世界表演时间
- 下一篇: 银川看输卵管炎最好的医院推荐