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

golang中三種線程安全的MAP小結(jié)

 更新時(shí)間:2024年08月15日 10:04:55   作者:私念  
在Go語(yǔ)言中,Map是并發(fā)不安全的,本文主要介紹了golang中三種線程安全的MAP小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

一、map 是什么

map 是 Go 中用于存儲(chǔ) key-value 關(guān)系數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu),類似 C++ 中的 map,Python 中的 dict。Go 中 map 的使用很簡(jiǎn)單,但是對(duì)于初學(xué)者,經(jīng)常會(huì)犯兩個(gè)錯(cuò)誤:沒(méi)有初始化,并發(fā)讀寫。

1、未初始化的 map 都是 nil,直接賦值會(huì)報(bào) panic。map 作為結(jié)構(gòu)體成員的時(shí)候,很容易忘記對(duì)它的初始化。

2、并發(fā)讀寫是我們使用 map 中很常見(jiàn)的一個(gè)錯(cuò)誤。多個(gè)協(xié)程并發(fā)讀寫同一個(gè) key 的時(shí)候,會(huì)出現(xiàn)沖突,導(dǎo)致 panic。

Go 內(nèi)置的 map 類型并沒(méi)有對(duì)并發(fā)場(chǎng)景場(chǎng)景進(jìn)行優(yōu)化,但是并發(fā)場(chǎng)景又很常見(jiàn),如何實(shí)現(xiàn)線程安全(并發(fā)安全)的 map就很重要了 

二、三種線程安全的 map

1、加讀寫鎖(RWMutex)

這是最容易想到的一種方式。常見(jiàn)的 map 的操作有增刪改查和遍歷,這里面查和遍歷是讀操作,增刪改是寫操作,因此對(duì)查和遍歷需要加讀鎖,對(duì)增刪改需要加寫鎖。

以 map[int]int 為例,借助 RWMutex,具體的實(shí)現(xiàn)方式如下:

type RWMap struct { // 一個(gè)讀寫鎖保護(hù)的線程安全的map
    sync.RWMutex // 讀寫鎖保護(hù)下面的map字段
    m map[int]int
}
// 新建一個(gè)RWMap
func NewRWMap(n int) *RWMap {
    return &RWMap{
        m: make(map[int]int, n),
    }
}
func (m *RWMap) Get(k int) (int, bool) { //從map中讀取一個(gè)值
    m.RLock()
    defer m.RUnlock()
    v, existed := m.m[k] // 在鎖的保護(hù)下從map中讀取
    return v, existed
}
func (m *RWMap) Set(k int, v int) { // 設(shè)置一個(gè)鍵值對(duì)
    m.Lock()              // 鎖保護(hù)
    defer m.Unlock()
    m.m[k] = v
}
func (m *RWMap) Delete(k int) { //刪除一個(gè)鍵
    m.Lock()                   // 鎖保護(hù)
    defer m.Unlock()
    delete(m.m, k)
}
func (m *RWMap) Len() int { // map的長(zhǎng)度
    m.RLock()   // 鎖保護(hù)
    defer m.RUnlock()
    return len(m.m)
}
func (m *RWMap) Each(f func(k, v int) bool) { // 遍歷map
    m.RLock()             //遍歷期間一直持有讀鎖
    defer m.RUnlock()
    for k, v := range m.m {
        if !f(k, v) {
            return
        }
    }
}

2、分片加鎖

通過(guò)讀寫鎖 RWMutex 實(shí)現(xiàn)的線程安全的 map,功能上已經(jīng)完全滿足了需要,但是面對(duì)高并發(fā)的場(chǎng)景,僅僅功能滿足可不行,性能也得跟上。鎖是性能下降的萬(wàn)惡之源之一。所以并發(fā)編程的原則就是盡可能減少鎖的使用。當(dāng)鎖不得不用的時(shí)候,可以減小鎖的粒度和持有的時(shí)間。

在第一種方法中,加鎖的對(duì)象是整個(gè) map,協(xié)程 A 對(duì) map 中的 key 進(jìn)行修改操作,會(huì)導(dǎo)致其它協(xié)程無(wú)法對(duì)其它 key 進(jìn)行讀寫操作。一種解決思路是將這個(gè) map 分成 n 塊,每個(gè)塊之間的讀寫操作都互不干擾,從而降低沖突的可能性。

