真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

go+語言+map+加鎖 go 定義map

徹底理解Golang Map

本文目錄如下,閱讀本文后,將一網(wǎng)打盡下面Golang Map相關(guān)面試題

10年積累的成都網(wǎng)站設(shè)計、網(wǎng)站制作經(jīng)驗,可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認識你,你也不認識我。但先網(wǎng)站制作后付款的網(wǎng)站建設(shè)流程,更有離石免費網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。

Go中的map是一個指針,占用8個字節(jié),指向hmap結(jié)構(gòu)體; 源碼 src/runtime/map.go 中可以看到map的底層結(jié)構(gòu)

每個map的底層結(jié)構(gòu)是hmap,hmap包含若干個結(jié)構(gòu)為bmap的bucket數(shù)組。每個bucket底層都采用鏈表結(jié)構(gòu)。接下來,我們來詳細看下map的結(jié)構(gòu)

bmap 就是我們常說的“桶”,一個桶里面會最多裝 8 個 key,這些 key 之所以會落入同一個桶,是因為它們經(jīng)過哈希計算后,哈希結(jié)果是“一類”的,關(guān)于key的定位我們在map的查詢和插入中詳細說明。在桶內(nèi),又會根據(jù) key 計算出來的 hash 值的高 8 位來決定 key 到底落入桶內(nèi)的哪個位置(一個桶內(nèi)最多有8個位置)。

bucket內(nèi)存數(shù)據(jù)結(jié)構(gòu)可視化如下:

注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 這樣的形式。源碼里說明這樣的好處是在某些情況下可以省略掉 padding字段,節(jié)省內(nèi)存空間。

當 map 的 key 和 value 都不是指針,并且 size 都小于 128 字節(jié)的情況下,會把 bmap 標記為不含指針,這樣可以避免 gc 時掃描整個 hmap。但是,我們看 bmap 其實有一個 overflow 的字段,是指針類型的,破壞了 bmap 不含指針的設(shè)想,這時會把 overflow 移動到 extra 字段來。

map是個指針,底層指向hmap,所以是個引用類型

golang 有三個常用的高級類型 slice 、map、channel, 它們都是 引用類型 ,當引用類型作為函數(shù)參數(shù)時,可能會修改原內(nèi)容數(shù)據(jù)。

golang 中沒有引用傳遞,只有值和指針傳遞。所以 map 作為函數(shù)實參傳遞時本質(zhì)上也是值傳遞,只不過因為 map 底層數(shù)據(jù)結(jié)構(gòu)是通過指針指向?qū)嶋H的元素存儲空間,在被調(diào)函數(shù)中修改 map,對調(diào)用者同樣可見,所以 map 作為函數(shù)實參傳遞時表現(xiàn)出了引用傳遞的效果。

因此,傳遞 map 時,如果想修改map的內(nèi)容而不是map本身,函數(shù)形參無需使用指針

map 底層數(shù)據(jù)結(jié)構(gòu)是通過指針指向?qū)嶋H的元素 存儲空間 ,這種情況下,對其中一個map的更改,會影響到其他map

map 在沒有被修改的情況下,使用 range 多次遍歷 map 時輸出的 key 和 value 的順序可能不同。這是 Go 語言的設(shè)計者們有意為之,在每次 range 時的順序被隨機化,旨在提示開發(fā)者們,Go 底層實現(xiàn)并不保證 map 遍歷順序穩(wěn)定,請大家不要依賴 range 遍歷結(jié)果順序。

map 本身是無序的,且遍歷時順序還會被隨機化,如果想順序遍歷 map,需要對 map key 先排序,再按照 key 的順序遍歷 map。

map默認是并發(fā)不安全的,原因如下:

Go 官方在經(jīng)過了長時間的討論后,認為 Go map 更應(yīng)適配典型使用場景(不需要從多個 goroutine 中進行安全訪問),而不是為了小部分情況(并發(fā)訪問),導(dǎo)致大部分程序付出加鎖代價(性能),決定了不支持。

場景: 2個協(xié)程同時讀和寫,以下程序會出現(xiàn)致命錯誤:fatal error: concurrent map writes

如果想實現(xiàn)map線程安全,有兩種方式:

方式一:使用讀寫鎖 map + sync.RWMutex

方式二:使用golang提供的 sync.Map

