Go語言核心36講(Go語言實戰與應用十三)–學習筆記

35 | 並發安全字典sync.Map (下)

我們在上一篇文章中談到了,由於並發安全字典提供的方法涉及的鍵和值的類型都是interface{},所以我們在調用這些方法的時候,往往還需要對鍵和值的實際類型進行檢查。

這裡大致有兩個方案。我們上一篇文章中提到了第一種方案,在編碼時就完全確定鍵和值的類型,然後利用 Go 語言的編譯器幫我們做檢查。

這樣做很方便,不是嗎?不過,雖然方便,但是卻讓這樣的字典類型缺少了一些靈活性。

如果我們還需要一個鍵類型為uint32並發安全字典的話,那就不得不再如法炮製地寫一遍代碼了。因此,在需求多樣化之後,工作量反而更大,甚至會產生很多雷同的代碼。

知識擴展

問題 1:怎樣保證並發安全字典中的鍵和值的類型正確性?(方案二)

那麼,如果我們既想保持sync.Map類型原有的靈活性,又想約束鍵和值的類型,那麼應該怎樣做呢?這就涉及了第二個方案。

在第二種方案中,我們封裝的結構體類型的所有方法,都可以與sync.Map類型的方法完全一致(包括方法名稱和方法簽名)。

不過,在這些方法中,我們就需要添加一些做類型檢查的代碼了。另外,這樣並發安全字典的鍵類型和值類型,必須在初始化的時候就完全確定。並且,這種情況下,我們必須先要保證鍵的類型是可比較的。

所以在設計這樣的結構體類型的時候,只包含sync.Map類型的字段就不夠了。

比如:

type ConcurrentMap struct {
 m         sync.Map
 keyType   reflect.Type
 valueType reflect.Type
}

這裡ConcurrentMap類型代表的是:可自定義鍵類型和值類型的並發安全字典。這個類型同樣有一個sync.Map類型的字段m,代表着其內部使用的並發安全字典。

另外,它的字段keyType和valueType,分別用於保存鍵類型和值類型。這兩個字段的類型都是reflect.Type,我們可稱之為反射類型。

這個類型可以代表 Go 語言的任何數據類型。並且,這個類型的值也非常容易獲得:通過調用reflect.TypeOf函數並把某個樣本值傳入即可。

調用表達式reflect.TypeOf(int(123))的結果值,就代表了int類型的反射類型值。

我們現在來看一看ConcurrentMap類型方法應該怎麼寫。

先說Load方法,這個方法接受一個interface{}類型的參數key,參數key代表了某個鍵的值。

因此,當我們根據 ConcurrentMap 在m字段的值中查找鍵值對的時候,就必須保證 ConcurrentMap 的類型是正確的。由於反射類型值之間可以直接使用操作符==或!=進行判等,所以這裡的類型檢查代碼非常簡單。

func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
 if reflect.TypeOf(key) != cMap.keyType {
  return
 }
 return cMap.m.Load(key)
}

我們把一個接口類型值傳入reflect.TypeOf函數,就可以得到與這個值的實際類型對應的反射類型值。

因此,如果參數值的反射類型與keyType字段代表的反射類型不相等,那麼我們就忽略後續操作,並直接返回。

這時,Load方法的第一個結果value的值為nil,而第二個結果ok的值為false。這完全符合Load方法原本的含義。

再來說Store方法。Store方法接受兩個參數key和value,它們的類型也都是interface{}。因此,我們的類型檢查應該針對它們來做。

func (cMap *ConcurrentMap) Store(key, value interface{}) {
 if reflect.TypeOf(key) != cMap.keyType {
  panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
 }
 if reflect.TypeOf(value) != cMap.valueType {
  panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
 }
 cMap.m.Store(key, value)
}

這裡的類型檢查代碼與Load方法中的代碼很類似,不同的是對檢查結果的處理措施。當參數key或value的實際類型不符合要求時,Store方法會立即引發 panic。

這主要是由於Store方法沒有結果聲明,所以在參數值有問題的時候,它無法通過比較平和的方式告知調用方。不過,這也是符合Store方法的原本含義的。

如果你不想這麼做,也是可以的,那麼就需要為Store方法添加一個error類型的結果。

並且,在發現參數值類型不正確的時候,讓它直接返回相應的error類型值,而不是引發 panic。要知道,這裡展示的只一個參考實現,你可以根據實際的應用場景去做優化和改進。

至於與ConcurrentMap類型相關的其他方法和函數,我在這裡就不展示了。它們在類型檢查方式和處理流程上並沒有特別之處。你可以在 demo72.go 文件中看到這些代碼。

