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在儲存鍵值對的時候,只要只讀字典中已存有這個鍵,並且該鍵值對未被標記為“已刪除”,就會把新值存到裡面並直接返回,這種情況下也不需要用到鎖。
否則,它才會在鎖的保護下把鍵值對儲存到髒字典中。這個時候,該鍵值對的“已刪除”標記會被抹去。
sync.Map 中的 read 與 dirty
順便說一句,只有當一個鍵值對應該被刪除,但卻仍然存在於只讀字典中的時候,才會被用標記為“已刪除”的方式進行邏輯刪除,而不會直接被物理刪除。
這種情況會在重建髒字典以後的一段時間內出現。不過,過不了多久,它們就會被真正刪除掉。在查詢和遍歷鍵值對的時候,已被邏輯刪除的鍵值對永遠會被無視。
對於刪除鍵值對,sync.Map會先去檢查只讀字典中是否有對應的鍵。如果沒有,髒字典中可能有,那麼它就會在鎖的保護下,試圖從髒字典中刪掉該鍵值對。
最後,sync.Map會把該鍵值對中指向值的那個指標置為nil,這是另一種邏輯刪除的方式。
除此之外,還有一個細節需要注意,只讀字典和髒字典之間是會互相轉換的。在髒字典中查詢鍵值對次數足夠多的時候,sync.Map會把髒字典直接作為只讀字典,儲存在它的read欄位中,然後把代表髒字典的dirty欄位的值置為nil。
在這之後,一旦再有新的鍵值對存入,它就會依據只讀字典去重建髒字典。這個時候,它會把只讀字典中已被邏輯刪除的鍵值對過濾掉。理所當然,這些轉換操作肯定都需要在鎖的保護下進行。
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
})
}
}
筆記原始碼
https://github.com/MingsonZheng/go-core-demo
本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。