sync.map是用讀寫分離實現(xiàn)的,其思想是空間換時間。和map+RWLock的實現(xiàn)方式相比,它做了一些優(yōu)化:可以無鎖訪問read map,而且會優(yōu)先操作read map,倘若只操作read map就可以滿足要求(增刪改查遍歷),那就不用去操作write map(它的讀寫都要加鎖),所以在某些特定場景中它發(fā)生鎖競爭的頻率會遠遠小于map+RWLock的實現(xiàn)方式。

golang中map是一個kv對集合。底層使用hash table,用鏈表來解決沖突 ,出現(xiàn)沖突時,不是每一個key都申請一個結(jié)構(gòu)通過鏈表串起來,而是以bmap為最小粒度掛載,一個bmap可以放8個kv。在哈希函數(shù)的選擇上,會在程序啟動時,檢測 cpu 是否支持 aes,如果支持,則使用 aes hash,否則使用 memhash。

map有3鐘初始化方式,一般通過make方式創(chuàng)建

map的創(chuàng)建通過生成匯編碼可以知道,make創(chuàng)建map時調(diào)用的底層函數(shù)是 runtime.makemap 。如果你的map初始容量小于等于8會發(fā)現(xiàn)走的是 runtime.fastrand 是因為容量小于8時不需要生成多個桶,一個桶的容量就可以滿足

makemap函數(shù)會通過 fastrand 創(chuàng)建一個隨機的哈希種子,然后根據(jù)傳入的 hint 計算出需要的最小需要的桶的數(shù)量,最后再使用 makeBucketArray 創(chuàng)建用于保存桶的數(shù)組,這個方法其實就是根據(jù)傳入的 B 計算出的需要創(chuàng)建的桶數(shù)量在內(nèi)存中分配一片連續(xù)的空間用于存儲數(shù)據(jù),在創(chuàng)建桶的過程中還會額外創(chuàng)建一些用于保存溢出數(shù)據(jù)的桶,數(shù)量是 2^(B-4) 個。初始化完成返回hmap指針。

找到一個 B,使得 map 的裝載因子在正常范圍內(nèi)

Go 語言中讀取 map 有兩種語法:帶 comma 和 不帶 comma。當要查詢的 key 不在 map 里,帶 comma 的用法會返回一個 bool 型變量提示 key 是否在 map 中;而不帶 comma 的語句則會返回一個 value 類型的零值。如果 value 是 int 型就會返回 0,如果 value 是 string 類型,就會返回空字符串。

map的查找通過生成匯編碼可以知道,根據(jù) key 的不同類型,編譯器會將查找函數(shù)用更具體的函數(shù)替換,以優(yōu)化效率:

函數(shù)首先會檢查 map 的標志位 flags。如果 flags 的寫標志位此時被置 1 了,說明有其他協(xié)程在執(zhí)行“寫”操作,進而導(dǎo)致程序 panic。這也說明了 map 對協(xié)程是不安全的。

key經(jīng)過哈希函數(shù)計算后,得到的哈希值如下(主流64位機下共 64 個 bit 位):

m: 桶的個數(shù)

從buckets 通過 hash m 得到對應(yīng)的bucket,如果bucket正在擴容,并且沒有擴容完成,則從oldbuckets得到對應(yīng)的bucket

計算hash所在桶編號:

用上一步哈希值最后的 5 個 bit 位,也就是 01010 ,值為 10,也就是 10 號桶(范圍是0~31號桶)

計算hash所在的槽位:

用上一步哈希值哈希值的高8個bit 位,也就是 10010111 ,轉(zhuǎn)化為十進制,也就是151,在 10 號 bucket 中尋找** tophash 值(HOB hash)為 151* 的 槽位**,即為key所在位置,找到了 2 號槽位,這樣整個查找過程就結(jié)束了。

如果在 bucket 中沒找到,并且 overflow 不為空,還要繼續(xù)去 overflow bucket 中尋找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。

通過上面找到了對應(yīng)的槽位,這里我們再詳細分析下key/value值是如何獲取的:

bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 個 key 的地址就要在此基礎(chǔ)上跨過 i 個 key 的大??;而我們又知道,value 的地址是在所有 key 之后,因此第 i 個 value 的地址還需要加上所有 key 的偏移。

通過匯編語言可以看到,向 map 中插入或者修改 key,最終調(diào)用的是 mapassign 函數(shù)。

