欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

go sync.Map基本原理深入解析

 更新時間:2023年01月19日 11:39:06   作者:eleven26  
這篇文章主要為大家介紹了go sync.Map基本原理深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

我們知道,go 里面提供了 map 這種類型讓我們可以存儲鍵值對數(shù)據(jù),但是如果我們在并發(fā)的情況下使用 map 的話,就會發(fā)現(xiàn)它是不支持并發(fā)地進行讀寫的(會報錯)。 在這種情況下,我們可以使用 sync.Mutex 來保證并發(fā)安全,但是這樣會導(dǎo)致我們在讀寫的時候,都需要加鎖,這樣就會導(dǎo)致性能的下降。

除了使用互斥鎖這種相對低效的方式,我們還可以使用 sync.Map 來保證并發(fā)安全,它在某些場景下有比使用 sync.Mutex 更高的性能。 本文就來探討一下 sync.Map 中的一些大家比較感興趣的問題,比如為什么有了 map 還要 sync.Map?它為什么快?sync.Map 的適用場景(注意:不是所有情況下都快。)等。

關(guān)于 sync.Map 的設(shè)計與實現(xiàn)原理,會在下一篇中再做講解。

map 在并發(fā)下的問題

如果我們看過 map 的源碼,就會發(fā)現(xiàn)其中有不少會引起 fatal 錯誤的地方,比如 mapaccess1(從 map 中讀取 key 的函數(shù))里面,如果發(fā)現(xiàn)正在寫 map,則會有 fatal 錯誤。 (如果還沒看過,可以跟著這篇 《go map 設(shè)計與實現(xiàn)》 看一下)

if h.flags&hashWriting != 0 {
    fatal("concurrent map read and map write")
}

map 并發(fā)讀寫異常的例子

下面是一個實際使用中的例子:

var m = make(map[int]int)
// 往 map 寫 key 的協(xié)程
go func() {
   // 往 map 寫入數(shù)據(jù)
    for i := 0; i < 10000; i++ {
        m[i] = i
    }
}()
// 從 map 讀取 key 的協(xié)程
go func() {
   // 從 map 讀取數(shù)據(jù)
    for i := 10000; i > 0; i-- {
        _ = m[i]
    }
}()
// 等待兩個協(xié)程執(zhí)行完畢
time.Sleep(time.Second)

這會導(dǎo)致報錯:

fatal error: concurrent map read and map write

這是因為我們同時對 map 進行讀寫,而 map 不支持并發(fā)讀寫,所以會報錯。如果 map 允許并發(fā)讀寫,那么可能在我們使用的時候會有很多錯亂的情況出現(xiàn)。 (具體如何錯亂,我們可以對比多線程的場景思考一下,本文不展開了)。

使用 sync.Mutex 保證并發(fā)安全

對于 map 并發(fā)讀寫報錯的問題,其中一種解決方案就是使用 sync.Mutex 來保證并發(fā)安全, 但是這樣會導(dǎo)致我們在讀寫的時候,都需要加鎖,這樣就會導(dǎo)致性能的下降。

使用 sync.Mutex 來保證并發(fā)安全,上面的代碼可以改成下面這樣:

var m = make(map[int]int)
// 互斥鎖
var mu sync.Mutex
// 寫 map 的協(xié)程
go func() {
    for i := 0; i < 10000; i++ {
        mu.Lock() // 寫 map,加互斥鎖
        m[i] = i
        mu.Unlock()
    }
}()
// 讀 map 的協(xié)程序
go func() {
    for i := 10000; i > 0; i-- {
        mu.Lock() // 讀 map,加互斥鎖
        _ = m[i]
        mu.Unlock()
    }
}()
time.Sleep(time.Second)

