大白話講講 Go 語言的 sync.Map(一)

2023-07-20 06:00:32

閱讀本文大約需要 4.25 分鐘。

程式是枯燥乏味的。

在講 sync.Map 之前,我們先說說什麼是 map(對映)。

我們每個人都有身份證號碼,如果我需要從身份證號碼查到對應的姓名,用 map 儲存是非常合適的。

map[000...001] = 張三
map[000...002] = 李四
...
map[999...993] = 錢五

身份證號碼有 18 位,如果要知道 111...002 這個人叫什麼名字,沒有 map 我只能從 000...001 一個一個往下查詢,效率是非常低的。

咦,那 map 不就是在查字典嘛?根據拼音、筆畫、部首,可以查到某個字的具體含義!

沒錯!Go 語言中的 map 在 Python 語言稱之為 dict(字典),意思是完全一樣的。

再設想另一個場景,

如果 map 儲存的是每個人銀行卡里的餘額(同一所銀行),那就是這樣子的形式(賬本):

map[張三] = 100.00
map[李四] = 600.00
map[錢五] = 800.00

某一天,李四要轉賬給張三和錢五,各 100 元,銀行為了提高轉賬速度,安排了兩名交易員同時處理。

交易員 A 和交易員 B 瞄了一眼賬本,開始操作:

交易員 A:李四的餘額是 600 元,張三的餘額是 100 元,轉賬後李四的餘額是 500 元,張三的餘額是 200 元。

交易員 B:李四的餘額是 600 元,錢五的餘額是 800 元,轉賬後李四的餘額是 500 元,錢五的餘額是 900 元。

賬本變成這個樣子:

map[張三] = 200.00
map[李四] = 500.00
map[錢五] = 900.00

賬本出問題了!銀行憑空多出 100 元!

一個一個來不就完了?可是你別忘了,我們是為了提高轉賬速度,才這樣做的。

在 Go 1.9 之前,大部分人還真的就是這麼幹的!

type Name        string
type Money       string
type AccountBook struct {
    lock sync.RWMutex
    m    map[Name]Money
}

sync.RWMutex 是一個讀寫鎖,在寫入資料的時候,阻止其他人寫入、讀取,讓其他人處於等待的狀態,直到操作完再釋放鎖。

本質上,上面的例子,就是讀取到了髒資料,如果能等待交易員 A 把賬本改完,交易員 B 再去操作,賬本就不會亂了。

如果你不知道鎖是什麼,我再給你講一個例子:

張三和李四兩個人,需要列印不同的檔案,

印表機只有一臺,放在列印室裡,列印室有鑰匙,

鑰匙只有一把,誰拿到列印室的鑰匙,誰就能進去列印。

列印室的鑰匙,就是鎖。

張三拿了鑰匙,進去列印室,列印完了,就出來後把鑰匙給了李四,李四列印完了把鑰匙還回列印室(真是有條不紊)。

我花費這麼多筆墨說 map,也是真的希望,就算你不是程式設計師,不是 Go 語言後端工程師,也可以看懂我的文章。

不得不承認,把複雜瑣碎的東西,講通透、講明白是一種本事。

教科書講 if...else、switch、while (true) 、異常和捕獲,

如果有下面的圖片這麼形象生動就好了:

看到圖片的那一瞬間,真的把我逗樂了。

多麼形象生動啊!

回頭想想,大學的 C 語言課程是多少人的噩夢,老師都是照書唸的,完全聽不進去。

我也不感慨了,咱們還是迴歸正題。

剛剛講了 map,接著往下講 sync.Map,它用來解決什麼問題?

我們知道 map + 鎖的形式,還是有等待的現象出現,不符合我們提高轉賬速度的初衷。

而 sync.Map 有一個非常巧妙的抽象(entry 的 p 指向具體資料的位置):

var m map[key]*entry

type entry struct {
    p unsafe.Pointer
}

還是看回上面的例子,做個小修改——原先的 map 是一個小賬本,我們又做了一個大賬本,原先的賬本變成:

map[張三] = 記錄在大賬本第 6 頁(翻開第 6 頁,內容是:100.00)
map[李四] = 記錄在大賬本第 7 頁(翻開第 7 頁,內容是:600.00)
map[錢五] = 記錄在大賬本第 8 頁(翻開第 8 頁,內容是:800.00)

假設小賬本 map 的張三、李四隻能一個一個排隊改,沒辦法做到同時修改,

而我們有了大賬本,可以直接同時修改張三、李四紙上的內容(兩頁紙互不影響了)。

(真實的計算機世界確實如此,具體是怎麼樣的,留一個思考題,下一篇文章細細解答)

更通俗的講,sync.Map 通過 entry 這個中間層的抽象,

把最開始整個小賬本的衝突(影響所有人),降低到大賬本上的某一頁紙(隻影響某個人),

用計算機術語講,就是降低鎖的粒度,從而提升效能!

另一方面,假設李四銷戶了,

我可以選擇在第 7 頁的紙上寫,已銷戶(expunged),

// expunged is an arbitrary pointer that marks
// entries which have been deleted from the 
// dirty map.
var expunged = unsafe.Pointer(new(interface{}))

如果是以前,只能把小賬本,李四那一張紙撕掉,

而撕掉小賬本的某一頁,也會影響所有人使用小賬本,

如果下次要把撕掉的那一頁放回去,也是非常麻煩,

在計算機的世界裡,這是資源的分配和回收的問題,會嚴重影響程式執行效率。

寫了一千七百字,直到現在只是冰山一角,sync.Map 的巧妙之處,遠遠不止 entry 的抽象。

今天先消化這麼多,下一篇文章會更深層次一些,敬請期待!


文章來源於本人部落格,釋出於 2021-05-04,原文連結:https://imlht.com/archives/234/