Golang的Map並發性能以及原理分析


1. golang map數據類型的問題

在Go 1.6之前, 內置的map類型是部分goroutine安全的,並發的讀沒有問題,並發的寫可能有問題。自go 1.6之后, 並發地讀寫map會報錯,這在一些知名的開源庫中都存在這個問題,所以go 1.9之前的解決方案是額外綁定一個鎖,封裝成一個新的struct或者單獨使用鎖都可以。

2. map如何導致出現並發問題

golang官方的faq已經提到build-in的map不是線程(goroutine)安全的。

現在就基於這個場景,構建出一段示例代碼

package main
func main() {
	m := make(map[int]int)
	go func() {
		for {
			_ = m[1]
		}
	}()
	go func() {
		for {
			m[2] = 2
		}
	}()
	select {}
}

上述這段的代碼的意思也很好理解,倆goroutine,第一個goroutine負責不停的讀取m這個map對象,而第二個goroutine在不停地往m這個map對象中不停的寫入同一個數據。最后我們來運行一下這段代碼看一下結果

go run main.go
fatal error: concurrent map read and map write

結果也是我們意料之中的事情,golang的build-in的map並不支持並發的讀寫操作。基於為什么會這樣,這就和go的源碼有關了,原因在於,在read的時候回去檢查hashWriting標志,如果存在這個標志,就會出現並發錯誤。

源碼URL跳轉鏈接

設置完之后又會取消hashWriting這個標識。源碼中會檢查是不是有並發的寫,刪除鍵的時候,遍歷的時候並發讀寫的問題。map的並發問題不是那么容易被發現,可以利用-race來檢查。

3. Go 1.9之前的解決方案

很多時候我們會並發的使用mao對象,尤其是在一定的規模項目中,map總會保存goroutine共享的數據。go官方在那個時候給出了一個簡單的解決方案。大家也肯定和官方想的一樣,就是加鎖。

var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

設置一個struct,嵌入一個讀寫鎖和一個map。

在讀數據的時候加鎖

counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

寫入的時候也加鎖

counter.Lock()
counter.m["some_key"]++
counter.Unlock()

4. 現有的map並發安全的解決方案以及問題

實現方式 原理 適用場景
map+Mutex 通過Mutex互斥鎖來實現多個goroutine對map的串行化訪問 讀寫都需要通過Mutex加鎖和釋放鎖,適用於讀寫比接近的場景
map+RWMutex 通過RWMutex來實現對map的讀寫進行讀寫鎖分離加鎖,從而實現讀的並發性能提高 同Mutex相比適用於讀多寫少的場景
sync.Map 底層通分離讀寫map和原子指令來實現讀的近似無鎖,並通過延遲更新的方式來保證讀的無鎖化 讀多修改少,元素增加刪除頻率不高的情況,在大多數情況下替代上述兩種實現

map的容量問題

image.png

在Mutex和RWMutex實現的並發安全的map中map隨着時間和元素數量的增加、刪除,容量會不斷的遞增,在某些情況下比如在某個時間點頻繁的進行大量數據的增加,然后又大量的刪除,其map的容量並不會隨着元素的刪除而縮小,而在sync.Map中,當進行元素從dirty進行提升到read map的時候會進行重建,可能會縮容

5. sync map

1. 無鎖讀與讀寫分離

image.png

1. 讀寫分離

並發訪問map讀的主要問題其實是在擴容的時候,可能會導致元素被hash到其他的地址,那如果我的讀的map不會進行擴容操作,就可以進行並發安全的訪問了,而sync.map里面正是采用了這種方式,對增加元素通過dirty來進行保存

2. 無鎖讀

通過read只讀和dirty寫map將操作分離,其實就只需要通過原子指令對read map來進行讀操作而不需要加鎖了,從而提高讀的性能

3. 寫加鎖與延遲提升

image.png

1. 寫加鎖

上面提到增加元素操作可能會先增加到dirty寫map中,那針對多個goroutine同時寫,其實就需要進行Mutex加鎖了