Go 比較知名的分片 map 的實(shí)現(xiàn)是 orcaman/concurrent-map,它的定義如下:

var SHARD_COUNT = 32   
// 分成SHARD_COUNT個(gè)分片的map
type ConcurrentMap []*ConcurrentMapShared
// 通過(guò)RWMutex保護(hù)的線程安全的分片,包含一個(gè)map
type ConcurrentMapShared struct {
    items        map[string]interface{}
    sync.RWMutex // Read Write mutex, guards access to internal map.
}
// 創(chuàng)建并發(fā)map
func New() ConcurrentMap {
    m := make(ConcurrentMap, SHARD_COUNT)
    for i := 0; i < SHARD_COUNT; i++ {
        m[i] = &ConcurrentMapShared{items: make(map[string]interface{})}
    }
    return m
}
// 根據(jù)key計(jì)算分片索引
func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared {
    return m[uint(fnv32(key))%uint(SHARD_COUNT)]
}

ConcurrentMap 其實(shí)就是一個(gè)切片,切片的每個(gè)元素都是第一種方法中攜帶了讀寫鎖的 map。

這里面 GetShard 方法就是用來(lái)計(jì)算每一個(gè) key 應(yīng)該分配到哪個(gè)分片上。再來(lái)看一下 Set 和 Get 操作。

func (m ConcurrentMap) Set(key string, value interface{}) {
    // 根據(jù)key計(jì)算出對(duì)應(yīng)的分片
    shard := m.GetShard(key)
    shard.Lock() //對(duì)這個(gè)分片加鎖,執(zhí)行業(yè)務(wù)操作
    shard.items[key] = value
    shard.Unlock()
}
func (m ConcurrentMap) Get(key string) (interface{}, bool) {
    // 根據(jù)key計(jì)算出對(duì)應(yīng)的分片
    shard := m.GetShard(key)
    shard.RLock()
    // 從這個(gè)分片讀取key的值
    val, ok := shard.items[key]
    shard.RUnlock()
    return val, ok
}

Get 和 Set 方法類似,都是根據(jù) key 用 GetShard 計(jì)算出分片索引,找到對(duì)應(yīng)的 map 塊,執(zhí)行讀寫操作。

3、sync 中的 map

分片加鎖的思路是將大塊的數(shù)據(jù)切分成小塊的數(shù)據(jù),從而減少?zèng)_突導(dǎo)致鎖阻塞的可能性。如果在一些特殊的場(chǎng)景下,將讀寫數(shù)據(jù)分開,是不是能在進(jìn)一步提升性能呢?

在內(nèi)置的 sync 包中(Go 1.9+)也有一個(gè)線程安全的 map,通過(guò)將讀寫分離的方式實(shí)現(xiàn)了某些特定場(chǎng)景下的性能提升。

其實(shí)在生產(chǎn)環(huán)境中,sync.map 用的很少,官方文檔推薦的兩種使用場(chǎng)景是:

a) when the entry for a given key is only ever written once but read many times, as in caches that only grow.
b) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys.

兩種場(chǎng)景都比較苛刻,要么是一寫多讀,要么是各個(gè)協(xié)程操作的 key 集合沒(méi)有交集(或者交集很少)。所以官方建議先對(duì)自己的場(chǎng)景做性能測(cè)評(píng),如果確實(shí)能顯著提高性能,再使用 sync.map。

sync.map 的整體思路就是用兩個(gè)數(shù)據(jù)結(jié)構(gòu)(只讀的 read 和可寫的 dirty)盡量將讀寫操作分開,來(lái)減少鎖對(duì)性能的影響。

下面詳細(xì)看下 sync.map 的定義和增刪改查實(shí)現(xiàn)。

sync.map 數(shù)據(jù)結(jié)構(gòu)定義

