源碼解讀 Golang 的 sync.Map 實現原理

簡介

Go 的內建 map 是不支持併發寫操做的,緣由是 map 寫操做不是併發安全的,當你嘗試多個 Goroutine 操做同一個 map,會產生報錯:fatal error: concurrent map writesgit

所以官方另外引入了 sync.Map 來知足併發編程中的應用。github

sync.Map 的實現原理可歸納爲:golang

  • 經過 read 和 dirty 兩個字段將讀寫分離,讀的數據存在只讀字段 read 上,將最新寫入的數據則存在 dirty 字段上
  • 讀取時會先查詢 read,不存在再查詢 dirty,寫入時則只寫入 dirty
  • 讀取 read 並不須要加鎖,而讀或寫 dirty 都須要加鎖
  • 另外有 misses 字段來統計 read 被穿透的次數(被穿透指須要讀 dirty 的狀況),超過必定次數則將 dirty 數據同步到 read 上
  • 對於刪除數據則直接經過標記來延遲刪除

數據結構

Map 的數據結構以下:編程

type Map struct {
    // 加鎖做用,保護 dirty 字段
    mu Mutex
    // 只讀的數據,實際數據類型爲 readOnly
    read atomic.Value
    // 最新寫入的數據
    dirty map[interface{}]*entry
    // 計數器,每次須要讀 dirty 則 +1
    misses int
}
複製代碼

其中 readOnly 的數據結構爲:安全

type readOnly struct {
    // 內建 map
    m  map[interface{}]*entry
    // 表示 dirty 裏存在 read 裏沒有的 key,經過該字段決定是否加鎖讀 dirty
    amended bool
}
複製代碼

entry 數據結構則用於存儲值的指針:微信

type entry struct {
    p unsafe.Pointer  // 等同於 *interface{}
}
複製代碼

屬性 p 有三種狀態:數據結構

  • p == nil: 鍵值已經被刪除,且 m.dirty == nil
  • p == expunged: 鍵值已經被刪除,但 m.dirty!=nilm.dirty 不存在該鍵值(expunged 實際是空接口指針)
  • 除以上狀況,則鍵值對存在,存在於 m.read.m 中,若是 m.dirty!=nil 則也存在於 m.dirty

Map 經常使用的有如下方法:併發

  • Load:讀取指定 key 返回 value
  • Store: 存儲(增或改)key-value
  • Delete: 刪除指定 key

源碼解析

Load

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 首先嚐試從 read 中讀取 readOnly 對象
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]

    // 若是不存在則嘗試從 dirty 中獲取
    if !ok && read.amended {
        m.mu.Lock()
        // 因爲上面 read 獲取沒有加鎖,爲了安全再檢查一次
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]

        // 確實不存在則從 dirty 獲取
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // 調用 miss 的邏輯
            m.missLocked()
        }
        m.mu.Unlock()
    }

    if !ok {
        return nil, false
    }
    // 從 entry.p 讀取值
    return e.load()
}

func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    // 當 miss 積累過多,會將 dirty 存入 read,而後 將 amended = false,且 m.dirty = nil
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}
複製代碼

Store

func (m *Map) Store(key, value interface{}) {
    read, _ := m.read.Load().(readOnly)
    // 若是 read 裏存在,則嘗試存到 entry 裏
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }

    // 若是上一步沒執行成功,則要分狀況處理
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    // 和 Load 同樣,從新從 read 獲取一次
    if e, ok := read.m[key]; ok {
        // 狀況 1:read 裏存在
        if e.unexpungeLocked() {
            // 若是 p == expunged,則須要先將 entry 賦值給 dirty(由於 expunged 數據不會留在 dirty)
            m.dirty[key] = e
        }
        // 用值更新 entry
        e.storeLocked(&value)
    } else if e, ok := m.dirty[key]; ok {
        // 狀況 2:read 裏不存在,但 dirty 裏存在,則用值更新 entry
        e.storeLocked(&value)
    } else {
        // 狀況 3:read 和 dirty 裏都不存在
        if !read.amended {
            // 若是 amended == false,則調用 dirtyLocked 將 read 拷貝到 dirty(除了被標記刪除的數據)
            m.dirtyLocked()
            // 而後將 amended 改成 true
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        // 將新的鍵值存入 dirty
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}

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

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

func (e *entry) storeLocked(i *interface{}) {
    atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

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 {
        // 判斷 entry 是否被刪除,不然就存到 dirty 中
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        // 若是有 p == nil(即鍵值對被 delete),則會在這個時機被置爲 expunged
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}
複製代碼

Delete

func (m *Map) Delete(key interface{}) {
    m.LoadAndDelete(key)
}

// LoadAndDelete 做用等同於 Delete,而且會返回值與是否存在
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
    // 獲取邏輯和 Load 相似,read 不存在則查詢 dirty
    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 {
            e, ok = m.dirty[key]
            m.missLocked()
        }
        m.mu.Unlock()
    }
    // 查詢到 entry 後執行刪除
    if ok {
        // 將 entry.p 標記爲 nil,數據並無實際刪除
        // 真正刪除數據並被被置爲 expunged,是在 Store 的 tryExpungeLocked 中
        return e.delete()
    }
    return nil, false
}
複製代碼

總結

可見,經過這種讀寫分離的設計,解決了併發狀況的寫入安全,又使讀取速度在大部分狀況能夠接近內建 map,很是適合讀多寫少的狀況。函數

sync.Map 還有一些其餘方法:學習

  • Range:遍歷全部鍵值對,參數是回調函數
  • LoadOrStore:讀取數據,若不存在則保存再讀取

這裏就再也不詳解了,可參見 源碼


本文屬於原創,首發於微信公衆號「面向人生編程」,如需轉載請後臺留言。

關注後回覆如下信息獲取更多資源 回覆【資料】獲取 Python / Java 等學習資源 回覆【插件】獲取爬蟲經常使用的 Chrome 插件 回覆【知乎】獲取最新知乎模擬登陸
相關文章
相關標籤/搜索