稍微總結一下。第一種方案適用於我們可以完全確定鍵和值具體類型的情況。在這種情況下,我們可以利用 Go 語言編譯器去做類型檢查,並用類型斷言表達式作為輔助,就像IntStrMap那樣。

在第二種方案中,我們無需在程序運行之前就明確鍵和值的類型,只要在初始化並發安全字典的時候,動態地給定它們就可以了。這裡主要需要用到reflect包中的函數和數據類型,外加一些簡單的判等操作。

第一種方案存在一個很明顯的缺陷,那就是無法靈活地改變字典的鍵和值的類型。一旦需求出現多樣化,編碼的工作量就會隨之而來。

第二種方案很好地彌補了這一缺陷,但是,那些反射操作或多或少都會降低程序的性能。我們往往需要根據實際的應用場景,通過嚴謹且一致的測試,來獲得和比較程序的各項指標,並以此作為方案選擇的重要依據之一。

問題 2:並發安全字典如何做到盡量避免使用鎖?

sync.Map類型在內部使用了大量的原子操作來存取鍵和值,並使用了兩個原生的map作為存儲介質。

其中一個原生map被存在了sync.Map的read字段中,該字段是sync/atomic.Value類型的。 這個原生字典可以被看作一個快照,它總會在條件滿足時,去重新保存所屬的sync.Map值中包含的所有鍵值對。

為了描述方便,我們在後面簡稱它為只讀字典。不過,只讀字典雖然不會增減其中的鍵,但卻允許變更其中的鍵所對應的值。所以,它並不是傳統意義上的快照,它的只讀特性只是對於其中鍵的集合而言的。

由read字段的類型可知,sync.Map在替換隻讀字典的時候根本用不着鎖。另外,這個只讀字典在存儲鍵值對的時候,還在值之上封裝了一層。

它先把值轉換為了unsafe.Pointer類型的值,然後再把後者封裝,並儲存在其中的原生字典中。如此一來,在變更某個鍵所對應的值的時候,就也可以使用原子操作了。

sync.Map中的另一個原生字典由它的dirty字段代表。 它存儲鍵值對的方式與read字段中的原生字典一致,它的鍵類型也是interface{},並且同樣是把值先做轉換和封裝後再進行儲存的。我們暫且把它稱為髒字典。

注意,髒字典和只讀字典如果都存有同一個鍵值對,那麼這裡的兩個鍵指的肯定是同一個基本值,對於兩個值來說也是如此。

正如前文所述,這兩個字典在存儲鍵和值的時候都只會存入它們的某個指針,而不是基本值。

sync.Map在查找指定的鍵所對應的值的時候,總會先去只讀字典中尋找,並不需要鎖定互斥鎖。只有當確定「只讀字典中沒有,但髒字典中可能會有這個鍵」的時候,它才會在鎖的保護下去訪問髒字典。

相對應的,sync.Map在存儲鍵值對的時候,只要只讀字典中已存有這個鍵,並且該鍵值對未被標記為「已刪除」,就會把新值存到裏面並直接返回,這種情況下也不需要用到鎖。

否則,它才會在鎖的保護下把鍵值對存儲到髒字典中。這個時候,該鍵值對的「已刪除」標記會被抹去。

image

sync.Map 中的 read 與 dirty

順便說一句,只有當一個鍵值對應該被刪除,但卻仍然存在於只讀字典中的時候,才會被用標記為「已刪除」的方式進行邏輯刪除,而不會直接被物理刪除。

這種情況會在重建髒字典以後的一段時間內出現。不過,過不了多久,它們就會被真正刪除掉。在查找和遍歷鍵值對的時候,已被邏輯刪除的鍵值對永遠會被無視。

對於刪除鍵值對,sync.Map會先去檢查只讀字典中是否有對應的鍵。如果沒有,髒字典中可能有,那麼它就會在鎖的保護下,試圖從髒字典中刪掉該鍵值對。

最後,sync.Map會把該鍵值對中指向值的那個指針置為nil,這是另一種邏輯刪除的方式。

除此之外,還有一個細節需要注意,只讀字典和髒字典之間是會互相轉換的。在髒字典中查找鍵值對次數足夠多的時候,sync.Map會把髒字典直接作為只讀字典,保存在它的read字段中,然後把代表髒字典的dirty字段的值置為nil。

在這之後,一旦再有新的鍵值對存入,它就會依據只讀字典去重建髒字典。這個時候,它會把只讀字典中已被邏輯刪除的鍵值對過濾掉。理所當然,這些轉換操作肯定都需要在鎖的保護下進行。

image

sync.Map 中 read 與 dirty 的互換

綜上所述,sync.Map的只讀字典和髒字典中的鍵值對集合,並不是實時同步的,它們在某些時間段內可能會有不同。

由於只讀字典中鍵的集合不能被改變,所以其中的鍵值對有時候可能是不全的。相反,髒字典中的鍵值對集合總是完全的,並且其中不會包含已被邏輯刪除的鍵值對。

因此,可以看出,在讀操作有很多但寫操作卻很少的情況下,並發安全字典的性能往往會更好。在幾個寫操作當中,新增鍵值對的操作對並發安全字典的性能影響是最大的,其次是刪除操作,最後才是修改操作。

如果被操作的鍵值對已經存在於sync.Map的只讀字典中,並且沒有被邏輯刪除,那麼修改它並不會使用到鎖,對其性能的影響就會很小。

總結

這兩篇文章中,我們討論了sync.Map類型,並談到了怎樣保證並發安全字典中的鍵和值的類型正確性。

為了進一步明確並發安全字典中鍵值的實際類型,這裡大致有兩種方案可選。

  • 其中一種方案是,在編碼時就完全確定鍵和值的類型,然後利用 Go 語言的編譯器幫我們做檢查。
  • 另一種方案是,接受動態的類型設置,並在程序運行的時候通過反射操作進行檢查。

這兩種方案各有利弊,前一種方案在擴展性方面有所欠缺,而後一種方案通常會影響到程序的性能。在實際使用的時候,我們一般都需要通過客觀的測試來幫助決策。

另外,在有些時候,與單純使用原生字典和互斥鎖的方案相比,使用sync.Map可以顯着地減少鎖的爭用。sync.Map本身確實也用到了鎖,但是,它會儘可能地避免使用鎖。

可能地避免使用鎖。這就要說到sync.Map對其持有兩個原生字典的巧妙使用了。這兩個原生字典一個被稱為只讀字典,另一個被稱為髒字典。通過對它們的分析,我們知道了並發安全字典的適用場景,以及每種操作對其性能的影響程度。

思考題

今天的思考題是:關於保證並發安全字典中的鍵和值的類型正確性,你還能想到其他的方案嗎?

package main

import (
	"errors"
	"fmt"
	"reflect"
	"sync"
)

// IntStrMap 代表鍵類型為int、值類型為string的並發安全字典。
type IntStrMap struct {
	m sync.Map
}

func (iMap *IntStrMap) Delete(key int) {
	iMap.m.Delete(key)
}

func (iMap *IntStrMap) Load(key int) (value string, ok bool) {
	v, ok := iMap.m.Load(key)
	if v != nil {
		value = v.(string)
	}
	return
}

func (iMap *IntStrMap) LoadOrStore(key int, value string) (actual string, loaded bool) {
	a, loaded := iMap.m.LoadOrStore(key, value)
	actual = a.(string)
	return
}

func (iMap *IntStrMap) Range(f func(key int, value string) bool) {
	f1 := func(key, value interface{}) bool {
		return f(key.(int), value.(string))
	}
	iMap.m.Range(f1)
}

func (iMap *IntStrMap) Store(key int, value string) {
	iMap.m.Store(key, value)
}

// ConcurrentMap 代表可自定義鍵類型和值類型的並發安全字典。
type ConcurrentMap struct {
	m         sync.Map
	keyType   reflect.Type
	valueType reflect.Type
}

func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {
	if keyType == nil {
		return nil, errors.New("nil key type")
	}
	if !keyType.Comparable() {
		return nil, fmt.Errorf("incomparable key type: %s", keyType)
	}
	if valueType == nil {
		return nil, errors.New("nil value type")
	}
	cMap := &ConcurrentMap{
		keyType:   keyType,
		valueType: valueType,
	}
	return cMap, nil
}

func (cMap *ConcurrentMap) Delete(key interface{}) {
	if reflect.TypeOf(key) != cMap.keyType {
		return
	}
	cMap.m.Delete(key)
}

func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
	if reflect.TypeOf(key) != cMap.keyType {
		return
	}
	return cMap.m.Load(key)
}

func (cMap *ConcurrentMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	if reflect.TypeOf(key) != cMap.keyType {
		panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
	}
	if reflect.TypeOf(value) != cMap.valueType {
		panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
	}
	actual, loaded = cMap.m.LoadOrStore(key, value)
	return
}