2. 延遲提升

上面提到了read只讀map和dirty寫map, 那就會有個問題,默認增加元素都放在dirty中,那后續訪問新的元素如果都通過 mutex加鎖,那read只讀map就失去意義,sync.Map中采用一直延遲提升的策略,進行批量將當前map中的所有元素都提升到read只讀map中從而為后續的讀訪問提供無鎖支持

2. 指針與惰性刪除

image.png

1. map里面的指針

map里面存儲數據都會涉及到一個問題就是存儲值還是指針,存儲值可以讓 map作為一個大的的對象,減輕垃圾回收的壓力(避免掃描所有小對象),而存儲指針可以減少內存利用,而sync.Map中其實采用了指針結合惰性刪除的方式,來進行 map的value的存儲

2. 惰性刪除

惰性刪除是並發設計中一中常見的設計,比如刪除某個個鏈表元素,如果要刪除則需要修改前后元素的指針,而采用惰性刪除,則通常只需要給某個標志位設定為刪除,然后在后續修改中再進行操作,sync.Map中也采用這種方式,通過給指針指向某個標識刪除的指針,從而實現惰性刪除

3. sync.map的特點

  • 空間換時間,通過冗余的兩個數據結構(read,dirty)實現加鎖對性能的影響。
  • 使用只讀鎖,避免讀寫沖突。
  • 動態調整,miss次數多了之后,將dirty數據提升為read。
  • double-checking。
  • 延遲刪除。刪除一個key值只是打標記,只有在提升dirty的時候才清理的數據。
  • 有限從read中讀取,更新,刪除,因為對read的讀取都不需要鎖。

4. 源代碼解析

這里寫圖片描述

1. Map

type Map struct {
	// 當涉及到dirty數據的操作的時候,需要使用這個鎖
	mu Mutex
	// 一個只讀的數據結構,因為只讀,所以不會有讀寫沖突。
	// 所以從這個數據中讀取總是安全的。
	// 實際上,實際也會更新這個數據的entries,如果entry是未刪除的(unexpunged), 並不需要加鎖。如果entry已經被刪除了,需要加鎖,以便更新dirty數據。
	read atomic.Value // readOnly
	// dirty數據包含當前的map包含的entries,它包含最新的entries(包括read中未刪除的數據,雖有冗余,但是提升dirty字段為read的時候非常快,不用一個一個的復制,而是直接將這個數據結構作為read字段的一部分),有些數據還可能沒有移動到read字段中。
	// 對於dirty的操作需要加鎖,因為對它的操作可能會有讀寫競爭。
	// 當dirty為空的時候, 比如初始化或者剛提升完,下一次的寫操作會復制read字段中未刪除的數據到這個數據中。
	dirty map[interface{}]*entry
	// 當從Map中讀取entry的時候,如果read中不包含這個entry,會嘗試從dirty中讀取,這個時候會將misses加一,
	// 當misses累積到 dirty的長度的時候, 就會將dirty提升為read,避免從dirty中miss太多次。因為操作dirty需要加鎖。
	misses int
}

sync的Map的數據結構比較簡單,只有四個字段,readmudirtymisses

它使用了冗余的數據結構readdirtydirty中會包含read中為刪除的entries,新增加的entries會加入到dirty中。

2. Readonly

read的數據結構是

type readOnly struct {
	m       map[interface{}]*entry
	amended bool // 如果Map.dirty有些數據不在其中的時候,這個值為true
}

只讀map,對該map元素的訪問不需要加鎖,但是該map也不會進行元素的增加,元素會被先添加到dirty中然后后續再轉移到read只讀map中,通過atomic原子操作不需要進行鎖操作。

amended指明Map.dirty中有readOnly.m未包含的數據,所以如果從Map.read找不到數據的話,還要進一步到Map.dirty中查找。

對Map.read的修改是通過原子操作進行的。

