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