這樣就不會報錯了,但是性能會有所下降,因為我們在讀寫的時候都需要加鎖。(如果需要更高性能,可以繼續(xù)讀下去,不要急著使用 sync.Mutex

sync.Mutex 的常見的用法是在結(jié)構(gòu)體中嵌入 sync.Mutex,而不是定義獨立的兩個變量。

使用 sync.RWMutex 保證并發(fā)安全

在上一小節(jié)中,我們使用了 sync.Mutex 來保證并發(fā)安全,但是在讀和寫的時候我們都需要加互斥鎖。 這就意味著,就算多個協(xié)程進行并發(fā)讀,也需要等待鎖。 但是互斥鎖的粒度太大了,但實際上,并發(fā)讀是沒有什么太大問題的,應(yīng)該被允許才對,如果我們允許并發(fā)讀,那么就可以提高性能。

當(dāng)然 go 的開發(fā)者也考慮到了這一點,所以在 sync 包中提供了 sync.RWMutex,這個鎖可以允許進行并發(fā)讀,但是寫的時候還是需要等待鎖。 也就是說,一個協(xié)程在持有寫鎖的時候,其他協(xié)程是既不能讀也不能寫的,只能等待寫鎖釋放才能進行讀寫。

使用 sync.RWMutex 來保證并發(fā)安全,我們可以改成下面這樣:

var m = make(map[int]int)
// 讀寫鎖(允許并發(fā)讀,寫的時候是互斥的)
var mu sync.RWMutex
// 寫入 map 的協(xié)程
go func() {
    for i := 0; i < 10000; i++ {
        // 寫入的時候需要加鎖
        mu.Lock()
        m[i] = i
        mu.Unlock()
    }
}()
// 讀取 map 的協(xié)程
go func() {
    for i := 10000; i > 0; i-- {
        // 讀取的時候需要加鎖,但是這個鎖是讀鎖
        // 多個協(xié)程可以同時使用 RLock 而不需要等待
        mu.RLock()
        _ = m[i]
        mu.RUnlock()
    }
}()
// 另外一個讀取 map 的協(xié)程
go func() {
    for i := 20000; i > 10000; i-- {
        // 讀取的時候需要加鎖,但是這個鎖是讀鎖
        // 多個協(xié)程可以同時使用 RLock 而不需要等待
        mu.RLock()
        _ = m[i]
        mu.RUnlock()
    }
}()
time.Sleep(time.Second)

這樣就不會報錯了,而且性能也提高了,因為我們在讀的時候,不需要等待鎖。

說明:

  • 多個協(xié)程可以同時使用 RLock 而不需要等待,這是讀鎖。
  • 只有一個協(xié)程可以使用 Lock,這是寫鎖,有寫鎖的時候,其他協(xié)程不能讀也不能寫。
  • 持有寫鎖的協(xié)程,可以使用 Unlock 來釋放鎖。
  • 寫鎖釋放之后,其他協(xié)程才能獲取到鎖(讀鎖或者寫鎖)。

也就是說,使用 sync.RWMutex 的時候,讀操作是可以并發(fā)執(zhí)行的,但是寫操作是互斥的。 這樣一來,相比 sync.Mutex 來說等待鎖的次數(shù)就少了,自然也就能獲得更好的性能了。

gin 框架里面就使用了 sync.RWMutex 來保證 Keys 讀寫操作的并發(fā)安全。

有了讀寫鎖為什么還要有 sync.Map?

通過上面的內(nèi)容,我們知道了,有下面兩種方式可以保證并發(fā)安全:

  • 使用 sync.Mutex,但是這樣的話,讀寫都是互斥的,性能不好。
  • 使用 sync.RWMutex,可以并發(fā)讀,但是寫的時候是互斥的,性能相對 sync.Mutex 要好一些。

但是就算我們使用了 sync.RWMutex,也還是有一些鎖的開銷。那么我們能不能再優(yōu)化一下呢?答案是可以的。那就是使用 sync.Map

sync.Map 在鎖的基礎(chǔ)上做了進一步優(yōu)化,在一些場景下使用原子操作來保證并發(fā)安全,性能更好。

使用原子操作替代讀鎖

但是就算使用 sync.RWMutex,讀操作依然還有鎖的開銷,那么有沒有更好的方式呢? 答案是有的,就是使用原子操作來替代讀鎖。

舉一個很常見的例子就是多個協(xié)程同時讀取一個變量,然后對這個變量進行累加操作:

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 期望結(jié)果應(yīng)該是 20000才對。
fmt.Println(a) // 實際:17089,而且每次都不一樣

這個例子中,我們期望的結(jié)果是 a 的值是 20000,但是實際上,每次運行的結(jié)果都不一樣,而且都不會等于 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)
   }
}

結(jié)果:

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()
   }
}

結(jié)果:

BenchmarkMutex-12      100000000          10.12 ns/op
BenchmarkAtomic-12     1000000000          0.3133 ns/op