雖然readdirty有冗余數據,但這些數據是通過指針指向同一個數據,所以盡管Map的value會很大,但是冗余的空間占用還是有限的。

3. entry

readOnly.mMap.dirty存儲的值類型是*entry,它包含一個指針p, 指向用戶存儲的value值。

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

entry是sync.Map中值得指針,如果當p指針指向expunged這個指針的時候,則表明該元素被刪除,但不會立即從map中刪除,如果在未刪除之前又重新賦值則會重用該元素。

p的值

  • nil: entry已被刪除了,並且m.dirty為nil
  • expunged: entry已被刪除了,並且m.dirty不為nil,而且這個entry不存在於m.dirty中
  • 其它: entry是一個正常的值

4. read map與dirty map的關系

img

從上圖中可以看出,read map 和 dirty map 中含有相同的一部分 entry,我們稱作是 normal entries,是雙方共享的。狀態就是上面所說的p的值nilunexpunged

但是 read map 中含有一部分 entry 是不屬於 dirty map 的,而這部分 entry 就是狀態為 expunged 狀態的 entry。而 dirty map 中有一部分 entry 也是不屬於 read map 的,而這部分其實是來自 Store 操作形成的(也就是新增的 entry),換句話說就是新增的 entry 是出現在 dirty map 中的。這句話其實在文中已經是重復了好幾次了,還是要請大家記住這一點,只要是添加的,就一定是添加到dirty map中。

現在可以了解read map 和 dirty map 的是什么了,那么還得理解一個重要的問題是: read map 和 dirty map 是用來干什么的,以及為什么這么設計?

第一個問題比較好回答,read map 是用來進行 lock free 操作的(其實可以讀寫,但是不能做刪除操作,因為一旦做了刪除操作,就不是線程安全的了,也就無法 lock free),而 dirty map 是用來在無法進行 lock free 操作的情況下,需要 lock 來做一些更新工作的對象。下面我們重點看看LoadStoreDeleteRange這四個方法,其它輔助方法可以參考這四個方法來理解。

5. Load

這里寫圖片描述

加載方法,也就是提供一個鍵key,查找對應的值value,如果不存在,通過ok反映:

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	// 1.首先從m.read中得到只讀readOnly,從它的map中查找,不需要加鎖
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	// 2. 如果沒找到,並且m.dirty中有新數據,需要從m.dirty查找,這個時候需要加鎖
	if !ok && read.amended {
		m.mu.Lock()
		// 雙檢查,避免加鎖的時候m.dirty提升為m.read,這個時候m.read可能被替換了。
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		// 如果m.read中還是不存在,並且m.dirty中有新數據
		if !ok && read.amended {
			// 從m.dirty查找
			e, ok = m.dirty[key]
			// 不管m.dirty中存不存在,都將misses計數加一
			// missLocked()中滿足條件后就會提升m.dirty
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

這里有兩個值的關注的地方。一個是首先從m.read中加載,不存在的情況下,並且m.dirty中有新數據,加鎖,然后從m.dirty中加載。

二是這里使用了雙檢查的處理,因為在下面的兩個語句中,這兩行語句並不是一個原子操作

if !ok && read.amended {
		m.mu.Lock()
		}

雖然第一句執行的時候條件滿足,但是在加鎖之前,m.dirty可能被提升為m.read,所以加鎖后還得再檢查m.read,后續的方法中都使用了這個方法。

雙檢查的技術Java程序員非常熟悉了,單例模式的實現之一就是利用雙檢查的技術。

可以看到,如果我們查詢的鍵值正好存在於m.read中,無須加鎖,直接返回,理論上性能優異。即使不存在於m.read中,經過miss幾次之后,m.dirty會被提升為m.read,又會從m.read中查找。所以對於更新/增加比較少的場景,加載存在的key很多的case,性能基本和無鎖的map類似。

下面看看m.dirty是如何被提升為m.read的。 missLocked方法中可能會將m.dirty提升。

dirty到read map的遷移

Load的源碼中有一個函數叫missLocked,這個函數比較重要,是關系dirty到read map遷移操作的,對着源碼着重說一說。

image.png

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
}

上面的最后三行代碼就是提升m.dirty的,很簡單的將m.dirty作為readOnlym字段,原子更新m.read。提升后m.dirtym.misses重置, 並且m.read.amended為false。這種做法無疑是會提升read map的命中率。

當持續的從read訪問穿透到dirty中后,就會觸發一次從dirty到read的遷移,這也意味着如果我們的元素讀寫比差比較小,其實就會導致頻繁的遷移操作,性能其實可能並不如rwmutex等實現。

6. store

這里寫圖片描述

這個方法是更新或者新增一個entry。

func (m *Map) Store(key, value interface{}) {
	// 如果m.read存在這個鍵,並且這個entry沒有被標記刪除,嘗試直接存儲。
	// 因為m.dirty也指向這個entry,所以m.dirty也保持最新的entry。
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
	// 如果`m.read`不存在或者已經被標記刪除
	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() { //標記成未被刪除
			m.dirty[key] = e //m.dirty中不存在這個鍵,所以加入m.dirty
		}
		e.storeLocked(&value) //更新
	} else if e, ok := m.dirty[key]; ok { // m.dirty存在這個鍵,更新
		e.storeLocked(&value)
	} else { //新鍵值
		if !read.amended { //m.dirty中沒有新的數據,往m.dirty中增加第一個新鍵
			m.dirtyLocked() //從m.read中復制未刪除的數據
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value) //將這個entry加入到m.dirty中
	}
	m.mu.Unlock()
}
// 在剛初始化和將所有元素遷移到read中后,dirty默認都是nil元素,而此時如果有新的元素增加,則需要先將read map中的所有未刪除數據先遷移到dirty中
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
		}
	}
}
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
}

