優化 Go 中的 map 併發存取

莫炎_67288發表於2018-12-28

地址:88250.b3log.org/optimizing-…

Catena (時序儲存引擎)中有一個函式的實現備受爭議,它從 map 中根據指定的 name 獲取一個 metricSource。每一次插入操作都會至少呼叫一次這個函式,現實場景中該函式呼叫更是頻繁,並且是跨多個協程的,因此我們必須要考慮同步。

該函式從 map[string]*metricSource 中根據指定的 name 獲取一個指向 metricSource 的指標,如果獲取不到則建立一個並返回。其中要注意的關鍵點是我們只會對這個 map 進行插入操作。

var source *memorySource
var present bool

p.lock.Lock() // lock the mutex
defer p.lock.Unlock() // unlock the mutex at the end

if source, present = p.sources[name]; !present {
	// The source wasn't found, so we'll create it.
	source = &memorySource{
		name: name,
		metrics: map[string]*memoryMetric{},
	}

	// Insert the newly created *memorySource.
	p.sources[name] = source
}
複製程式碼

經測試,該實現大約可以達到 1,400,000 插入/秒(通過協程併發呼叫,GOMAXPROCS 設定為 4)。看上去很快,但實際上它是慢於單個協程的,因為多個協程間存在鎖競爭。

下面給出不存在竟態條件、執行緒安全,應該算是“正確”的版本了。使用了 RWMutex,讀操作不會被鎖,寫操作保持同步。

var source *memorySource
var present bool

p.lock.RLock()
if source, present = p.sources[name]; !present {
	// The source wasn't found, so we'll create it.
	p.lock.RUnlock()
	p.lock.Lock()
	if source, present = p.sources[name]; !present {
		source = &memorySource{
			name: name,
			metrics: map[string]*memoryMetric{},
		}

		// Insert the newly created *memorySource.
		p.sources[name] = source
	}
	p.lock.Unlock()
} else {
	p.lock.RUnlock()
}
複製程式碼

相關文章