我們可以看到,使用原子操作的性能比使用鎖的性能要好很多。而且在 BenchmarkMutex 里面甚至還沒有做讀取數(shù)據(jù)的操作。

sync.Map 里面的原子操作

sync.Map 里面相比 sync.RWMutex,性能更好的原因就是使用了原子操作。 在我們從 sync.Map 里面讀取數(shù)據(jù)的時候,會先使用一個原子 Load 操作來讀取 sync.Map 里面的 key(從 read 中讀取)。 注意:這里拿到的是 key 的一份快照,我們對其進行讀操作的時候也可以同時往 sync.Map 中寫入新的 key,這是保證它高性能的一個很關(guān)鍵的設(shè)計(類似讀寫分離)。

sync.Map 里面的 Load 方法里面就包含了上述的流程:

// Load 方法從 sync.Map 里面讀取數(shù)據(jù)。
func (m *Map) Load(key any) (value any, ok bool) {
   // 先從只讀 map 里面讀取數(shù)據(jù)。
   // 這一步是不需要鎖的,只有一個原子操作。
   read := m.loadReadOnly()
   e, ok := read.m[key]
   if !ok && read.amended { // 如果沒有找到,并且 dirty 里面有一些 read 中沒有的 key,那么就需要從 dirty 里面讀取數(shù)據(jù)。
      // 這里才需要鎖
      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()
}

上面的代碼我們可能還看不懂,但是沒關(guān)系,這里我們只需要知道的是,從 sync.Map 讀取數(shù)據(jù)的時候,會先做原子操作,如果沒找到,再進行加鎖操作,這樣就減少了使用鎖的頻率了,自然也就可以獲得更好的性能(但要注意的是并不是所有情況下都能獲得更好的性能)。至于具體實現(xiàn),在下一篇文章中會進行更加詳細的分析。

也就是說,sync.Map 之所以更快,是因為相比 RWMutex,進一步減少了鎖的使用,而這也就是 sync.Map 存在的原因了

sync.Map 的基本用法

現(xiàn)在我們知道了,sync.Map 里面是利用了原子操作來減少鎖的使用。但是我們好像連 sync.Map 的一些基本操作都還不了解,現(xiàn)在就讓我們再來看看 sync.Map 的基本用法。

sync.Map 的使用還是挺簡單的,map 中有的操作,在 sync.Map 都有,只不過區(qū)別是,在 sync.Map 中,所有的操作都需要通過調(diào)用其方法來進行。 sync.Map 里面幾個常用的方法有(CRUD):

  • Store:我們新增或者修改數(shù)據(jù)的時候,都可以使用 Store 方法。
  • Load:讀取數(shù)據(jù)的方法。
  • Range:遍歷數(shù)據(jù)的方法。
  • Delete:刪除數(shù)據(jù)的方法。
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 中,keyvalue 都是 interface{} 類型的,也就是說,我們可以使用任意類型的 keyvalue。 而不像 map,只能存在一種類型的 keyvalue。從這個角度來看,它的類型類似于 map[any]any

另外一個需要注意的是,Range 方法的參數(shù)是一個函數(shù),這個函數(shù)如果返回 false,那么遍歷就會停止。

sync.Map 的使用場景

sync.Map 源碼中,已經(jīng)告訴了我們 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 類型針對兩種常見用例進行了優(yōu)化:

  • 當(dāng)給定 key 的條目只寫入一次但讀取多次時,如在只會增長的緩存中。(讀多寫少)
  • 當(dāng)多個 goroutine 讀取、寫入和覆蓋不相交的鍵集的條目。(不同 goroutine 操作不同的 key)

在這兩種情況下,與 Go map 與單獨的 MutexRWMutex 配對相比,使用 sync.Map 可以顯著減少鎖競爭(很多時候只需要原子操作就可以)。

總結(jié)

普通的 map 不支持并發(fā)讀寫。

有以下兩種方式可以實現(xiàn) map 的并發(fā)讀寫:

  • 使用 sync.Mutex 互斥鎖。讀和寫的時候都使用互斥鎖,性能相比 sync.RWMutex 會差一些。
  • 使用 sync.RWMutex 讀寫鎖。讀的鎖是可以共享的,但是寫鎖是獨占的。性能相比 sync.Mutex 會好一些。
  • sync.Map 里面會先進行原子操作來讀取 key,如果讀取不到的時候,才會需要加鎖。所以性能相比 sync.Mutexsync.RWMutex 會好一些。

sync.Map 里面幾個常用的方法有(CRUD):

  • Store:我們新增或者修改數(shù)據(jù)的時候,都可以使用 Store 方法。
  • Load:讀取數(shù)據(jù)的方法。
  • Range:遍歷數(shù)據(jù)的方法。
  • Delete:刪除數(shù)據(jù)的方法。

sync.Map 的使用場景,sync.Map 針對以下兩種場景做了優(yōu)化:

  • key 只會寫入一次,但是會被讀取多次的場景。
  • 多個 goroutine 讀取、寫入和覆蓋不相交的鍵集的條目。

以上就是go sync.Map基本原理深入解析的詳細內(nèi)容,更多關(guān)于go sync.Map基本原理的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • golang 使用 viper 讀取自定義配置文件

    golang 使用 viper 讀取自定義配置文件

    這篇文章主要介紹了golang 使用 viper 讀取自定義配置文件,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-01-01
  • RabbitMQ延時消息隊列在golang中的使用詳解

    RabbitMQ延時消息隊列在golang中的使用詳解

    延時隊列常使用在某些業(yè)務(wù)場景,使用延時隊列可以簡化系統(tǒng)的設(shè)計和開發(fā)、提高系統(tǒng)的可靠性和可用性、提高系統(tǒng)的性能,下面我們就來看看如何在golang中使用RabbitMQ的延時消息隊列吧
    2023-11-11
  • Go語言操作MySQL的知識總結(jié)

    Go語言操作MySQL的知識總結(jié)

    Go語言中的database/sql包提供了保證SQL或類SQL數(shù)據(jù)庫的泛用接口,并不提供具體的數(shù)據(jù)庫驅(qū)動。本文介紹了Go語言操作MySQL的相關(guān)知識,感興趣的可以了解一下
    2022-11-11
  • 詳解如何使用pprof簡單檢測和修復(fù)Go語言中的內(nèi)存泄漏

    詳解如何使用pprof簡單檢測和修復(fù)Go語言中的內(nèi)存泄漏

    雖然?Go?有自動垃圾回收(GC),它能回收不再被使用的內(nèi)存,但這并不意味著?Go?程序中不會發(fā)生內(nèi)存泄漏,下面我們就來看看如何使用pprof進行檢測和修復(fù)Go語言中的內(nèi)存泄漏吧
    2025-01-01
  • 淺談Go中SIGTERM信的實現(xiàn)

    淺談Go中SIGTERM信的實現(xiàn)

    在Go語言中優(yōu)雅處理SIGTERM信號需通過os/signal包實現(xiàn),包括信號注冊、異步監(jiān)聽和資源清理,具有一定的參考價值,感興趣的可以了解一下
    2025-09-09
  • 利用go語言實現(xiàn)查找二叉樹中的最大寬度

    利用go語言實現(xiàn)查找二叉樹中的最大寬度

    這篇文章主要介紹了利用go語言實現(xiàn)查找二叉樹中的最大寬度,文章圍繞主題展開詳細介紹,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-05-05
  • golang判斷兩個事件是否存在沖突的方法示例

    golang判斷兩個事件是否存在沖突的方法示例

    這篇文章主要為大家詳細介紹了golang判斷兩個事件是否存在沖突的方法示例,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-10-10
  • Go-客戶信息關(guān)系系統(tǒng)的實現(xiàn)

    Go-客戶信息關(guān)系系統(tǒng)的實現(xiàn)

    這篇文章主要介紹了Go-客戶信息關(guān)系系統(tǒng)的實現(xiàn),本文章內(nèi)容詳細,具有很好的參考價值,希望對大家有所幫助,需要的朋友可以參考下
    2023-01-01
  • 利用Go語言實現(xiàn)Raft日志同步

    利用Go語言實現(xiàn)Raft日志同步

    這篇文章主要為大家詳細介紹了如何利用Go語言實現(xiàn)Raft日志同步,文中的示例代碼講解詳細,對我們深入了解Go語言有一定的幫助,需要的可以參考一下
    2023-05-05
  • golang-gorm自動建表問題

    golang-gorm自動建表問題

    這篇文章主要介紹了golang-gorm自動建表問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-02-02

最新評論