你可以看到,以上操作都是先從操作m.read開始的,不滿足條件再加鎖,然后操作m.dirty

Store可能會在某種情況下(初始化或者m.dirty剛被提升后)從m.read中復制數據,如果這個時候m.read中數據量非常大,可能會影響性能。

read map到dirty map的遷移

着重的講一下源碼中的dirtyLocked函數

在剛初始化和將所有元素遷移到read中后,dirty默認都是nil元素,而此時如果有新的元素增加,則需要先將read map中的所有未刪除數據先遷移到dirty中。

image.png

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

7. Delete

這里寫圖片描述

刪除一個鍵值。

func (m *Map) Delete(key interface{}) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}
	if ok {
		e.delete()
	}
}

同樣,刪除操作還是從m.read中開始, 如果這個entry不存在於m.read中,並且m.dirty中有新數據,則加鎖嘗試從m.dirty中刪除。

注意,還是要雙檢查的。 從m.dirty中直接刪除即可,就當它沒存在過,但是如果是從m.read中刪除,並不會直接刪除,而是打標記:

func (e *entry) delete() (hadValue bool) {
	for {
		p := atomic.LoadPointer(&e.p)
		// 已標記為刪除
		if p == nil || p == expunged {
			return false
		}
		// 原子操作,e.p標記為nil
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return true
		}
	}
}

這里有一個比較有意思的地方,原子操作e.p標記為nil而不是expunged,其中的原因是啥,我也仔細的想了一想,在開篇的時候我給了一張read map和dirty map關系的圖,unexpunged的entry是readmap有而dirtymap中沒有的,而這個值執行CAS條件表明entry 既不是 nil 也不是 expunged 的,那么就是說這個 entry 必定是存在於 dirty map 中的,也就不能置成 expunged

8. Range

因為for ... range map是內建的語言特性,所以沒有辦法使用for range遍歷sync.Map, 但是可以使用它的Range方法,通過回調的方式遍歷。

func (m *Map) Range(f func(key, value interface{}) bool) {
	read, _ := m.read.Load().(readOnly)
	// 如果m.dirty中有新數據,則提升m.dirty,然后在遍歷
	if read.amended {
		//提升m.dirty
		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 range是安全的
	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) {
			break
		}
	}
}