實際上插入或修改 key 的語法是一樣的,只不過前者操作的 key 在 map 中不存在,而后者操作的 key 存在 map 中。

mapassign 有一個系列的函數(shù),根據(jù) key 類型的不同,編譯器會將其優(yōu)化為相應(yīng)的“快速函數(shù)”。

我們只用研究最一般的賦值函數(shù) mapassign 。

map的賦值會附帶著map的擴容和遷移,map的擴容只是將底層數(shù)組擴大了一倍,并沒有進行數(shù)據(jù)的轉(zhuǎn)移,數(shù)據(jù)的轉(zhuǎn)移是在擴容后逐步進行的,在遷移的過程中每進行一次賦值(access或者delete)會至少做一次遷移工作。

1.判斷map是否為nil

每一次進行賦值/刪除操作時,只要oldbuckets != nil 則認為正在擴容,會做一次遷移工作,下面會詳細說下遷移過程

根據(jù)上面查找過程,查找key所在位置,如果找到則更新,沒找到則找空位插入即可

經(jīng)過前面迭代尋找動作,若沒有找到可插入的位置,意味著需要擴容進行插入,下面會詳細說下擴容過程

通過匯編語言可以看到,向 map 中刪除 key,最終調(diào)用的是 mapdelete 函數(shù)

刪除的邏輯相對比較簡單,大多函數(shù)在賦值操作中已經(jīng)用到過,核心還是找到 key 的具體位置。尋找過程都是類似的,在 bucket 中挨個 cell 尋找。找到對應(yīng)位置后,對 key 或者 value 進行“清零”操作,將 count 值減 1,將對應(yīng)位置的 tophash 值置成 Empty

再來說觸發(fā) map 擴容的時機:在向 map 插入新 key 的時候,會進行條件檢測,符合下面這 2 個條件,就會觸發(fā)擴容:

1、裝載因子超過閾值

源碼里定義的閾值是 6.5 (loadFactorNum/loadFactorDen),是經(jīng)過測試后取出的一個比較合理的因子

我們知道,每個 bucket 有 8 個空位,在沒有溢出,且所有的桶都裝滿了的情況下,裝載因子算出來的結(jié)果是 8。因此當裝載因子超過 6.5 時,表明很多 bucket 都快要裝滿了,查找效率和插入效率都變低了。在這個時候進行擴容是有必要的。

對于條件 1,元素太多,而 bucket 數(shù)量太少,很簡單:將 B 加 1,bucket 最大數(shù)量( 2^B )直接變成原來 bucket 數(shù)量的 2 倍。于是,就有新老 bucket 了。注意,這時候元素都在老 bucket 里,還沒遷移到新的 bucket 來。新 bucket 只是最大數(shù)量變?yōu)樵瓉碜畲髷?shù)量的 2 倍( 2^B * 2 ) 。

2、overflow 的 bucket 數(shù)量過多

在裝載因子比較小的情況下,這時候 map 的查找和插入效率也很低,而第 1 點識別不出來這種情況。表面現(xiàn)象就是計算裝載因子的分子比較小,即 map 里元素總數(shù)少,但是 bucket 數(shù)量多(真實分配的 bucket 數(shù)量多,包括大量的 overflow bucket)

不難想像造成這種情況的原因:不停地插入、刪除元素。先插入很多元素,導(dǎo)致創(chuàng)建了很多 bucket,但是裝載因子達不到第 1 點的臨界值,未觸發(fā)擴容來緩解這種情況。之后,刪除元素降低元素總數(shù)量,再插入很多元素,導(dǎo)致創(chuàng)建很多的 overflow bucket,但就是不會觸發(fā)第 1 點的規(guī)定,你能拿我怎么辦?overflow bucket 數(shù)量太多,導(dǎo)致 key 會很分散,查找插入效率低得嚇人,因此出臺第 2 點規(guī)定。這就像是一座空城,房子很多,但是住戶很少,都分散了,找起人來很困難

對于條件 2,其實元素沒那么多,但是 overflow bucket 數(shù)特別多,說明很多 bucket 都沒裝滿。解決辦法就是開辟一個新 bucket 空間,將老 bucket 中的元素移動到新 bucket,使得同一個 bucket 中的 key 排列地更緊密。這樣,原來,在 overflow bucket 中的 key 可以移動到 bucket 中來。結(jié)果是節(jié)省空間,提高 bucket 利用率,map 的查找和插入效率自然就會提升。

