golang map是線程安全的嗎


不是線程安全的。在同一時間段內,讓不同 goroutine 中的代碼,對同一個字典進行讀寫操作是不安全
的。字典值本身可能會因這些操作而產生混亂,相關的程序也可能會因此發生不可預知的問題。

1.什么是map?

map是一個可以存儲key/value對的一種數據結構,map像slice一樣是引用類型,map內部實現是一個hash table,因此在map中存入的數據是無序的(map內部實現)。而每次從map中讀取的數據也是無序的,因為golang在設計之初,map迭代器的順序就是隨機的,有別於C/C++,雖然存入map的數據是無序的,但是每次從map中讀取的數據是一樣的

聲明和初始化

// 聲明一個map,因為map是引用類型,所以m是nil
var m map[KeyType]ValueType
// 初始化方式一,空map,空並不是nil
m := map[KeyType]ValueType{}
//初始化方式二,兩種初始化的方式是等價的
m := make(map[KeyType]ValueType)

基本操作

m := map[string]int{}
// 增加一個key/value對
m["Tony"] = 10
// 刪除Key Tony
delete(m, "Tony")
// 修改Key Tony的值
m["Tony"] = 20
// 判斷某個Key是否存在
if _, ok := m["Tony"]; ok {
    fmt.Println("Tony is exists")
}
// 遍歷map
for key, value := range m {
    fmt.Printf("Key = %s, Value = %d", key, value)
}
// 使用多個值對map進行初始化
mp := map[string]int {
    "Tina": 10,
    "Divad": 20,
    "Tom": 5,
}

Key和Value可以使用什么類型?

Key :只要是可比較(可以使用==進行比較,兩邊的操作數可以相互賦值)的類型就可以,像整形,字符串類型,浮點型,數組(必須類型相同);而map,slice和function不能作為Key的類型。

Value :任何類型都可以。

2.如何安全的使用map

方式一:sync.Map

在 2017 年發布的 Go 1.9 中正式加入了並發安全的字典類型sync.Map。這個字典類型提供了一些常用的鍵值存取操作方法,並保證了這些操作的並發安全。同時,它的存、取、刪等操作都可以基本保證在常數時間內執行完畢。換句話說,它們的算法復雜度與map類型一樣都是O(1)的。在有些時候,與單純使用原生map和互斥鎖的方案相比,使用sync.Map可以顯著地減少鎖的爭用。sync.Map本身雖然也用到了鎖,但是,它其實在盡可能地避免使用鎖。

代碼:

 var ma sync.Map// 該類型是開箱即用,只需要聲明既可
    ma.Store("key", "value") // 存儲值
    ma.Delete("key") //刪除值
    ma.LoadOrStore("key", "value")// 獲取值,如果沒有則存儲
    fmt.Println(ma.Load("key"))//獲取值
    
    //遍歷
    ma.Range(func(key, value interface{}) bool {
        fmt.Printf("key:%s ,value:%s \n", key, value)
        //如果返回:false,則退出循環,
        return true
    })

方式二:增加同步機制

map在並發訪問中使用不安全,因為不清楚當同時對map進行讀寫的時候會發生什么,如果像通過goroutine進行並發訪問,則需要一種同步機制來保證訪問數據的安全性。一種方式是使用sync.RWMutex

// 通過匿名結構體聲明了一個變量counter,變量中包含了map和sync.RWMutex
var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}
// 讀取數據的時候使用讀鎖
counter.RLock()
n := counter.m["Tony"]
counter.RUnlock()
// 寫數據的使用使用寫鎖
counter.Lock()
counter.m["Tony"]++
counter.Unlock()

擴展:

map映射過程

哈希表中查找與某個鍵值對應的那個元素值,那么我們需要先把鍵值作為參數傳給這個哈希表。哈希表會先用哈希函數(hash function)把鍵值轉換為哈希值。哈希值通常是一個無符號的整數。一個哈希表會持有一定
數量的桶(bucket),也可稱之為哈希桶,這些哈希桶會均勻地儲存其所屬哈希表收納的那些鍵 - 元素對。因此,哈希表會先用這個鍵的哈希值的低幾位去定位到一個哈希桶,然后再去這個哈希桶中,查找這個鍵。
由於鍵 - 元素對總是被捆綁在一起存儲的,所以一旦找到了鍵,就一定能找到對應的元素值。隨后,哈希表就會把相應的元素值作為結果返回。只要這個鍵 - 元素對存在於哈希表中就一定會被查找到。

為什么說並發安全字典在盡量避免使用鎖?

//sync.Map 包的結構
type Map struct {
   mu Mutex //
   
   /*
        由read字段的類型可知,sync.Map在替換只讀字典的時候根本用不着鎖。另外,這個只讀字典
    在存儲鍵值對的時候,還在值之上封裝了一層。它先把值轉換為了unsafe.Pointer類型的值,然后再把后者封裝,並儲存在其中的原生字典中。如此一來,在變更某個鍵所對應的值的時候,就也可以使用原子操作了。
   */
   read atomic.Value// 只讀字典
   /*
        它存儲鍵值對的方式與read字段中的原生字典一致,它的鍵類型也是interface{},並且同樣是把值先做   轉換和封裝后再進行儲存的
   */
   dirty map[interface{}]*entry//臟字典。
   misses int//重建的判斷條件
}

查找鍵值對的時候,會先去只讀字典中尋找,並不需要鎖定互斥鎖。只有當確定“只讀字典中沒有,但臟字典中可能會有這個鍵”的時候,它才會在鎖的保護下去訪問臟字典。相對應的,sync.Map在存儲鍵值對的時候,只要只讀字典中已存有這個鍵,並且該鍵值對未被標記為“已刪除”,就會把新值存到里面並直接返回,這種情況下也不需要用到鎖。否則,它才會在鎖的保護下把鍵值對存儲到臟字典中。這個時候,該鍵值對的“已刪除”標記會被
抹去。

當一個鍵值對應該被刪除,但卻仍然存在於只讀字典中的時候,才會被用標記為“已刪除”的方式進行邏輯刪除,而不會直接被物理刪除。

這種情況會在重建臟字典以后的一段時間內出現。不過,過不了多久,它們就會被真正刪除掉。
在查找和遍歷鍵值對的時候,已被邏輯刪除的鍵值對永遠會被無視。

對於刪除鍵值對,sync.Map會先去檢查只讀字典中是否有對應的鍵。如果沒有,臟字典中可能有,那么它就會在鎖的保護下,試圖從臟字典中刪掉該鍵值對。最后,sync.Map會把該鍵值對中指向值的那個指針置為nil,這是另一種邏輯刪除的方式。

除此之外,還有一個細節需要注意,只讀字典和臟字典之間是會互相轉換的。在臟字典中查找鍵
值對次數足夠多的時候,sync.Map會把臟字典直接作為只讀字典,保存在它的read字段中,然
后把代表臟字典的dirty字段的值置為nil。

在讀操作有很多但寫操作卻很少的情況下,並發安全字典的性能往往會更好。在幾個寫操作當中,新增鍵值對的操作對並發安全字典的性能影響是最大的,其次是刪除操作,最后才是修改操作。

如果被操作的鍵值對已經存在於sync.Map的只讀字典中,並且沒有被邏輯刪除,那么修改它並
不會使用到鎖,對其性能的影響就會很小。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM