深入理解Go-sync.Map原理剖析

Map is like a Go map[interface{}]interface{} but is safe for concurrent use

by multiple goroutines without additional locking or coordination.

Loads, stores, and deletes run in amortized constant time.

上面一段是官方對sync.Map 的描述,從描述中看,sync.Mapmap 很像,sync.Map 的底層實現也是依靠了map,但是sync.Map 相對於 map 來說,是併發安全的。

1. 結構概覽

1.1. sync.Map

sync.Map的結構體了

type Map struct {
    mu Mutex

  // 後面是readOnly結構體,依靠map實現,僅僅只用來讀
    read atomic.Value // readOnly

    // 這個map主要用來寫的,部分時候也承擔讀的能力
    dirty map[interface{}]*entry

    // 記錄自從上次更新了read之後,從read讀取key失敗的次數
    misses int
}

1.2. readOnly

sync.Map.read屬性所對應的結構體了,這裏不太明白爲什麼不把readOnly結構體的屬性直接放入到sync.Map結構體裏

type readOnly struct {
  // 讀操作所對應的map
    m       map[interface{}]*entry
  // dirty是否包含m中不存在的key
    amended bool // true if the dirty map contains some key not in m.
}

1.3. entry

entry就是unsafe.Pointer,記錄的是數據存儲的真實地址

type entry struct {
    p unsafe.Pointer // *interface{}
}

1.4. 結構示意圖

通過上面的結構體,我們可以簡單畫出來一個結構示意圖

2. 流程分析

我們通過下面的動圖(也可以手動debug),看一下在我們執行Store Load Delete 的時候,這個結構體的變換是如何的,先增加一點我們的認知

func main() {
    m := sync.Map{}
    m.Store("test1", "test1")
    m.Store("test2", "test2")
    m.Store("test3", "test3")
    m.Load("test1")
    m.Load("test2")
    m.Load("test3")
    m.Store("test4", "test4")
    m.Delete("test")
    m.Load("test")
}

以上面代碼爲例,我們看一下m的結構變換

3. 源碼分析

3.1. 新增key

新增一個key value,通過Store方法來實現

func (m *Map) Store(key, value interface{}) {
    read, _ := m.read.Load().(readOnly)
  // 如果這個key存在,通過tryStore更新
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
  // 走到這裏有兩種情況,1. key不存在 2. key對應的值被標記爲expunged,read中的entry拷貝到dirty時,會將key標記爲expunged,需要手動解鎖
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
    // 第二種情況,先解鎖,然後添加到dirty
        if e.unexpungeLocked() {
            // The entry was previously expunged, which implies that there is a
            // non-nil dirty map and this entry is not in it.
            m.dirty[key] = e
        }
        e.storeLocked(&value)
    } else if e, ok := m.dirty[key]; ok {
    // m中沒有,但是dirty中存在,更新dirty中的值
        e.storeLocked(&value)
    } else {
    // 如果amend==false,說明dirty和read是一致的,但是我們需要新加key到dirty裏面,所以更新read.amended
        if !read.amended {
            // We're adding the first new key to the dirty map.
            // Make sure it is allocated and mark the read-only map as incomplete.
      // 這一步會將read中所有的key標記爲 expunged
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}

3.1.1. tryLock

func (e *entry) tryStore(i *interface{}) bool {
    p := atomic.LoadPointer(&e.p)
  // 這個entry是key對應的entry,p是key對應的值,如果p被設置爲expunged,不能直接更新存儲
    if p == expunged {
        return false
    }
    for {
    // 原子更新
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
        if p == expunged {
            return false
        }
    }
}

tryLock會對key對應的值,進行判斷,是否被設置爲了expunged,這種情況下不能直接更新

3.1.2. dirtyLock

這裏就是設置 expunged 標誌的地方了,而這個函數正是將read中的數據同步到dirty的操作

func (m *Map) dirtyLocked() {
  // dirty != nil 說明dirty在上次read同步dirty數據後,已經有了修改了,這時候read的數據不一定準確,不能同步
    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 {
    // 這裏調用tryExpungeLocked 來給entry,即key對應的值 設置標誌位
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

3.1.3. tryExpungeLocked

通過原子操作,給entry,key對應的值設置 expunged 標誌

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
}

3.1.4. unexpungeLocked

func (e *entry) unexpungeLocked() (wasExpunged bool) {
    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

根據上面分析,我們發現,在新增的時候,分爲四種情況:

  1. key原先就存在於read中,獲取key所對應內存地址,原子性修改
  2. key存在,但是key所對應的值被標記爲 expunged,解鎖,解除標記,並更新dirty中的key,與read中進行同步,然後修改key對應的值
  3. read中沒有key,但是dirty中存在這個key,直接修改dirty中key的值
  4. read和dirty中都沒有值,先判斷自從read上次同步dirty的內容後有沒有再修改過dirty的內容,沒有的話,先同步read和dirty的值,然後添加新的key value到dirty上面

當出現第四種情況的時候,很容易產生一個困惑:既然read.amended == false,表示數據沒有修改,爲什麼還要將read的數據同步到dirty裏面呢?

這個答案在Load 函數裏面會有答案,因爲,read同步dirty的數據的時候,是直接把dirty指向map的指針交給了read.m,然後將dirty的指針設置爲nil,所以,同步之後,dirty就爲nil

下面看看具體的實現

3.2. 讀取(Load)

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
  // 如果read的map中沒有,且存在修改
    if !ok && read.amended {
        m.mu.Lock()
        // Avoid reporting a spurious miss if m.dirty got promoted while we were
        // blocked on m.mu. (If further loads of the same key will not miss, it's
        // not worth copying the dirty map for this key.)
    // 再查找一次,有可能剛剛將dirty升級爲read了
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
      // 如果amended 還是處於修改狀態,則去dirty中查找
            e, ok = m.dirty[key]
            // Regardless of whether the entry was present, record a miss: this key
            // will take the slow path until the dirty map is promoted to the read
            // map.
      // 增加misses的計數,在計數達到一定規則的時候,觸發升級dirty爲read
            m.missLocked()
        }
        m.mu.Unlock()
    }
  // read dirty中都沒有找到
    if !ok {
        return nil, false
    }
  // 找到了,通過load判斷具體返回內容
    return e.load()
}

func (e *entry) load() (value interface{}, ok bool) {
    p := atomic.LoadPointer(&e.p)
  // 如果p爲nil或者expunged標識,則key不存在
    if p == nil || p == expunged {
        return nil, false
    }
    return *(*interface{})(p), true
}

爲什麼找到了p,但是p對應的值爲nil呢?這個答案在後面解析Delete函數的時候會被揭曉

3.2.1. missLocked

func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
  // 直接把dirty的指針給read.m,並且設置dirty爲nil,這裏也就是 Store 函數的最後會調用 m.dirtyLocked的原因
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

3.3. 刪除(Delete)

這裏的刪除並不是簡單的將key從map中刪除

func (m *Map) Delete(key interface{}) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
  // read中沒有這個key,但是Map被標識修改了,那麼去dirty裏面看看
    if !ok && read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
      // 調用delete刪除dirty的map,delete會判斷key是否存在的
            delete(m.dirty, key)
        }
        m.mu.Unlock()
    }
  // 如果read中存在,則假刪除
    if ok {
        e.delete()
    }
}

func (e *entry) delete() (hadValue bool) {
    for {
        p := atomic.LoadPointer(&e.p)
    // 已經是被刪除了,不需要管了
        if p == nil || p == expunged {
            return false
        }
    // 原子性 將key的值設置爲nil
        if atomic.CompareAndSwapPointer(&e.p, p, nil) {
            return true
        }
    }
}

根據上面的邏輯可以看出,刪除的時候,存在以下幾種情況

  1. read中沒有,且Map存在修改,則嘗試刪除dirty中的map中的key
  2. read中沒有,且Map不存在修改,那就是沒有這個key,無需操作
  3. read中有,嘗試將key對應的值設置爲nil,後面讀取的時候就知道被刪了,因爲dirty中map的值跟read的map中的值指向的都是同一個地址空間,所以,修改了read也就是修改了dirty

3.3. 遍歷(Range)

遍歷的邏輯就比較簡單了,Map只有兩種狀態,被修改過和沒有修改過

修改過:將dirty的指針交給read,read就是最新的數據了,然後遍歷read的map

沒有修改過:遍歷read的map就好了

func (m *Map) Range(f func(key, value interface{}) bool) {
    read, _ := m.read.Load().(readOnly)
    if read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        if read.amended {
            read = readOnly{m: m.dirty}
            m.read.Store(read)
            m.dirty = nil
            m.misses = 0
        }
        m.mu.Unlock()
    }

    for k, e := range read.m {
        v, ok := e.load()
        if !ok {
            continue
        }
        if !f(k, v) {
            break
        }
    }
}

3.4. 適用場景

在官方介紹的時候,也對適用場景做了說明

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,

(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.

通過對源碼的分析來理解一下產生這兩條規則的原因:

讀多寫少:讀多寫少的環境下,都是從read的map去讀取,不需要加鎖,而寫多讀少的情況下,需要加鎖,其次,存在將read數據同步到dirty的操作的可能性,大量的拷貝操作會大大的降低性能

讀寫不同的key:sync.Map是針對key的值的原子操作,相當於加鎖加載 key上,所以,多個key的讀寫是可以同時併發的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章