9. Load Store Delete

Load Store Delete 的操作都基本描述完了,可以用下面的一張圖用來總結一下:

img

6. read map 和 dirty map 的設計分析

最核心和最基本的原因就是: 通過分離出 readonly 的部分,從而可以形成 lock free 的優化。

從上面的流程可以發現,對於 read map 中 entry 的操作是不需要 lock 的,但是為什么就能夠保證這樣的無鎖操作是 thread-safe 的呢?

這是因為 read map 是 read-only 的,不過這里的 read-only 是指 entry 不會被刪除,其實值是可以被更新,而值的更新是可以通過 CAS 操作保證 thread-safe 的,所以讀者可以發現,即使在持有 lock 的時候,仍然需要 CAS 來對 read map 中的 entry 進行操作,此外對於 read map 本身的更新也是 通過 atomic 來操作的(在 missLocked 方法中)。

7 syncmap 的缺陷

其實通過上面的分析,了解了整個流程的話,讀者會很容易理解這個 syncmap 的缺點:當需要不停地新增和刪除的時候,會導致 dirty map 不停地更新,甚至在 miss 過多之后,導致 dirty 成為 nil,並進入重建的過程

8. 關於 lock free 的啟發

lock free 會給並發的性能帶了較高的提升,目前通過 syncmap 的代碼分析,我們也對 lock free 有一些了解,下面會記錄一下筆者從 syncmap 中得到的對 lock free 的一些理解。

9. BenchMark測試,用數據說話

1. map無鎖並發讀與map有鎖並發讀的性能差異

package lock_test

import (
	"fmt"
	"sync"
	"testing"
)

var cache map[string]string

const NUM_OF_READER int = 40
const READ_TIMES = 100000

func init() {
	cache = make(map[string]string)

	cache["a"] = "aa"
	cache["b"] = "bb"
}

func lockFreeAccess() {

	var wg sync.WaitGroup
	wg.Add(NUM_OF_READER)
	for i := 0; i < NUM_OF_READER; i++ {
		go func() {
			for j := 0; j < READ_TIMES; j++ {
				_, err := cache["a"]
				if !err {
					fmt.Println("Nothing")
				}
			}
			wg.Done()
		}()
	}
	wg.Wait()
}

func lockAccess() {

	var wg sync.WaitGroup
	wg.Add(NUM_OF_READER)
	m := new(sync.RWMutex)
	for i := 0; i < NUM_OF_READER; i++ {
		go func() {
			for j := 0; j < READ_TIMES; j++ {

				m.RLock()
				_, err := cache["a"]
				if !err {
					fmt.Println("Nothing")
				}
				m.RUnlock()
			}
			wg.Done()
		}()
	}
	wg.Wait()
}

func BenchmarkLockFree(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		lockFreeAccess()
	}
}

func BenchmarkLock(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		lockAccess()
	}
}

上面的代碼比較簡單,BenchMark測試的就是倆函數,一個是lockFreeAccess,一個是lockAccess,這兩個函數的區別就是lockFreeAccess是無鎖的,lockAccess還是帶鎖的。

go test -bench=.
goos: darwin
goarch: amd64
pkg: go_learning/code/ch48/lock
BenchmarkLockFree-4   	     100	  12014281 ns/op
BenchmarkLock-4       	       6	 199626870 ns/op
PASS
ok  	go_learning/code/ch48/lock	3.245s

執行go test -bench=. 很明顯的看出BenchmarkLockFree和BenchmarkLock,BenchmarkLockFree-4每次執行耗時是12014281納秒,而BenchmarkLock-4 是199626870納秒,兩者明顯就是相差了一個量級。

cpu差異

