1. 程式人生 > 程式設計 >深入理解Go-sync.Map原理剖析

深入理解Go-sync.Map原理剖析

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.

上面一段是官方對sync.Map 的描述,從描述中看,sync.Mapmap 很像,sync.Map 的底層實現也是依靠了map,但是sync.Map 相對於 map 來說,是併發安全的。

1. 結構概覽

1.1. sync.Map

sync.Map的結構體了

type Map struct {
	mu Mutex

  // 後面是readOnly結構體,依靠map實現,僅僅只用來讀
	read atomic.Value // readOnly

	// 這個map主要用來寫的,部分時候也承擔讀的能力
	dirty map[interface{}]*entry

	// 記錄自從上次更新了read之後,從read讀取key失敗的次數
	misses int
}
複製程式碼

1.2. readOnly

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.
}
複製程式碼

1.3. entry

entry就是unsafe.Pointer,記錄的是資料儲存的真實地址

type entry struct {
	p unsafe.Pointer // *interface{}
}

複製程式碼

1.4. 結構示意圖

通過上面的結構體,我們可以簡單畫出來一個結構示意圖

2. 流程分析

我們通過下面的動圖(也可以手動debug),看一下在我們執行Store Load Delete 的時候,這個結構體的變換是如何的,先增加一點我們的認知

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的結構變換

3. 原始碼分析

3.1. 新增key

新增一個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()
}
複製程式碼

3.1.1. tryLock

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,這種情況下不能直接更新

3.1.2. dirtyLock

這裡就是設定 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
		}
	}
}
複製程式碼

3.1.3. tryExpungeLocked

通過原子操作,給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
}
複製程式碼

3.1.4. unexpungeLocked

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

根據上面分析,我們發現,在新增的時候,分為四種情況:

  1. key原先就存在於read中,獲取key所對應記憶體地址,原子性修改
  2. key存在,但是key所對應的值被標記為 expunged,解鎖,解除標記,並更新dirty中的key,與read中進行同步,然後修改key對應的值
  3. read中沒有key,但是dirty中存在這個key,直接修改dirty中key的值
  4. read和dirty中都沒有值,先判斷自從read上次同步dirty的內容後有沒有再修改過dirty的內容,沒有的話,先同步read和dirty的值,然後新增新的key value到dirty上面

當出現第四種情況的時候,很容易產生一個困惑:既然read.amended == false,表示資料沒有修改,為什麼還要將read的資料同步到dirty裡面呢?

這個答案在Load 函式裡面會有答案,因為,read同步dirty的資料的時候,是直接把dirty指向map的指標交給了read.m,然後將dirty的指標設定為nil,所以,同步之後,dirty就為nil

下面看看具體的實現

3.2. 讀取(Load)

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函式的時候會被揭曉

3.2.1. missLocked

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
}
複製程式碼

3.3. 刪除(Delete)

這裡的刪除並不是簡單的將key從map中刪除

func (m *Map) Delete(key interface{}) {
	read,ok := read.m[key]
  // read中沒有這個key,但是Map被標識修改了,那麼去dirty裡面看看
	if !ok && read.amended {
		m.mu.Lock()
		read,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,nil) {
			return true
		}
	}
}
複製程式碼

根據上面的邏輯可以看出,刪除的時候,存在以下幾種情況

  1. read中沒有,且Map存在修改,則嘗試刪除dirty中的map中的key
  2. read中沒有,且Map不存在修改,那就是沒有這個key,無需操作
  3. read中有,嘗試將key對應的值設定為nil,後面讀取的時候就知道被刪了,因為dirty中map的值跟read的map中的值指向的都是同一個地址空間,所以,修改了read也就是修改了dirty

3.3. 遍歷(Range)

遍歷的邏輯就比較簡單了,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
		}
	}
}
複製程式碼

3.4. 適用場景

在官方介紹的時候,也對適用場景做了說明

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的讀寫是可以同時併發的