「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」前端
咱們都知道go的map是併發不安全的,當幾個goruotine同時對一個map進行讀寫操做時,就會出現併發寫問題fatal error: concurrent map writes
後端
理論上只要在多核cpu下,若是子goroutine和主gouroutine同時在運行,就會出現問題。咱們不妨用go自帶的-race
來檢測下,能夠運行 go run -race main.go
安全
經過檢測,咱們能夠發現,存在
data race
,即數據競爭問題。有人說這簡單,加鎖解決,加鎖當然能夠解決,可是你懂的,鎖的開銷問題。
撇開數據競爭的問題,咱們能夠經過看個例子來了解下鎖的開銷:markdown
BenchmarkAddMapWithUnLock
是測試無鎖的BenchmarkAddMapWithLock
是測試有鎖的經過go test -bench .
來跑測試,得出的結果以下:併發
能夠發現無鎖的平均耗時約
6.6 ms
,帶鎖的平均耗時約7.0 ms
,雖然說相差無幾,但也反應加鎖的開銷。在一些複雜的案例中,可能會更明顯。源碼分析
有人說,既然鎖開銷大,那麼就用go內置的方法sync.map,它能夠解決併發問題。sync.map確實能夠解決併發map問題,可是它在讀多寫少的狀況下,比較適合,能夠保證併發安全,同時又不須要鎖的開銷,在寫多讀少的狀況下反而可能會更差,主要是由於它的設計,咱們從源碼分析看看:post
read的數據存在readOnly.m中,也是個map,value是個entry的指針,entry是個結構體具體類型以下:性能
裏面就一個p,當咱們設置一個key的value時,能夠理解爲p就是指向這個value的指針(p就是value的地址)。
當readOnly.amended = true
的時候,表示read的數據不是最新的,dirty裏面包含一些read沒有的新key。
3. Map的dirty也是map類型,從命名來看它是髒的,能夠理解某些場景新加kv的時候,會先加到dirty中,它比read要新。
4. Map的misses,當從read中沒讀到數據,且amended=true的時候,會嘗試從dirty中讀取,而且misses會加1,當misssed數量大於等於dirty的長度的時候,就會把dirty賦給read,同時重置missed和dirty。測試
sync.map的核心思想就是空間換時間。
假設如今有個畫展對外展現(read
)n幅畫,一羣人來看,你們在這個畫展上想看什麼就看什麼,不用等待、不用排隊。這時上了副新畫,可是因爲畫展示在在工做時間,不能直接掛上去,並且新畫可能還要保養什麼,暫時不放在畫展(read
)上,因而就先放在備份的倉庫中(dirty
),若是真有人要看這幅新畫,那麼只能領他到倉庫中(dirty
)中去看,假設這時來了個新畫,此時倉庫中有n+1副畫了,這時有人來問:有沒有這幅新畫呀,經理說:有,你和我到倉庫中去看下。這時又有人來問:有沒有這幅新畫呀,經理說:有,你和我到倉庫中去看下。當問有沒有這幅新畫的次數達到了n+1的時候,這時畫展的老闆發現這幅新畫要看的人還很多。因而對經理說:你去看下,等下沒人看畫展(read
)的時候,把畫展(read
)的畫所有下掉,把倉庫(dirty)裏面的畫所有換上。當經理所有換結束後,此時畫展(read
)上已是最全最新的畫了。
sync.map的原理大概就相似上面的例子,在少許人對新畫(新的k、v
)感興趣的時候,就帶他去倉庫(dirty
)看,此時由於經理只有一個,因此每次只能帶一我的(加鎖
),效率低,其餘的畫,在畫展(read
)上,隨便看,效率高。atom
tryStore裏面有判斷
p == expunged
就返回false。p有三種類型:nil
(read中的key被delete的時候其實軟刪除,只是把p設置成nil)、expunged
(被刪除的key(p==nil)會在read copy 到 dirty的時候再被設置成expunged)、其餘正常的value的地址
,這裏若是是expunged就不選擇更新value。
而後在dirty中設置新的k、v。(這裏能夠發現新的k、v都是先加在dirty的map中的,read是沒有的)。
6. 如今dirty是比較乾淨的數據了(已經清空了nil或expunged的key),設置amended=true(說明此時dirty不爲空,且dirty中有新數據)
7. 解鎖
總結:
if else
的分支判斷。總體確定是比常規map加鎖性能要差的。 6. 若是沒有對應的key,就返回nil,有的話,就返回對應的value
總結:
總結:當刪除的key在read中,能夠經過軟刪除來標記,這樣自己read對應的map不會由於頻繁刪除而觸發等量擴容,關於map的擴容規則能夠參考map原理。
經過分析了sync.map咱們發現,在讀多寫少的狀況下,仍是比較優秀的,相比常規map加鎖那種確定是更好的,可是寫多讀少的狀況下,並不適合,由於仍是涉及到頻繁的加鎖、read和dirty交換等開銷,搞很差還比常規的map加鎖性能更差。咱們仍是經過一個極端的例子來看:
BenchmarkAddMapWithUnLock
是測試無鎖的BenchmarkAddMapWithLock
是測試有鎖的BenchmarkAddMapWithSyncMap
是測試sync.map3個方法都是對一個map加10w條數據。
經過go test -bench .
來跑測試,得出的結果以下:
能夠看出sync.map的耗時是其餘的兩個的5倍左右。sync.map是個好東西,可是場景用錯,反而拔苗助長。