go中的sync.Map

Go 1.9中的sync.Map提供了線程安全的map,它的優點總結如下:(網上找的)

1.空間換時間。 通過冗餘的兩個數據結構(read、dirty),實現加鎖對性能的影響。
2.使用只讀數據(read),避免讀寫衝突。
3.動態調整,miss次數多了之後,將dirty數據提升爲read。
4.double-checking。
5.延遲刪除。 刪除一個鍵值只是打標記,只有在提升dirty的時候才清理刪除的數據。

6.優先從read讀取、更新、刪除,因爲對read的讀取不需要鎖。

其實當我們讀完它內部實現後就能很好地理解以上好處了。

先從Map的數據結構看起

type Map struct {
	mu Mutex
	read atomic.Value // readOnly
	dirty map[interface{}]*entry
	misses int
}

Mutex加鎖的工具,read一個只讀的數據結構,所以對它的讀總是線程安全的。dirty是map結構,它包含整個Map中的最新的entries,它不是線程安全的,爲了保證多線程安全的情況下操作它,需要對它加鎖;當dirty爲空時,下一次寫操作會複製read字段中未刪除的數據到dirty。misses計數器,當從Map中讀取entry的時候,如果read中不包含這個entry,會嘗試從dirty中讀取,這個時候會將misses加一, 當misses累積到 dirty的長度的時候, 就會將dirty提升爲read,避免從dirty中miss太多次。因爲操作dirty需要加鎖。

    其中read atomic.Value中存的是readOnly數據結構,我們來看下readOnly

type readOnly struct {
	m       map[interface{}]*entry
	amended bool // true if the dirty map contains some key not in m.
}

m無非存數據,amended爲true的話說明,dirty數組包含一些新的entries,卻存沒有在readOnly中。

因爲這裏數據是隻讀的,所以讀數據時候優先讀read數據,若read中沒有,則去dirty中讀,讀到則misses加一。

Map中還一個expunged指針,指向被刪除的鍵值對。Map中的entry結構體無非存有指向value的指針

var expunged = unsafe.Pointer(new(interface{}))

// An entry is a slot in the map corresponding to a particular key.
type entry struct {
	p unsafe.Pointer // *interface{}
}

既然是Map,那我們先看下如何存數據,Store(key, value interfaces{})

func (m *Map) Store(key, value interface{}) {
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		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 {
		e.storeLocked(&value)
	} else {
		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.
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

先看這個情況:第一次new() Map後,裏面肯定沒有任何數據,第一次Store傳入鍵值對到Map中。

首先通過atomic.Value的Load方法,加載readOnly到read中,此時read中肯定沒有任何數據,於是我們要從dirty中取數據,由於它不是線程安全,所以調用mutex鎖工具lock,於是讀dirty中,也找不到,則判斷下amended是否爲false(確保是不是第一次往dirty中加數據),由於readOnly是剛創建的其ammended值爲false,所以此時方法調用進入m.dirtyLocked()

func (m *Map) dirtyLocked() {
	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 {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

該方法創建dirty,把readOnly的(未被刪除的)鍵值對複製到dirty中,此時readOnly爲空。

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
}

創建完dirty後,會創建新的readOnly賦值給read,此時amended爲true;最後在dirty中添加鍵值對,解鎖。

情況二:如果此時並不是第一次,並且一開始就在readOnly中找到。

此時調用e.tryStore(&Value);

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
		}
	}
}

先判斷取到的value是不是已經被刪除的,如果是則return false;否則,調用cas替換原value的地址值指向新的value,則完成對read中的鍵值對的更新。如果在更新的過程中,value值被刪除則return false;

情況三:如果在read中沒找到,在dirty中找到,則加鎖,加鎖後還是要再load read(以防此時dirty晉升爲read),從read中讀到被標記刪除的value則cas更新dirty中對應key的value設爲nil,最後調用storePionter更改value的值。

func (e *entry) storeLocked(i *interface{}) {
	atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

我們再來看Load()方法吧,讀數據。

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	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.)
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			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.
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}
老樣子,先從read中找,如果找到則返回value對應的數據。如果沒找到,需要判斷amended是否爲true,false的話說明dirty中沒有多餘數據,那找不到。如果爲true,則去dirty中找,操作dirty需要先加鎖,先判斷read中有無(防止dirty晉升成read),找到返回,沒找到再找dirty,如果找到那麼給misses加一。
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
}
如果misses長度大於等於dirty的長度,則晉升dirty爲read,這步驟複雜度爲O1,因爲無非指針替換,再把dirty更新爲nil,misses更新爲0;

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