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.less
上面一段是官方對sync.Map
的描述,從描述中看,sync.Map
跟map
很像,sync.Map
的底層實現也是依靠了map
,可是sync.Map
相對於 map
來講,是併發安全的。函數
sync.Map的結構體了源碼分析
type Map struct {
mu Mutex
// 後面是readOnly結構體,依靠map實現,僅僅只用來讀
read atomic.Value // readOnly
// 這個map主要用來寫的,部分時候也承擔讀的能力
dirty map[interface{}]*entry
// 記錄自從上次更新了read以後,從read讀取key失敗的次數
misses int
}
複製代碼
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.
}
複製代碼
entry就是unsafe.Pointer,記錄的是數據存儲的真實地址ui
type entry struct {
p unsafe.Pointer // *interface{}
}
複製代碼
經過上面的結構體,咱們能夠簡單畫出來一個結構示意圖this
咱們經過下面的動圖(也能夠手動debug),看一下在咱們執行Store
Load
Delete
的時候,這個結構體的變換是如何的,先增長一點咱們的認知atom
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的結構變換spa
新增一個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()
}
複製代碼
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,這種狀況下不能直接更新
這裏就是設置 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
}
}
}
複製代碼
經過原子操做,給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
}
複製代碼
func (e *entry) unexpungeLocked() (wasExpunged bool) {
return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}
複製代碼
根據上面分析,咱們發現,在新增的時候,分爲四種狀況:
當出現第四種狀況的時候,很容易產生一個困惑:既然read.amended == false,表示數據沒有修改,爲何還要將read的數據同步到dirty裏面呢?
這個答案在Load
函數裏面會有答案,由於,read同步dirty的數據的時候,是直接把dirty指向map的指針交給了read.m,而後將dirty的指針設置爲nil,因此,同步以後,dirty就爲nil
下面看看具體的實現
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
函數的時候會被揭曉
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
}
複製代碼
這裏的刪除並非簡單的將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
}
}
}
複製代碼
根據上面的邏輯能夠看出,刪除的時候,存在如下幾種狀況
遍歷的邏輯就比較簡單了,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
}
}
}
複製代碼
在官方介紹的時候,也對適用場景作了說明
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的讀寫是能夠同時併發的