type Map struct {
    mu Mutex
    // 基本上你可以把它看成一個(gè)安全的只讀的map
    // 它包含的元素其實(shí)也是通過(guò)原子操作更新的,但是已刪除的entry就需要加鎖操作了
    read atomic.Value // readOnly
    // 包含需要加鎖才能訪問(wèn)的元素
    // 包括所有在read字段中但未被expunged(刪除)的元素以及新加的元素
    dirty map[interface{}]*entry
    // 記錄從read中讀取miss的次數(shù),一旦miss數(shù)和dirty長(zhǎng)度一樣了,就會(huì)把dirty提升為read,并把dirty置空
    misses int
}
type readOnly struct {
    m       map[interface{}]*entry
    amended bool // 當(dāng)dirty中包含read沒(méi)有的數(shù)據(jù)時(shí)為true,比如新增一條數(shù)據(jù)
}
// expunged是用來(lái)標(biāo)識(shí)此項(xiàng)已經(jīng)刪掉的指針
// 當(dāng)map中的一個(gè)項(xiàng)目被刪除了,只是把它的值標(biāo)記為expunged,以后才有機(jī)會(huì)真正刪除此項(xiàng)
var expunged = unsafe.Pointer(new(interface{}))
// entry代表一個(gè)值
type entry struct {
    p unsafe.Pointer // *interface{}
}

Map 的定義中,read 字段通過(guò) atomic.Values 存儲(chǔ)被高頻讀的 readOnly 類型的數(shù)據(jù)。dirty 存儲(chǔ)

Store 方法

Store 方法用來(lái)設(shè)置一個(gè)鍵值對(duì),或者更新一個(gè)鍵值對(duì)。

func (m *Map) Store(key, value interface{}) {
    read, _ := m.read.Load().(readOnly)
    // 如果read字段包含這個(gè)項(xiàng),說(shuō)明是更新,cas更新項(xiàng)目的值即可
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    // read中不存在,或者cas更新失敗,就需要加鎖訪問(wèn)dirty了
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok { // 雙檢查,看看read是否已經(jīng)存在了
        if e.unexpungeLocked() {
            // 此項(xiàng)目先前已經(jīng)被刪除了,需要添加到 dirty 中
            m.dirty[key] = e
        }
        e.storeLocked(&value) // 更新
    } else if e, ok := m.dirty[key]; ok { // 如果dirty中有此項(xiàng)
        e.storeLocked(&value) // 直接更新
    } else { // 否則就是一個(gè)新的key
        if !read.amended { //如果dirty為nil
            // 需要?jiǎng)?chuàng)建dirty對(duì)象,并且標(biāo)記read的amended為true,
            // 說(shuō)明有元素它不包含而dirty包含
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value) //將新值增加到dirty對(duì)象中
    }
    m.mu.Unlock()
}
// tryStore利用 cas 操作來(lái)更新value。
// 更新之前會(huì)判斷這個(gè)鍵值對(duì)有沒(méi)有被打上刪除的標(biāo)記
func (e *entry) tryStore(i *interface{}) bool {
    for {
        p := atomic.LoadPointer(&e.p)
        if p == expunged {
            return false
        }
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
            return true
        }
    }
}
// 將值設(shè)置成 nil,表示沒(méi)有被刪除
func (e *entry) unexpungeLocked() (wasExpunged bool) {
    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
// 通過(guò)復(fù)制 read 生成 dirty
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
        }
    }
}
// 標(biāo)記刪除
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}

第2-6行,通過(guò) cas 進(jìn)行鍵值對(duì)更新,更新成功直接返回。

第8-28行,通過(guò)互斥鎖加鎖來(lái)處理處理新增鍵值對(duì)和更新失敗的場(chǎng)景(鍵值對(duì)被標(biāo)記刪除)。

第11行,再次檢查 read 中是否已經(jīng)存在要 Store 的 key(雙檢查是因?yàn)橹皺z查的時(shí)候沒(méi)有加鎖,中途可能有協(xié)程修改了 read)。

如果該鍵值對(duì)之前被標(biāo)記刪除,先將這個(gè)鍵值對(duì)寫到 dirty 中,同時(shí)更新 read。

如果 dirty 中已經(jīng)有這一項(xiàng)了,直接更新 read。

如果是一個(gè)新的 key。dirty 為空的情況下通過(guò)復(fù)制 read 創(chuàng)建 dirty,不為空的情況下直接更新 dirty。

Load 方法

Load 方法比較簡(jiǎn)單,先是從 read 中讀數(shù)據(jù),讀不到,再通過(guò)互斥鎖鎖從 dirty 中讀數(shù)據(jù)。

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 首先從read處理
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended { // 如果不存在并且dirty不為nil(有新的元素)
        m.mu.Lock()
        // 雙檢查,看看read中現(xiàn)在是否存在此key
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {//依然不存在,并且dirty不為nil
            e, ok = m.dirty[key]// 從dirty中讀取
            // 不管dirty中存不存在,miss數(shù)都加1
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load() //返回讀取的對(duì)象,e既可能是從read中獲得的,也可能是從dirty中獲得的
}
func (m *Map) missLocked() {
    m.misses++ // misses計(jì)數(shù)加一
    if m.misses < len(m.dirty) { // 如果沒(méi)達(dá)到閾值(dirty字段的長(zhǎng)度),返回
        return
    }
    m.read.Store(readOnly{m: m.dirty}) //把dirty字段的內(nèi)存提升為read字段
    m.dirty = nil // 清空dirty
    m.misses = 0  // misses數(shù)重置為0
}

這里需要注意的是,如果出現(xiàn)多次從 read 中讀不到數(shù)據(jù),得到 dirty 中讀取的情況,就直接把 dirty 升級(jí)成 read,以提高 read 效率。

Delete 方法

下面是 Go1.13 中 Delete 的實(shí)現(xiàn)方式,如果 key 在 read 中,就將值置成 nil;如果在 dirty 中,直接刪除 key。

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 { // 說(shuō)明可能在
            delete(m.dirty, key)
        }
        m.mu.Unlock()
    }
    if ok {
        e.delete()
    }
}
func (e *entry) delete() (hadValue bool) {
    for {
        p := atomic.LoadPointer(&e.p)
        if p == nil || p == expunged {
            return false
        }
        if atomic.CompareAndSwapPointer(&e.p, p, nil) {
            return true
        }
    }
}

補(bǔ)充說(shuō)明一下,delete() 執(zhí)行完之后,e.p 變成 nil,下次 Store 的時(shí)候,執(zhí)行到 dirtyLocked() 這一步的時(shí)候,會(huì)被標(biāo)記成 enpunged。因此在 read 中 nil 和 enpunged 都表示刪除狀態(tài)。

sync.map 總結(jié)

上面對(duì)源碼粗略的梳理了一遍,最后在總結(jié)一下 sync.map 的實(shí)現(xiàn)思路:

  • 讀寫分離。讀(更新)相關(guān)的操作盡量通過(guò)不加鎖的 read 實(shí)現(xiàn),寫(新增)相關(guān)的操作通過(guò) dirty 加鎖實(shí)現(xiàn)。
  • 動(dòng)態(tài)調(diào)整。新寫入的 key 都只存在 dirty 中,如果 dirty 中的 key 被多次讀取,dirty 就會(huì)上升成不需要加鎖的 read。
  • 延遲刪除。Delete 只是把被刪除的 key 標(biāo)記成 nil,新增 key-value 的時(shí)候,標(biāo)記成 enpunged;dirty 上升成 read 的時(shí)候,標(biāo)記刪除的 key 被批量移出 map。這樣的好處是 dirty 變成 read 之前,這些 key 都會(huì)命中 read,而 read 不需要加鎖,無(wú)論是讀還是更新,性能都很高。

總結(jié)了 sync.map 的設(shè)計(jì)思路后,我們就能理解官方文檔推薦的 sync.map 的兩種應(yīng)用場(chǎng)景了。

三、總結(jié)

Go 內(nèi)置的 map 使用起來(lái)很方便,但是在并發(fā)頻繁的 Go 程序中很容易出現(xiàn)并發(fā)讀寫沖突導(dǎo)致的問(wèn)題。本文介紹了三種常見(jiàn)的線程安全 map 的實(shí)現(xiàn)方式,分別是讀寫鎖、分片鎖和 sync.map。

較常使用的是前兩種,而在特定的場(chǎng)景下,sync.map 的性能會(huì)有更優(yōu)的表現(xiàn)。

到此這篇關(guān)于golang中三種線程安全的MAP小結(jié)的文章就介紹到這了,更多相關(guān)golang 線程安全的MAP內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 一文帶你搞懂Golang如何正確退出Goroutine

    一文帶你搞懂Golang如何正確退出Goroutine

    在Go語(yǔ)言中,Goroutine是一種輕量級(jí)線程,它的退出機(jī)制對(duì)于并發(fā)編程至關(guān)重要,下午就來(lái)介紹幾種Goroutine的退出機(jī)制,希望對(duì)大家有所幫助
    2023-06-06
  • 使用go操作redis的有序集合(zset)

    使用go操作redis的有序集合(zset)

    這篇文章主要介紹了使用go操作redis的有序集合(zset),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-12-12
  • golang使用swagger的過(guò)程詳解

    golang使用swagger的過(guò)程詳解

    這篇文章主要介紹了golang使用swagger的過(guò)程詳解,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧
    2024-06-06
  • golang封裝一個(gè)執(zhí)行命令行的函數(shù)(return?stderr/stdout/exitcode)示例代碼

    golang封裝一個(gè)執(zhí)行命令行的函數(shù)(return?stderr/stdout/exitcode)示例代碼

    在?Go?語(yǔ)言中,您可以使用?os/exec?包來(lái)執(zhí)行外部命令,不通過(guò)調(diào)用?shell,并且能夠獲得進(jìn)程的退出碼、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤輸出,下面給大家分享golang封裝一個(gè)執(zhí)行命令行的函數(shù)(return?stderr/stdout/exitcode)的方法,感興趣的朋友跟隨小編一起看看吧
    2024-06-06
  • golang實(shí)現(xiàn)實(shí)時(shí)監(jiān)聽(tīng)文件并自動(dòng)切換目錄

    golang實(shí)現(xiàn)實(shí)時(shí)監(jiān)聽(tīng)文件并自動(dòng)切換目錄

    這篇文章主要給大家介紹了golang實(shí)現(xiàn)實(shí)時(shí)監(jiān)聽(tīng)文件,并自動(dòng)切換目錄,文中通過(guò)代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的參考價(jià)值,需要的朋友可以參考下
    2023-12-12
  • golang?gorm開發(fā)架構(gòu)及寫插件示例

    golang?gorm開發(fā)架構(gòu)及寫插件示例

    這篇文章主要為大家介紹了golang?gorm開發(fā)架構(gòu)及寫插件的詳細(xì)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪
    2022-04-04
  • Golang?Mutex互斥鎖源碼分析

    Golang?Mutex互斥鎖源碼分析

    本篇文章,我們將一起來(lái)探究下Golang?Mutex底層是如何實(shí)現(xiàn)的,知其然,更要知其所以然。文中的示例代碼講解詳細(xì),感興趣的可以了解一下
    2022-10-10
  • Go語(yǔ)言超時(shí)退出的三種實(shí)現(xiàn)方式總結(jié)

    Go語(yǔ)言超時(shí)退出的三種實(shí)現(xiàn)方式總結(jié)

    這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中超時(shí)退出的三種實(shí)現(xiàn)方式,文中的示例代碼簡(jiǎn)潔易懂,對(duì)我們深入了解Go語(yǔ)言有一定的幫助,需要的可以了解一下
    2023-06-06
  • Golang defer延遲語(yǔ)句的實(shí)現(xiàn)

    Golang defer延遲語(yǔ)句的實(shí)現(xiàn)

    defer擁有注冊(cè)延遲調(diào)用的機(jī)制,本文主要介紹了Golang defer延遲語(yǔ)句的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2024-07-07
  • 使用Go HTTP客戶端打造高性能服務(wù)

    使用Go HTTP客戶端打造高性能服務(wù)

    大多數(shù)語(yǔ)言都有提供各自的 HTTP 客戶端,本文將動(dòng)手實(shí)踐如何使用Go語(yǔ)言發(fā)起HTTP請(qǐng)求,并討論其中有可能遇到的問(wèn)題。具有一定的參考價(jià)值,感興趣的可以了解一下
    2021-12-12

最新評(píng)論