由于 map 擴容需要將原有的 key/value 重新搬遷到新的內(nèi)存地址,如果有大量的 key/value 需要搬遷,會非常影響性能。因此 Go map 的擴容采取了一種稱為“漸進式”的方式,原有的 key 并不會一次性搬遷完畢,每次最多只會搬遷 2 個 bucket。

上面說的 hashGrow() 函數(shù)實際上并沒有真正地“搬遷”,它只是分配好了新的 buckets,并將老的 buckets 掛到了 oldbuckets 字段上。真正搬遷 buckets 的動作在 growWork() 函數(shù)中,而調(diào)用 growWork() 函數(shù)的動作是在 mapassign 和 mapdelete 函數(shù)中。也就是插入或修改、刪除 key 的時候,都會嘗試進行搬遷 buckets 的工作。先檢查 oldbuckets 是否搬遷完畢,具體來說就是檢查 oldbuckets 是否為 nil。

如果未遷移完畢,賦值/刪除的時候,擴容完畢后(預(yù)分配內(nèi)存),不會馬上就進行遷移。而是采取 增量擴容 的方式,當有訪問到具體 bukcet 時,才會逐漸的進行遷移(將 oldbucket 遷移到 bucket)

nevacuate 標識的是當前的進度,如果都搬遷完,應(yīng)該和2^B的長度是一樣的

在evacuate 方法實現(xiàn)是把這個位置對應(yīng)的bucket,以及其沖突鏈上的數(shù)據(jù)都轉(zhuǎn)移到新的buckets上。

轉(zhuǎn)移的判斷直接通過tophash 就可以,判斷tophash中第一個hash值即可

遍歷的過程,就是按順序遍歷 bucket,同時按順序遍歷 bucket 中的 key。

map遍歷是無序的,如果想實現(xiàn)有序遍歷,可以先對key進行排序

為什么遍歷 map 是無序的?

如果發(fā)生過遷移,key 的位置發(fā)生了重大的變化,有些 key 飛上高枝,有些 key 則原地不動。這樣,遍歷 map 的結(jié)果就不可能按原來的順序了。

如果就一個寫死的 map,不會向 map 進行插入刪除的操作,按理說每次遍歷這樣的 map 都會返回一個固定順序的 key/value 序列吧。但是 Go 杜絕了這種做法,因為這樣會給新手程序員帶來誤解,以為這是一定會發(fā)生的事情,在某些情況下,可能會釀成大錯。

Go 做得更絕,當我們在遍歷 map 時,并不是固定地從 0 號 bucket 開始遍歷,每次都是從一個**隨機值序號的 bucket 開始遍歷,并且是從這個 bucket 的一個 隨機序號的 cell **開始遍歷。這樣,即使你是一個寫死的 map,僅僅只是遍歷它,也不太可能會返回一個固定序列的 key/value 對了。

(十一)golang 內(nèi)存分析

編寫過C語言程序的肯定知道通過malloc()方法動態(tài)申請內(nèi)存,其中內(nèi)存分配器使用的是glibc提供的ptmalloc2。 除了glibc,業(yè)界比較出名的內(nèi)存分配器有Google的tcmalloc和Facebook的jemalloc。二者在避免內(nèi)存碎片和性能上均比glic有比較大的優(yōu)勢,在多線程環(huán)境中效果更明顯。

Golang中也實現(xiàn)了內(nèi)存分配器,原理與tcmalloc類似,簡單的說就是維護一塊大的全局內(nèi)存,每個線程(Golang中為P)維護一塊小的私有內(nèi)存,私有內(nèi)存不足再從全局申請。另外,內(nèi)存分配與GC(垃圾回收)關(guān)系密切,所以了解GC前有必要了解內(nèi)存分配的原理。

為了方便自主管理內(nèi)存,做法便是先向系統(tǒng)申請一塊內(nèi)存,然后將內(nèi)存切割成小塊,通過一定的內(nèi)存分配算法管理內(nèi)存。 以64位系統(tǒng)為例,Golang程序啟動時會向系統(tǒng)申請的內(nèi)存如下圖所示:

預(yù)申請的內(nèi)存劃分為spans、bitmap、arena三部分。其中arena即為所謂的堆區(qū),應(yīng)用中需要的內(nèi)存從這里分配。其中spans和bitmap是為了管理arena區(qū)而存在的。

