1. 程式人生 > >go sync.Map使用和介紹

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的時候才刪除。