func (cMap *ConcurrentMap) Range(f func(key, value interface{}) bool) {
	cMap.m.Range(f)
}

func (cMap *ConcurrentMap) Store(key, value interface{}) {
	if reflect.TypeOf(key) != cMap.keyType {
		panic(fmt.Errorf("wrong key type: %v", reflect.TypeOf(key)))
	}
	if reflect.TypeOf(value) != cMap.valueType {
		panic(fmt.Errorf("wrong value type: %v", reflect.TypeOf(value)))
	}
	cMap.m.Store(key, value)
}

// pairs 代表測試用的鍵值對列表。
var pairs = []struct {
	k int
	v string
}{
	{k: 1, v: "a"},
	{k: 2, v: "b"},
	{k: 3, v: "c"},
	{k: 4, v: "d"},
}

func main() {
	// 示例1。
	var sMap sync.Map
	//sMap.Store([]int{1, 2, 3}, 4) // 這行代碼會引發panic。
	_ = sMap

	// 示例2。
	{
		var iMap IntStrMap
		iMap.Store(pairs[0].k, pairs[0].v)
		iMap.Store(pairs[1].k, pairs[1].v)
		iMap.Store(pairs[2].k, pairs[2].v)
		fmt.Println("[Three pairs have been stored in the IntStrMap instance]")

		iMap.Range(func(key int, value string) bool {
			fmt.Printf("The result of an iteration in Range: %d, %s\n",
				key, value)
			return true
		})

		k0 := pairs[0].k
		v0, ok := iMap.Load(k0)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v0, ok, k0)

		k3 := pairs[3].k
		v3, ok := iMap.Load(k3)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v3, ok, k3)

		k2, v2 := pairs[2].k, pairs[2].v
		actual2, loaded2 := iMap.LoadOrStore(k2, v2)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual2, loaded2, k2, v2)
		v3 = pairs[3].v
		actual3, loaded3 := iMap.LoadOrStore(k3, v3)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual3, loaded3, k3, v3)

		k1 := pairs[1].k
		iMap.Delete(k1)
		fmt.Printf("[The pair with the key of %v has been removed from the IntStrMap instance]\n",
			k1)
		v1, ok := iMap.Load(k1)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v1, ok, k1)
		v1 = pairs[1].v
		actual1, loaded1 := iMap.LoadOrStore(k1, v1)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual1, loaded1, k1, v1)

		iMap.Range(func(key int, value string) bool {
			fmt.Printf("The result of an iteration in Range: %d, %s\n",
				key, value)
			return true
		})
	}
	fmt.Println()

	// 示例2。
	{
		cMap, err := NewConcurrentMap(
			reflect.TypeOf(pairs[0].k), reflect.TypeOf(pairs[0].v))
		if err != nil {
			fmt.Printf("fatal error: %s", err)
			return
		}
		cMap.Store(pairs[0].k, pairs[0].v)
		cMap.Store(pairs[1].k, pairs[1].v)
		cMap.Store(pairs[2].k, pairs[2].v)
		fmt.Println("[Three pairs have been stored in the ConcurrentMap instance]")

		cMap.Range(func(key, value interface{}) bool {
			fmt.Printf("The result of an iteration in Range: %d, %s\n",
				key, value)
			return true
		})

		k0 := pairs[0].k
		v0, ok := cMap.Load(k0)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v0, ok, k0)

		k3 := pairs[3].k
		v3, ok := cMap.Load(k3)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v3, ok, k3)

		k2, v2 := pairs[2].k, pairs[2].v
		actual2, loaded2 := cMap.LoadOrStore(k2, v2)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual2, loaded2, k2, v2)
		v3 = pairs[3].v
		actual3, loaded3 := cMap.LoadOrStore(k3, v3)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual3, loaded3, k3, v3)

		k1 := pairs[1].k
		cMap.Delete(k1)
		fmt.Printf("[The pair with the key of %v has been removed from the ConcurrentMap instance]\n",
			k1)
		v1, ok := cMap.Load(k1)
		fmt.Printf("The result of Load: %v, %v (key: %v)\n",
			v1, ok, k1)
		v1 = pairs[1].v
		actual1, loaded1 := cMap.LoadOrStore(k1, v1)
		fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
			actual1, loaded1, k1, v1)

		cMap.Range(func(key, value interface{}) bool {
			fmt.Printf("The result of an iteration in Range: %d, %s\n",
				key, value)
			return true
		})
	}
}

筆記源碼

//github.com/MingsonZheng/go-core-demo

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發佈,但務必保留文章署名 鄭子銘 (包含鏈接: //www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。