arena的大小為512G,為了方便管理把arena區(qū)域劃分成一個個的page,每個page為8KB,一共有512GB/8KB個頁;

spans區(qū)域存放span的指針,每個指針對應(yīng)一個page,所以span區(qū)域的大小為(512GB/8KB)乘以指針大小8byte = 512M

bitmap區(qū)域大小也是通過arena計算出來,不過主要用于GC。

span是用于管理arena頁的關(guān)鍵數(shù)據(jù)結(jié)構(gòu),每個span中包含1個或多個連續(xù)頁,為了滿足小對象分配,span中的一頁會劃分更小的粒度,而對于大對象比如超過頁大小,則通過多頁實現(xiàn)。

根據(jù)對象大小,劃分了一系列class,每個class都代表一個固定大小的對象,以及每個span的大小。如下表所示:

上表中每列含義如下:

class: class ID,每個span結(jié)構(gòu)中都有一個class ID, 表示該span可處理的對象類型

bytes/obj:該class代表對象的字節(jié)數(shù)

bytes/span:每個span占用堆的字節(jié)數(shù),也即頁數(shù)乘以頁大小

objects: 每個span可分配的對象個數(shù),也即(bytes/spans)/(bytes/obj)waste

bytes: 每個span產(chǎn)生的內(nèi)存碎片,也即(bytes/spans)%(bytes/obj)上表可見最大的對象是32K大小,超過32K大小的由特殊的class表示,該class ID為0,每個class只包含一個對象。

span是內(nèi)存管理的基本單位,每個span用于管理特定的class對象, 跟據(jù)對象大小,span將一個或多個頁拆分成多個塊進行管理。src/runtime/mheap.go:mspan定義了其數(shù)據(jù)結(jié)構(gòu):

以class 10為例,span和管理的內(nèi)存如下圖所示:

spanclass為10,參照class表可得出npages=1,nelems=56,elemsize為144。其中startAddr是在span初始化時就指定了某個頁的地址。allocBits指向一個位圖,每位代表一個塊是否被分配,本例中有兩個塊已經(jīng)被分配,其allocCount也為2。next和prev用于將多個span鏈接起來,這有利于管理多個span,接下來會進行說明。

有了管理內(nèi)存的基本單位span,還要有個數(shù)據(jù)結(jié)構(gòu)來管理span,這個數(shù)據(jù)結(jié)構(gòu)叫mcentral,各線程需要內(nèi)存時從mcentral管理的span中申請內(nèi)存,為了避免多線程申請內(nèi)存時不斷的加鎖,Golang為每個線程分配了span的緩存,這個緩存即是cache。src/runtime/mcache.go:mcache定義了cache的數(shù)據(jù)結(jié)構(gòu)

alloc為mspan的指針數(shù)組,數(shù)組大小為class總數(shù)的2倍。數(shù)組中每個元素代表了一種class類型的span列表,每種class類型都有兩組span列表,第一組列表中所表示的對象中包含了指針,第二組列表中所表示的對象不含有指針,這么做是為了提高GC掃描性能,對于不包含指針的span列表,沒必要去掃描。根據(jù)對象是否包含指針,將對象分為noscan和scan兩類,其中noscan代表沒有指針,而scan則代表有指針,需要GC進行掃描。mcache和span的對應(yīng)關(guān)系如下圖所示:

mchache在初始化時是沒有任何span的,在使用過程中會動態(tài)的從central中獲取并緩存下來,跟據(jù)使用情況,每種class的span個數(shù)也不相同。上圖所示,class 0的span數(shù)比class1的要多,說明本線程中分配的小對象要多一些。

cache作為線程的私有資源為單個線程服務(wù),而central則是全局資源,為多個線程服務(wù),當某個線程內(nèi)存不足時會向central申請,當某個線程釋放內(nèi)存時又會回收進central。src/runtime/mcentral.go:mcentral定義了central數(shù)據(jù)結(jié)構(gòu):

lock: 線程間互斥鎖,防止多線程讀寫沖突

spanclass : 每個mcentral管理著一組有相同class的span列表

nonempty: 指還有內(nèi)存可用的span列表

empty: 指沒有內(nèi)存可用的span列表

nmalloc: 指累計分配的對象個數(shù)線程從central獲取span步驟如下:

將span歸還步驟如下:

從mcentral數(shù)據(jù)結(jié)構(gòu)可見,每個mcentral對象只管理特定的class規(guī)格的span。事實上每種class都會對應(yīng)一個mcentral,這個mcentral的集合存放于mheap數(shù)據(jù)結(jié)構(gòu)中。src/runtime/mheap.go:mheap定義了heap的數(shù)據(jù)結(jié)構(gòu):

lock: 互斥鎖

spans: 指向spans區(qū)域,用于映射span和page的關(guān)系

bitmap:bitmap的起始地址

arena_start: arena區(qū)域首地址

arena_used: 當前arena已使用區(qū)域的最大地址

central: 每種class對應(yīng)的兩個mcentral

從數(shù)據(jù)結(jié)構(gòu)可見,mheap管理著全部的內(nèi)存,事實上Golang就是通過一個mheap類型的全局變量進行內(nèi)存管理的。mheap內(nèi)存管理示意圖如下:

系統(tǒng)預(yù)分配的內(nèi)存分為spans、bitmap、arean三個區(qū)域,通過mheap管理起來。接下來看內(nèi)存分配過程。

針對待分配對象的大小不同有不同的分配邏輯:

(0, 16B) 且不包含指針的對象: Tiny分配

(0, 16B) 包含指針的對象:正常分配

[16B, 32KB] : 正常分配

(32KB, -) : 大對象分配其中Tiny分配和大對象分配都屬于內(nèi)存管理的優(yōu)化范疇,這里暫時僅關(guān)注一般的分配方法。

以申請size為n的內(nèi)存為例,分配步驟如下:

Golang內(nèi)存分配是個相當復(fù)雜的過程,其中還摻雜了GC的處理,這里僅僅對其關(guān)鍵數(shù)據(jù)結(jié)構(gòu)進行了說明,了解其原理而又不至于深陷實現(xiàn)細節(jié)。1、Golang程序啟動時申請一大塊內(nèi)存并劃分成spans、bitmap、arena區(qū)域

2、arena區(qū)域按頁劃分成一個個小塊。

3、span管理一個或多個頁。

4、mcentral管理多個span供線程申請使用

5、mcache作為線程私有資源,資源來源于mcentral。

Go語言——sync.Map詳解

sync.Map是1.9才推薦的并發(fā)安全的map,除了互斥量以外,還運用了原子操作,所以在這之前,有必要了解下 Go語言——原子操作

go1.10\src\sync\map.go

entry分為三種情況:

從read中讀取key,如果key存在就tryStore。

注意這里開始需要加鎖,因為需要操作dirty。

條目在read中,首先取消標記,然后將條目保存到dirty里。(因為標記的數(shù)據(jù)不在dirty里)

最后原子保存value到條目里面,這里注意read和dirty都有條目。

總結(jié)一下Store:

這里可以看到dirty保存了數(shù)據(jù)的修改,除非可以直接原子更新read,繼續(xù)保持read clean。

有了之前的經(jīng)驗,可以猜測下load流程:

與猜測的 區(qū)別 :

由于數(shù)據(jù)保存兩份,所以刪除考慮:

先看第二種情況。加鎖直接刪除dirty數(shù)據(jù)。思考下貌似沒什么問題,本身就是臟數(shù)據(jù)。

第一種和第三種情況唯一的區(qū)別就是條目是否被標記。標記代表刪除,所以直接返回。否則CAS操作置為nil。這里總感覺少點什么,因為條目其實還是存在的,雖然指針nil。

看了一圈貌似沒找到標記的邏輯,因為刪除只是將他變成nil。

之前以為這個邏輯就是簡單的將為標記的條目拷貝給dirty,現(xiàn)在看來大有文章。

p == nil,說明條目已經(jīng)被delete了,CAS將他置為標記刪除。然后這個條目就不會保存在dirty里面。

這里其實就跟miss邏輯串起來了,因為miss達到閾值之后,dirty會全量變成read,也就是說標記刪除在這一步最終刪除。這個還是很巧妙的。

真正的刪除邏輯:

很繞。。。。

Go Map 為什么是非線程安全的?

Go map 默認是并發(fā)不安全的,同時對 map 進行并發(fā)讀寫的時,程序會 panic,原因如下:Go 官方經(jīng)過長時間的討論,認為 map 適配的場景應(yīng)該是簡單的(不需要從多個 gorountine 中進行安全訪問的),而不是為了小部分情況(并發(fā)訪問),導(dǎo)致大部分程序付出鎖的代價,因此決定了不支持。

