閱讀本文大約需要 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/