go sync.Map使用和介紹

sync.Map使用和介紹

1、首先看下該sync.Map的使用:

package main
import (
    "sync"
    "fmt"
)

func main() {
    //開箱即用
    var sm sync.Map
    //store 方法,添加元素
    sm.Store(1,"a")
    //Load 方法,獲得value
    if v,ok:=sm.Load(1);ok{
        fmt.Println(v)
    }
    //LoadOrStore方法,獲取或者保存
    //參數是一對key:value,如果該key存在且沒有被標記刪除則返回原先的value(不更新)和true;不存在則store,返回該value 和false
    if vv,ok:=sm.LoadOrStore(1,"c");ok{
        fmt.Println(vv)
    }
    if vv,ok:=sm.LoadOrStore(2,"c");!ok{
        fmt.Println(vv)
    }
    //遍歷該map,參數是個函數,該函數參的兩個參數是遍歷獲得的key和value,返回一個bool值,當返回false時,遍歷立刻結束。
    sm.Range(func(k,v interface{})bool{
        fmt.Print(k)
        fmt.Print(":")
        fmt.Print(v)
        fmt.Println()
        return true
    })
}

運行結果:

a
a
c
1:a
2:c

2、利用傳統的sync.RWMutex+Map 實現併發安全的map:

var rwmap = struct{
    sync.RWMutex
    m map[string]string
}{m: make(map[string]sring)}

讀數據時候,讀鎖鎖定:

rwmap.RLock()
value:= rwmap.m["key"]
rwmap.RUnlock()
fmt.Println("key:", value)

寫數據的時候,寫鎖鎖定:

rwmap.Lock()
rwmap.m["key"]="value"
rwmap.Unlock()

3、兩種map的性能對比:
下面是有人做的兩種map性能對比圖:
這裏寫圖片描述
可見隨着cpu核心數的增加、併發加劇,這種讀寫鎖+map的方式性能在不停的衰減,並且在覈數爲4的時候出現了性能的拐點;而sync.Map雖然性能不是特別好,但是相對比較平穩。
4、sync.Map 源碼解析
源碼位於:src\sync\map.go
首先查看一下sync.Map的數據結構:

  type Map struct {
    // 該鎖用來保護dirty
    mu Mutex
    // 存讀的數據,因爲是atomic.value類型,只讀類型,所以它的讀是併發安全的
    read atomic.Value // readOnly
    //包含最新的寫入的數據,並且在寫的時候,會把read 中未被刪除的數據拷貝到該dirty中,因爲是普通的map存在併發安全問題,需要用到上面的mu字段。
    dirty map[interface{}]*entry
    // 從read讀數據的時候,會將該字段+1,當等於len(dirty)的時候,會將dirty拷貝到read中(從而提升讀的性能)。
    misses int
}

read的數據結構是:

type readOnly struct {
    m  map[interface{}]*entry
    // 如果Map.dirty的數據和m 中的數據不一樣是爲true
    amended bool 
}

entry的數據結構:

type entry struct {
    //可見value是個指針類型,雖然read和dirty存在冗餘情況(amended=false),但是由於是指針類型,存儲的空間應該不是問題
    p unsafe.Pointer // *interface{}
}

Delete
首先來看delete方法

func (m *Map) Delete(key interface{}) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    //如果read中沒有,並且dirty中有新元素,那麼就去dirty中去找
    if !ok && read.amended {
        m.mu.Lock()
        //這是雙檢查(上面的if判斷和鎖不是一個原子性操作)
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            //直接刪除
            delete(m.dirty, key)
        }
        m.mu.Unlock()
    }
    if ok {
    //如果read中存在該key,則將該value 賦值nil(採用標記的方式刪除!)
        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
        }
    }
}

Store
新加元素

func (m *Map) Store(key, value interface{}) {
    // 如果m.read存在這個key,並且沒有被標記刪除,則嘗試更新。
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    // 如果read不存在或者已經被標記刪除
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
    //如果entry被標記expunge,則表明dirty沒有key,可添加入dirty,並更新entry
        if e.unexpungeLocked() { 
            //加入dirty中
            m.dirty[key] = e
        }
        //更新value值
        e.storeLocked(&value) 
        //dirty 存在該key,更新
    } else if e, ok := m.dirty[key]; ok { 
        e.storeLocked(&value)
        //read 和dirty都沒有,新添加一條
    } else {
     //dirty中沒有新的數據,往dirty中增加第一個新鍵
        if !read.amended { 
            //將read中未刪除的數據加入到dirty中
            m.dirtyLocked() 
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value) 
    }
    m.mu.Unlock()
}

//將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))
    //read如果較大的話,可能影響性能
    for k, e := range read.m {
    //通過此次操作,dirty中的元素都是未被刪除的,可見expunge的元素不在dirty中
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}
//判斷entry是否被標記刪除,並且將標記爲nil的entry更新標記爲expunge
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        // 將已經刪除標記爲nil的數據標記爲expunged
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}
//對entry 嘗試更新
func (e *entry) tryStore(i *interface{}) bool {
    p := atomic.LoadPointer(&e.p)
    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
        }
    }
}
//read裏 將標記爲expunge的更新爲nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
//更新entry
func (e *entry) storeLocked(i *interface{}) {
    atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

可見,每次操作先檢查read,因爲read 併發安全,性能好些;read不滿足,則加鎖檢查dirty,一旦是新的鍵值,dirty會被read更新。
Load
加載方法,查找key。

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    //因read只讀,線程安全,先查看是否滿足條件
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    //如果read沒有,並且dirty有新數據,那從dirty中查找,由於dirty是普通map,線程不安全,這個時候用到互斥鎖了
    if !ok && read.amended {
        m.mu.Lock()
        // 雙重檢查
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        // 如果read中還是不存在,並且dirty中有新數據
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // mssLocked()函數是性能是sync.Map 性能得以保證的重要函數,目的講有鎖的dirty數據,替換到只讀線程安全的read裏
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load()
}
//dirty 提升至read 關鍵函數,當misses 經過多次因爲load之後,大小等於len(dirty)時候,講dirty替換到read裏,以此達到性能提升。
func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    //原子操作,耗時很小
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

源碼用的是1.9版本,通過閱讀源碼我們發現sync.Map是通過冗餘的兩個數據結構(read、dirty),實現性能的提升。爲了提升性能,load、delete、store等操作儘量使用只讀的read;爲了提高read的key擊中概率,採用動態調整,將dirty數據提升爲read;對於數據的刪除,採用延遲標記刪除法,只有在提升dirty的時候才刪除。

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