并發(fā)讀寫可能引發(fā)的問題

使用 sync.RWMutex 解決并發(fā)讀寫的問題

使用 sync.Map 解決并發(fā)讀寫的問題

實際上, sync.Map 也是通過加鎖的方式實現(xiàn)并發(fā)安全的, sync.Map 源碼的數(shù)據(jù)結(jié)構(gòu)如下:

就像官方考慮的那樣,我們在使用中,應(yīng)盡量避免對 Map 進行并發(fā)讀寫,嘗試通過其他方式解決問題,如數(shù)據(jù)解耦、分布執(zhí)行、動態(tài)規(guī)劃等,真的需要并發(fā)讀寫時,為避免產(chǎn)生并發(fā)讀寫的問題,請使用鎖的機制進行控制

Golang 并發(fā)讀寫map安全問題詳解

下面先寫一段測試程序,然后看下運行結(jié)果:

運行結(jié)果:

發(fā)生了錯誤,提示:fatal error: concurrent map read and map write, map 發(fā)生了同時讀和寫了; 但是這個錯誤并不是每次運行都會出現(xiàn),就是有的時候會出現(xiàn),有的時候并不會出現(xiàn),根據(jù)筆者多次運行結(jié)果(其他例子,讀者可以自己嘗試下)來看還會有另外一種報錯就是:fatal error: concurrent map writes,就是map發(fā)生了同時寫,但是只是讀是不會有問題的。關(guān)于不同的運行結(jié)果小伙伴們可以自己寫幾個例子去測試下。下面就這兩個錯誤的發(fā)生,筆者給出如下解釋:

(1) fatal error: concurrent map read and map write

就是當一個goroutine在寫數(shù)據(jù),而同時另外一個goroutine要讀數(shù)據(jù)就會報錯,不過這個報錯也很好理解:還沒寫完就讀,讀的數(shù)據(jù)會有問題,或者反過來還沒讀完就開始寫了,同樣會導(dǎo)致讀取的數(shù)據(jù)有問題;

(2) fatal error: concurrent map writes

兩個goroutine 同時寫一個內(nèi)存地址,這種操作也是不允許的,會導(dǎo)致一些比較奇怪的問題;

總體來看其實就是寫map的操作和其他的讀或者寫同時發(fā)生了,導(dǎo)致的報錯,做過幾年開發(fā)的人可能會想到使用鎖來解決,比如寫map某個key的時候,通過鎖來保證其他goroutine不能再對其寫或者讀了。

實現(xiàn)思路:

(1) 當寫map的某個key時,通過鎖來保證其他goroutine不能再對其寫或者讀了。

(2) 當讀map的某個key時,通過鎖來保證其他的goroutine不能再對其寫,但是可以讀。

于是我們馬上想到golang 的讀寫鎖貌似符合需求,下面來實現(xiàn)下:

再來看下運行結(jié)果:

發(fā)現(xiàn)沒有報錯了,并且多次運行的結(jié)果都不會報錯,說明這個方法是有用的,不過在go1.9版本后就有sync.Map了,不過這個適用場景是讀多寫少的場景,如果寫很多的話效率比較差,具體的原因在這里筆者就不介紹了,后面會寫篇文章詳細介紹下。

今天的文章就到這里了,如果有不對的地方歡迎小伙伴給我留言,看到會即時回復(fù)的。

Golang中sync.Map的實現(xiàn)原理

前面,我們講了map的用法以及原理 Golang中map的實現(xiàn)原理 ,但我們知道,map在并發(fā)讀寫的情況下是不安全。需要并發(fā)讀寫時,一般的做法是加鎖,但這樣性能并不高,Go語言在 1.9 版本中提供了一種效率較高的并發(fā)安全的 sync.Map,今天,我們就來講講 sync.Map的用法以及原理

sync.Map與map不同,不是以語言原生形態(tài)提供,而是在 sync 包下的特殊結(jié)構(gòu):

我們下來看下sync.Map結(jié)構(gòu)體

結(jié)構(gòu)體之間的關(guān)系如下圖所示:

總結(jié)一下:

Load方法比較簡單,總結(jié)一下:

總結(jié)如下:


網(wǎng)站名稱:go+語言+map+加鎖 go 定義map
網(wǎng)頁路徑:http://www.weahome.cn/article/hidosc.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部