go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
Type: cpu
Time: Aug 10, 2020 at 2:27am (GMT)
Duration: 2.76s, Total samples = 6.62s (240.26%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 6.49s, 98.04% of 6.62s total
Dropped 8 nodes (cum <= 0.03s)
Showing top 10 nodes out of 34
      flat  flat%   sum%        cum   cum%
     1.57s 23.72% 23.72%      1.58s 23.87%  sync.(*RWMutex).RLock (inline)
     1.46s 22.05% 45.77%      1.47s 22.21%  sync.(*RWMutex).RUnlock (inline)
     1.40s 21.15% 66.92%      1.82s 27.49%  runtime.mapaccess2_faststr
     0.80s 12.08% 79.00%      0.80s 12.08%  runtime.findnull
     0.51s  7.70% 86.71%      2.07s 31.27%  go_learning/code/ch48/lock_test.lockFreeAccess.func1
     0.28s  4.23% 90.94%      0.28s  4.23%  runtime.pthread_cond_wait
     0.16s  2.42% 93.35%      0.16s  2.42%  runtime.newstack
     0.15s  2.27% 95.62%      0.15s  2.27%  runtime.add (partial-inline)
     0.11s  1.66% 97.28%      0.13s  1.96%  runtime.(*bmap).keys (inline)
     0.05s  0.76% 98.04%      3.40s 51.36%  go_learning/code/ch48/lock_test.lockAccess.func1

很明顯 lockAccess的cpu耗時3.40s,而lockFreeAccess的cpu耗時是2.07s。

2. 對比ConcurrentHashMap

如果熟悉Java的同學,可以對比一下java的ConcurrentHashMap的實現,在map的數據非常大的情況下,一把鎖會導致大並發的客戶端共爭一把鎖,Java的解決方案是shard, 內部使用多個鎖,每個區間共享一把鎖,這樣減少了數據共享一把鎖帶來的性能影響。go的官方雖然沒有提供類似Java的ConcurrentHashMap的實現,但是我也還是試着去嘗試了下。

1. concurrent_map_benchmark_adapter.go

package maps

import "github.com/easierway/concurrent_map"

type ConcurrentMapBenchmarkAdapter struct {
	cm *concurrent_map.ConcurrentMap
}

func (m *ConcurrentMapBenchmarkAdapter) Set(key interface{}, value interface{}) {
	m.cm.Set(concurrent_map.StrKey(key.(string)), value)
}

func (m *ConcurrentMapBenchmarkAdapter) Get(key interface{}) (interface{}, bool) {
	return m.cm.Get(concurrent_map.StrKey(key.(string)))
}

func (m *ConcurrentMapBenchmarkAdapter) Del(key interface{}) {
	m.cm.Del(concurrent_map.StrKey(key.(string)))
}

func CreateConcurrentMapBenchmarkAdapter(numOfPartitions int) *ConcurrentMapBenchmarkAdapter {
	conMap := concurrent_map.CreateConcurrentMap(numOfPartitions)
	return &ConcurrentMapBenchmarkAdapter{conMap}
}

2. map_benchmark_test.go

package maps

import (
	"strconv"
	"sync"
	"testing"
)

const (
	NumOfReader = 100
	NumOfWriter = 10
)

type Map interface {
	Set(key interface{}, val interface{})
	Get(key interface{}) (interface{}, bool)
	Del(key interface{})
}

func benchmarkMap(b *testing.B, hm Map) {
	for i := 0; i < b.N; i++ {
		var wg sync.WaitGroup
		for i := 0; i < NumOfWriter; i++ {
			wg.Add(1)
			go func() {
				for i := 0; i < 100; i++ {
					hm.Set(strconv.Itoa(i), i*i)
					hm.Set(strconv.Itoa(i), i*i)
					hm.Del(strconv.Itoa(i))
				}
				wg.Done()
			}()
		}
		for i := 0; i < NumOfReader; i++ {
			wg.Add(1)
			go func() {
				for i := 0; i < 100; i++ {
					hm.Get(strconv.Itoa(i))
				}
				wg.Done()
			}()
		}
		wg.Wait()
	}
}

func BenchmarkSyncmap(b *testing.B) {
	b.Run("map with RWLock", func(b *testing.B) {
		hm := CreateRWLockMap()
		benchmarkMap(b, hm)
	})

	b.Run("sync.map", func(b *testing.B) {
		hm := CreateSyncMapBenchmarkAdapter()
		benchmarkMap(b, hm)
	})

	b.Run("concurrent map", func(b *testing.B) {
		superman := CreateConcurrentMapBenchmarkAdapter(199)
		benchmarkMap(b, superman)
	})
}

3. rw_map.go

package maps

import "sync"

type RWLockMap struct {
	m    map[interface{}]interface{}
	lock sync.RWMutex
}

func (m *RWLockMap) Get(key interface{}) (interface{}, bool) {
	m.lock.RLock()
	v, ok := m.m[key]
	m.lock.RUnlock()
	return v, ok
}

func (m *RWLockMap) Set(key interface{}, value interface{}) {
	m.lock.Lock()
	m.m[key] = value
	m.lock.Unlock()
}

func (m *RWLockMap) Del(key interface{}) {
	m.lock.Lock()
	delete(m.m, key)
	m.lock.Unlock()
}

func CreateRWLockMap() *RWLockMap {
	m := make(map[interface{}]interface{}, 0)
	return &RWLockMap{m: m}
}

4. sync_map_benchmark_adapter.go

package maps

import "sync"

func CreateSyncMapBenchmarkAdapter() *SyncMapBenchmarkAdapter {
	return &SyncMapBenchmarkAdapter{}
}

type SyncMapBenchmarkAdapter struct {
	m sync.Map
}

func (m *SyncMapBenchmarkAdapter) Set(key interface{}, val interface{}) {
	m.m.Store(key, val)
}

func (m *SyncMapBenchmarkAdapter) Get(key interface{}) (interface{}, bool) {
	return m.m.Load(key)
}

func (m *SyncMapBenchmarkAdapter) Del(key interface{}) {
	m.m.Delete(key)
}

1 . 讀寫量級一致,相差不多

benchmarkMap設置的是100次寫和100次讀

go test -bench=.
goos: darwin
goarch: amd64
pkg: go_learning/code/ch48/maps
BenchmarkSyncmap/map_with_RWLock-4         	     398	   2780087 ns/op
BenchmarkSyncmap/sync.map-4                	     530	   2173851 ns/op
BenchmarkSyncmap/concurrent_map-4          	     693	   1505866 ns/op
PASS
ok  	go_learning/code/ch48/maps	4.709s

從結果上看基於Java的ConcurrentHashMap的性能更優異,其次是go的sync.map最后才是RWLock.

2. 讀多寫少

benchmarkMap設置的是10次寫和100次讀

go test -bench=.
goos: darwin
goarch: amd64
pkg: go_learning/code/ch48/maps
BenchmarkSyncmap/map_with_RWLock-4         	     630	   1644799 ns/op
BenchmarkSyncmap/sync.map-4                	    2103	    588642 ns/op
BenchmarkSyncmap/concurrent_map-4          	    1088	   1140983 ns/op
PASS
ok  	go_learning/code/ch48/maps	6.286s

從結果上看go的sync.map的性能高於其他兩個不是一點點,所以對於並發操作讀多寫少的情況下,sync.map是嘴合適的選擇。

3. 讀少寫多

benchmarkMap設置的是100次寫和10次讀

go test -bench=.
goos: darwin
goarch: amd64
pkg: go_learning/code/ch48/maps
BenchmarkSyncmap/map_with_RWLock-4         	     788	   1369344 ns/op
BenchmarkSyncmap/sync.map-4                	     650	   1744666 ns/op
BenchmarkSyncmap/concurrent_map-4          	    2065	    577972 ns/op
PASS
ok  	go_learning/code/ch48/maps	5.288s

從結果上看基於Java的ConcurrentHashMap的性能更優異,其次是RWLock最后才是sync.map.為什么sync.map會這么慢,在上面的分析中都有說明。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM