最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

concurrent-map 和 sync.Map,我該選擇哪個(gè)?

2023-02-21 10:14 作者:吳小敏63  | 我要投稿

官方的map并不是線程安全的,如果我們?cè)诙嗑€程中并發(fā)對(duì)一個(gè)map進(jìn)行讀寫操作,是會(huì)引發(fā)panic的。解決方案除了使用鎖來對(duì)map進(jìn)行保護(hù)外,還有兩種方式:

一,開源項(xiàng)目 concurrent-map 提供了可以用來做并發(fā)安全的map

二,Go1.9之后,標(biāo)準(zhǔn)庫提供了一個(gè)sync.Map

這兩種并發(fā)安全的map,我們應(yīng)該怎么選擇呢?

在concurrent-map我看到這么一段話:

標(biāo)準(zhǔn)庫中的sync.Map是專為append-only場(chǎng)景設(shè)計(jì)的。因此,如果您想將Map用于一個(gè)類似內(nèi)存數(shù)據(jù)庫,那么使用我們的版本可能會(huì)受益。你可以在golang repo上讀到更多,這里 and 這里 譯注:sync.Map在讀多寫少性能比較好,否則并發(fā)性能很差

concurrent-map為什么會(huì)有這種表述呢?這篇文章就來庖丁解牛下。

concurrent-map

concurrent-map是Golang中一個(gè)流行的并發(fā)安全的哈希表庫,它允許多個(gè)goroutine同時(shí)對(duì)哈希表進(jìn)行讀寫操作,而不需要使用顯式的鎖或同步原語。

該庫的核心原理是使用分片鎖,將哈希表分成多個(gè)小的哈希表片段,并為每個(gè)片段分配一個(gè)獨(dú)立的鎖。當(dāng)多個(gè)goroutine嘗試同時(shí)讀寫同一個(gè)片段時(shí),只有該片段上的鎖會(huì)被鎖住,而其他片段的鎖則不受影響,從而避免了整個(gè)哈希表被鎖住的情況。

當(dāng)進(jìn)行寫操作時(shí),只需要鎖住要寫入的片段的鎖,以確保原子性操作。當(dāng)進(jìn)行讀操作時(shí),則不需要鎖住片段的鎖,只需要對(duì)該片段上的讀取操作進(jìn)行同步即可。

此外,concurrent-map庫還使用了一些優(yōu)化策略,如緩存哈希值和桶的地址,以減少計(jì)算和查找時(shí)間,從而提高并發(fā)讀寫性能。

總之,concurrent-map庫的原理是基于分片鎖和其他優(yōu)化策略來實(shí)現(xiàn)高效的并發(fā)安全哈希表。

我們先看它的使用方式:

// 創(chuàng)建一個(gè)新的 map. m := cmap.New[string]() // 設(shè)置變量m一個(gè)鍵為“foo”值為“bar”鍵值對(duì) m.Set("foo", "bar") // 從m中獲取指定鍵值. bar, ok := m.Get("foo") // 刪除鍵為“foo”的項(xiàng) m.Remove("foo")

它的New方法創(chuàng)建了一個(gè)ConcurrentMap結(jié)構(gòu)

type ConcurrentMap[K comparable, V any] struct { shards ? []*ConcurrentMapShared[K, V] sharding func(key K) uint32}

我們看ConcurrentMap結(jié)構(gòu)中的shards,是用來代表map分片之后的這些存儲(chǔ)分片ConcurrentMapShared。

而sharing這個(gè)匿名函數(shù)代表的是分配的hash函數(shù)。

而存儲(chǔ)分片是一個(gè)基礎(chǔ)的,帶有互斥鎖的map

type ConcurrentMapShared[K comparable, V any] struct { ? items ? ? ? ?map[K]V ? sync.RWMutex }

所以看到這里我們其實(shí)心里明白了個(gè)七七八八了,再看下它的New/Set/Get的流程如下:

cmap.New

創(chuàng)建一個(gè)ConcurrentMap

初始化ConcurrentMapShared

cmap.Set

根據(jù)需要設(shè)置的key查找對(duì)應(yīng)的ConcurrentMapShared

加鎖寫分片中的map

cmap.Get

根據(jù)需要查找的key找出對(duì)應(yīng)分片ConcurrentMapShared

加讀鎖讀取分片中的map

是的,基本原理就是如上圖所示。concurrent-map就是將一個(gè)大map拆分成若干個(gè)小map,然后用若干個(gè)小mutex 對(duì)這些小map進(jìn)行保護(hù)。這樣,通過降低鎖的粒度提升并發(fā)程度。畢竟嘛,一個(gè)諸葛亮不如十個(gè)臭皮匠。

sync.Map

sync.Map是Golang標(biāo)準(zhǔn)庫中提供的一個(gè)并發(fā)安全的哈希表,它與常規(guī)的map相比,可以在多個(gè)goroutine并發(fā)訪問時(shí),保證數(shù)據(jù)的安全性和一致性。

理解sync.Map,最關(guān)鍵就是理解Map結(jié)構(gòu)。

type Map struct { mu Mutex //互斥鎖,用于鎖定dirty map //優(yōu)先讀map,支持原子操作,注釋中有readOnly不是說read是只讀,而是它的結(jié)構(gòu)體。read實(shí)際上有寫的操作 read atomic.Value // readOnly // dirty是一個(gè)當(dāng)前最新的map,允許讀寫 dirty map[any]*entry // 主要記錄read讀取不到數(shù)據(jù)加鎖讀取read map以及dirty map的次數(shù),當(dāng)misses等于dirty的長(zhǎng)度時(shí),會(huì)將dirty復(fù)制到read misses int}

這里的sync.Map的邏輯還是比較復(fù)雜的。我們?cè)倏此腟tore函數(shù)和Load函數(shù)。

func (m *Map) Store(key, value any) func (m *Map) Load(key any) (value any, ok bool)

我們先把Store的代碼流程圖畫出來

有key

沒有key

在read中存在

在read中不存在
在dirty中存在

在read中不存在
在dirty中不存在

Store

判斷read中是否有key

在read中tryStore

CompareAndSwapPointer

原子替換read中對(duì)應(yīng)指針

加鎖

判斷key的位置

dirty中存入這對(duì)keyvalue

read中原子替換指針

解鎖

dirty中原子替換指針

read中所有元素復(fù)制到dirty一份

read中增加這個(gè)keyvalue

dirty中增加這個(gè)keyvalue

我們看下,這里面有幾個(gè)步驟是非常有細(xì)節(jié)的。

首先,第一次判斷read中是否有key的時(shí)候是沒有加鎖的,所以當(dāng)?shù)谝淮闻袛嘟Y(jié)束后,一旦明確read中沒有key,要做后續(xù)的操作之前,先做一次加鎖操作,做完加鎖操作之后,又判斷了一次key是否在read中。這是為什么呢?其實(shí)是由于在加鎖這個(gè)操作的前后,map還是有可能有變化的,人不可能兩次踏入同一個(gè)河流,map也不可能在加鎖前后兩次都不變,所以這里必須進(jìn)行二次判斷,這里可以說是非常細(xì)節(jié)了。

其次,在判斷read或者dirty中已經(jīng)有key的時(shí)候,Store做的操作不是復(fù)制一份value到目標(biāo)結(jié)構(gòu),而是使用原子替換atomic.StorePointer 來將目標(biāo)map中key對(duì)應(yīng)的value指針替換為參數(shù)value。為什么呢? - 這是極致的性能優(yōu)化寫法,原子替換能減少一次值拷貝操作,做一次指針賦值就能替換拷貝內(nèi)存操作。從這里我們也能理解為什么這個(gè)并發(fā)map會(huì)放在atomic包中,因?yàn)樗膶?shí)現(xiàn)大量依賴atomic的原子操作。

同樣,我們將Load的代碼轉(zhuǎn)化為流程圖如下,

有key

沒有key

有key

沒有key

Load

判斷read中是否有key

直接返回對(duì)應(yīng)的value

加鎖

再次判斷read中是否有key

返回dirty中是否有key

標(biāo)記map的miss值加一

如果miss值大于dirty的個(gè)數(shù)

將dirty中的map通過指針切換到read

dirty置空

標(biāo)記map的miss值為0

從Load中我們大致能看出sync.Map的思路。

sync.Map內(nèi)部使用兩個(gè)map,read和dirty。其實(shí)read的map的作用是擋在讀寫操作的第一個(gè)屏障。如果讀寫在這個(gè)read中能直接操作的話,我們就直接在read中讀寫,那么就可以完全避免使用鎖,性能自然就提升了。

而dirty的作用就相當(dāng)于是一個(gè)緩沖區(qū),一旦要寫的key在read中找不到,我們就會(huì)先寫dirty中。這個(gè)好處是什么?也是不去影響讀read的操作,不會(huì)出現(xiàn)并發(fā)讀寫一個(gè)數(shù)據(jù)結(jié)構(gòu)的情況。

而什么時(shí)候dirty的緩存清空同步到read中呢?就是“當(dāng)map的miss標(biāo)記大于dirty的個(gè)數(shù)的時(shí)候”。

這里我讀的時(shí)候也確實(shí)有這個(gè)疑問,為什么是“當(dāng)miss標(biāo)記個(gè)數(shù)大于dirty個(gè)數(shù)”。而不是當(dāng)miss標(biāo)記個(gè)數(shù)大于某個(gè)值呢?我是這么理解,miss是代表讀操作在read中失效的數(shù)量,而dirty個(gè)數(shù)代表寫操作在read中失效的數(shù)量。如果使用固定值來比對(duì)miss個(gè)數(shù),那么這個(gè)固定值是不好定的,比如一個(gè)有10個(gè)key的map和一個(gè)有10000個(gè)key的map如果都是一樣的固定值,那是明顯不合適的。所以就找了這么個(gè)“浮動(dòng)閾值”。

concurrent-map和sync.map的比較

我們?cè)倩氐阶铋_始的那一段話:

標(biāo)準(zhǔn)庫中的sync.Map是專為append-only場(chǎng)景設(shè)計(jì)的。因此,如果您想將Map用于一個(gè)類似內(nèi)存數(shù)據(jù)庫,那么使用我們的版本可能會(huì)受益。你可以在golang repo上讀到更多,這里 and 這里 譯注:sync.Map在讀多寫少性能比較好,否則并發(fā)性能很差

通過以上的代碼分析,我們看出sync.Map的這個(gè)機(jī)制,是一個(gè)想追求無鎖讀寫的結(jié)構(gòu),它最好的運(yùn)行方式是讀永遠(yuǎn)都命中read,寫只命中dirty,這用能不用任何鎖機(jī)制就能做到map讀寫。而它最差的運(yùn)行狀態(tài)是read和dirty不斷做替換和清理動(dòng)作,性能就無法達(dá)到預(yù)期。而什么時(shí)候可能出現(xiàn)最差運(yùn)行狀態(tài)呢?- 大量的寫操作和大量的讀操作。大量讀寫會(huì)導(dǎo)致“map的miss標(biāo)記大于dirty的個(gè)數(shù)”。 這個(gè)時(shí)候sync.Map中第一層屏障會(huì)失效,dirty就會(huì)頻繁變動(dòng)。

而current-map就相當(dāng)于是一個(gè)比較中等中規(guī)中矩的方案。它的每次讀寫都會(huì)用到鎖,只是這個(gè)鎖的粒度比較小。它的最優(yōu)運(yùn)行方式是我們的所有并發(fā)讀寫都是分散在不同的hash切片中。它的最差運(yùn)行方式就是我們所有的并發(fā)讀寫都集中在一個(gè)hash切片。但是按照實(shí)際運(yùn)行邏輯,這兩種極端情況都不會(huì)發(fā)生。

所以總結(jié)下來,concurrent-map 的這段話確實(shí)沒有騙我們:

sync.Map在讀多寫少性能比較好,而concurrent-map 在key的hash度高的情況下性能比較好。

在無法確定讀寫比的情況下,建議使用 concurrent-map。

最后說一句:世上本沒有煩惱,選擇多了,便有了幸福的煩惱。


concurrent-map 和 sync.Map,我該選擇哪個(gè)?的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
磐石市| 阜康市| 三明市| 西盟| 鹤山市| 安龙县| 康保县| 保亭| 鲁甸县| 保康县| 长兴县| 穆棱市| 锡林浩特市| 龙江县| 海口市| 彩票| 巴楚县| 文化| 普兰店市| 山丹县| 北海市| 页游| 商城县| 彰化市| 东光县| 永清县| 内黄县| 哈巴河县| 马鞍山市| 大兴区| 延寿县| 商南县| 台北市| 丁青县| 安塞县| 奉新县| 会泽县| 许昌市| 东乡族自治县| 滨州市| 峨眉山市|