<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
我們知道,go 裡面提供了 map
這種型別讓我們可以儲存鍵值對資料,但是如果我們在並行的情況下使用 map
的話,就會發現它是不支援並行地進行讀寫的(會報錯)。 在這種情況下,我們可以使用 sync.Mutex
來保證並行安全,但是這樣會導致我們在讀寫的時候,都需要加鎖,這樣就會導致效能的下降。
除了使用互斥鎖這種相對低效的方式,我們還可以使用 sync.Map
來保證並行安全,它在某些場景下有比使用 sync.Mutex
更高的效能。 本文就來探討一下 sync.Map
中的一些大家比較感興趣的問題,比如為什麼有了 map
還要 sync.Map
?它為什麼快?sync.Map
的適用場景(注意:不是所有情況下都快。)等。
關於 sync.Map
的設計與實現原理,會在下一篇中再做講解。
如果我們看過 map
的原始碼,就會發現其中有不少會引起 fatal
錯誤的地方,比如 mapaccess1
(從 map
中讀取 key
的函數)裡面,如果發現正在寫 map
,則會有 fatal
錯誤。 (如果還沒看過,可以跟著這篇 《go map 設計與實現》 看一下)
if h.flags&hashWriting != 0 { fatal("concurrent map read and map write") }
下面是一個實際使用中的例子:
var m = make(map[int]int) // 往 map 寫 key 的協程 go func() { // 往 map 寫入資料 for i := 0; i < 10000; i++ { m[i] = i } }() // 從 map 讀取 key 的協程 go func() { // 從 map 讀取資料 for i := 10000; i > 0; i-- { _ = m[i] } }() // 等待兩個協程執行完畢 time.Sleep(time.Second)
這會導致報錯:
fatal error: concurrent map read and map write
這是因為我們同時對 map
進行讀寫,而 map
不支援並行讀寫,所以會報錯。如果 map
允許並行讀寫,那麼可能在我們使用的時候會有很多錯亂的情況出現。 (具體如何錯亂,我們可以對比多執行緒的場景思考一下,本文不展開了)。
對於 map
並行讀寫報錯的問題,其中一種解決方案就是使用 sync.Mutex
來保證並行安全, 但是這樣會導致我們在讀寫的時候,都需要加鎖,這樣就會導致效能的下降。
使用 sync.Mutex
來保證並行安全,上面的程式碼可以改成下面這樣:
var m = make(map[int]int) // 互斥鎖 var mu sync.Mutex // 寫 map 的協程 go func() { for i := 0; i < 10000; i++ { mu.Lock() // 寫 map,加互斥鎖 m[i] = i mu.Unlock() } }() // 讀 map 的協程式 go func() { for i := 10000; i > 0; i-- { mu.Lock() // 讀 map,加互斥鎖 _ = m[i] mu.Unlock() } }() time.Sleep(time.Second)
這樣就不會報錯了,但是效能會有所下降,因為我們在讀寫的時候都需要加鎖。(如果需要更高效能,可以繼續讀下去,不要急著使用 sync.Mutex
)
sync.Mutex
的常見的用法是在結構體中嵌入 sync.Mutex
,而不是定義獨立的兩個變數。
在上一小節中,我們使用了 sync.Mutex
來保證並行安全,但是在讀和寫的時候我們都需要加互斥鎖。 這就意味著,就算多個協程進行並行讀,也需要等待鎖。 但是互斥鎖的粒度太大了,但實際上,並行讀是沒有什麼太大問題的,應該被允許才對,如果我們允許並行讀,那麼就可以提高效能。
當然 go 的開發者也考慮到了這一點,所以在 sync
包中提供了 sync.RWMutex
,這個鎖可以允許進行並行讀,但是寫的時候還是需要等待鎖。 也就是說,一個協程在持有寫鎖的時候,其他協程是既不能讀也不能寫的,只能等待寫鎖釋放才能進行讀寫。
使用 sync.RWMutex
來保證並行安全,我們可以改成下面這樣:
var m = make(map[int]int) // 讀寫鎖(允許並行讀,寫的時候是互斥的) var mu sync.RWMutex // 寫入 map 的協程 go func() { for i := 0; i < 10000; i++ { // 寫入的時候需要加鎖 mu.Lock() m[i] = i mu.Unlock() } }() // 讀取 map 的協程 go func() { for i := 10000; i > 0; i-- { // 讀取的時候需要加鎖,但是這個鎖是讀鎖 // 多個協程可以同時使用 RLock 而不需要等待 mu.RLock() _ = m[i] mu.RUnlock() } }() // 另外一個讀取 map 的協程 go func() { for i := 20000; i > 10000; i-- { // 讀取的時候需要加鎖,但是這個鎖是讀鎖 // 多個協程可以同時使用 RLock 而不需要等待 mu.RLock() _ = m[i] mu.RUnlock() } }() time.Sleep(time.Second)
這樣就不會報錯了,而且效能也提高了,因為我們在讀的時候,不需要等待鎖。
說明:
RLock
而不需要等待,這是讀鎖。Lock
,這是寫鎖,有寫鎖的時候,其他協程不能讀也不能寫。Unlock
來釋放鎖。也就是說,使用 sync.RWMutex
的時候,讀操作是可以並行執行的,但是寫操作是互斥的。 這樣一來,相比 sync.Mutex
來說等待鎖的次數就少了,自然也就能獲得更好的效能了。
gin 框架裡面就使用了 sync.RWMutex
來保證 Keys
讀寫操作的並行安全。
通過上面的內容,我們知道了,有下面兩種方式可以保證並行安全:
sync.Mutex
,但是這樣的話,讀寫都是互斥的,效能不好。sync.RWMutex
,可以並行讀,但是寫的時候是互斥的,效能相對 sync.Mutex
要好一些。但是就算我們使用了 sync.RWMutex
,也還是有一些鎖的開銷。那麼我們能不能再優化一下呢?答案是可以的。那就是使用 sync.Map
。
sync.Map
在鎖的基礎上做了進一步優化,在一些場景下使用原子操作來保證並行安全,效能更好。
但是就算使用 sync.RWMutex
,讀操作依然還有鎖的開銷,那麼有沒有更好的方式呢? 答案是有的,就是使用原子操作來替代讀鎖。
舉一個很常見的例子就是多個協程同時讀取一個變數,然後對這個變數進行累加操作:
var a int32 var wg sync.WaitGroup wg.Add(2) go func() { for i := 0; i < 10000; i++ { a++ } wg.Done() }() go func() { for i := 0; i < 10000; i++ { a++ } wg.Done() }() wg.Wait() // a 期望結果應該是 20000才對。 fmt.Println(a) // 實際:17089,而且每次都不一樣
這個例子中,我們期望的結果是 a
的值是 20000
,但是實際上,每次執行的結果都不一樣,而且都不會等於 20000
。 其中很簡單粗暴的一種解決方法是加鎖,但是這樣的話,效能就不好了,但是我們可以使用原子操作來解決這個問題:
var a atomic.Int32 var wg sync.WaitGroup wg.Add(2) go func() { for i := 0; i < 10000; i++ { a.Add(1) } wg.Done() }() go func() { for i := 0; i < 10000; i++ { a.Add(1) } wg.Done() }() wg.Wait() fmt.Println(a.Load()) // 20000
我們來看一下,使用鎖和原子操作的效能差多少:
func BenchmarkMutexAdd(b *testing.B) { var a int32 var mu sync.Mutex for i := 0; i < b.N; i++ { mu.Lock() a++ mu.Unlock() } } func BenchmarkAtomicAdd(b *testing.B) { var a atomic.Int32 for i := 0; i < b.N; i++ { a.Add(1) } }
結果:
BenchmarkMutexAdd-12 100000000 10.07 ns/op
BenchmarkAtomicAdd-12 205196968 5.847 ns/op
我們可以看到,使用原子操作的效能比使用鎖的效能要好一些。
也許我們會覺得上面這個例子是寫操作,那麼讀操作呢?我們來看一下:
func BenchmarkMutex(b *testing.B) { var mu sync.RWMutex for i := 0; i < b.N; i++ { mu.RLock() mu.RUnlock() } } func BenchmarkAtomic(b *testing.B) { var a atomic.Int32 for i := 0; i < b.N; i++ { _ = a.Load() } }
結果:
BenchmarkMutex-12 100000000 10.12 ns/op
BenchmarkAtomic-12 1000000000 0.3133 ns/op
我們可以看到,使用原子操作的效能比使用鎖的效能要好很多。而且在 BenchmarkMutex
裡面甚至還沒有做讀取資料的操作。
sync.Map
裡面相比 sync.RWMutex
,效能更好的原因就是使用了原子操作。 在我們從 sync.Map
裡面讀取資料的時候,會先使用一個原子 Load
操作來讀取 sync.Map
裡面的 key
(從 read
中讀取)。 注意:這裡拿到的是 key
的一份快照,我們對其進行讀操作的時候也可以同時往 sync.Map
中寫入新的 key
,這是保證它高效能的一個很關鍵的設計(類似讀寫分離)。
sync.Map
裡面的 Load
方法裡面就包含了上述的流程:
// Load 方法從 sync.Map 裡面讀取資料。 func (m *Map) Load(key any) (value any, ok bool) { // 先從唯讀 map 裡面讀取資料。 // 這一步是不需要鎖的,只有一個原子操作。 read := m.loadReadOnly() e, ok := read.m[key] if !ok && read.amended { // 如果沒有找到,並且 dirty 裡面有一些 read 中沒有的 key,那麼就需要從 dirty 裡面讀取資料。 // 這裡才需要鎖 m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] m.missLocked() } m.mu.Unlock() } // key 不存在 if !ok { return nil, false } // 使用原子操作讀取 return e.Load() }
上面的程式碼我們可能還看不懂,但是沒關係,這裡我們只需要知道的是,從 sync.Map 讀取資料的時候,會先做原子操作,如果沒找到,再進行加鎖操作,這樣就減少了使用鎖的頻率了,自然也就可以獲得更好的效能(但要注意的是並不是所有情況下都能獲得更好的效能)。至於具體實現,在下一篇文章中會進行更加詳細的分析。
也就是說,sync.Map 之所以更快,是因為相比 RWMutex,進一步減少了鎖的使用,而這也就是 sync.Map 存在的原因了
現在我們知道了,sync.Map
裡面是利用了原子操作來減少鎖的使用。但是我們好像連 sync.Map
的一些基本操作都還不瞭解,現在就讓我們再來看看 sync.Map
的基本用法。
sync.Map
的使用還是挺簡單的,map
中有的操作,在 sync.Map
都有,只不過區別是,在 sync.Map
中,所有的操作都需要通過呼叫其方法來進行。 sync.Map
裡面幾個常用的方法有(CRUD
):
Store
:我們新增或者修改資料的時候,都可以使用 Store
方法。Load
:讀取資料的方法。Range
:遍歷資料的方法。Delete
:刪除資料的方法。var m sync.Map // 寫入/修改 m.Store("foo", 1) // 讀取 fmt.Println(m.Load("foo")) // 1 true // 遍歷 m.Range(func(key, value interface{}) bool { fmt.Println(key, value) // foo 1 return true }) // 刪除 m.Delete("foo") fmt.Println(m.Load("foo")) // nil false
注意:在 sync.Map
中,key
和 value
都是 interface{}
型別的,也就是說,我們可以使用任意型別的 key
和 value
。 而不像 map
,只能存在一種型別的 key
和 value
。從這個角度來看,它的型別類似於 map[any]any
。
另外一個需要注意的是,Range
方法的引數是一個函數,這個函數如果返回 false
,那麼遍歷就會停止。
在 sync.Map
原始碼中,已經告訴了我們 sync.Map
的使用場景:
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.
翻譯過來就是,Map 型別針對兩種常見用例進行了優化:
key
的條目只寫入一次但讀取多次時,如在只會增長的快取中。(讀多寫少)在這兩種情況下,與 Go map
與單獨的 Mutex
或 RWMutex
配對相比,使用 sync.Map
可以顯著減少鎖競爭(很多時候只需要原子操作就可以)。
普通的 map
不支援並行讀寫。
有以下兩種方式可以實現 map
的並行讀寫:
sync.Mutex
互斥鎖。讀和寫的時候都使用互斥鎖,效能相比 sync.RWMutex
會差一些。sync.RWMutex
讀寫鎖。讀的鎖是可以共用的,但是寫鎖是獨佔的。效能相比 sync.Mutex
會好一些。sync.Map
裡面會先進行原子操作來讀取 key
,如果讀取不到的時候,才會需要加鎖。所以效能相比 sync.Mutex
和 sync.RWMutex
會好一些。sync.Map
裡面幾個常用的方法有(CRUD
):
Store
:我們新增或者修改資料的時候,都可以使用 Store
方法。Load
:讀取資料的方法。Range
:遍歷資料的方法。Delete
:刪除資料的方法。sync.Map
的使用場景,sync.Map
針對以下兩種場景做了優化:
key
只會寫入一次,但是會被讀取多次的場景。以上就是go sync.Map基本原理深入解析的詳細內容,更多關於go sync.Map基本原理的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45