Go 語言 map 是併發安全的嗎?

来源:https://www.cnblogs.com/alwaysbeta/archive/2023/05/27/17436289.html
-Advertisement-
Play Games

**原文鏈接:** [Go 語言 map 是併發安全的嗎?](https://mp.weixin.qq.com/s/4mDzMdMbunR_p94Du65QOA) Go 語言中的 map 是一個非常常用的數據結構,它允許我們快速地存儲和檢索鍵值對。然而,在併發場景下使用 map 時,還是有一些問題需 ...


原文鏈接: Go 語言 map 是併發安全的嗎?

Go 語言中的 map 是一個非常常用的數據結構,它允許我們快速地存儲和檢索鍵值對。然而,在併發場景下使用 map 時,還是有一些問題需要註意的。

本文將探討 Go 語言中的 map 是否是併發安全的,並提供三種方案來解決併發問題。

先來回答一下題目的問題,答案就是併發不安全

看一段代碼示例,當兩個 goroutine 同時對同一個 map 進行寫操作時,會發生什麼?

package main

import "sync"

func main() {
    m := make(map[string]int)
    m["foo"] = 1

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        for i := 0; i < 1000; i++ {
            m["foo"]++
        }
        wg.Done()
    }()

    go func() {
        for i := 0; i < 1000; i++ {
            m["foo"]++
        }
        wg.Done()
    }()

    wg.Wait()
}

在這個例子中,我們可以看到,兩個 goroutine 將嘗試同時對 map 進行寫入。運行這個程式時,我們將看到一個錯誤:

fatal error: concurrent map writes

也就是說,在併發場景下,這樣操作 map 是不行的。

為什麼是不安全的

因為它沒有內置的鎖機制來保護多個 goroutine 同時對其進行讀寫操作。

當多個 goroutine 同時對同一個 map 進行讀寫操作時,就會出現數據競爭和不一致的結果。

就像上例那樣,當兩個 goroutine 同時嘗試更新同一個鍵值對時,最終的結果可能取決於哪個 goroutine 先完成了更新操作。這種不確定性可能會導致程式出現錯誤或崩潰。

Go 語言團隊沒有將 map 設計成併發安全的,是因為這樣會增加程式的開銷並降低性能。

如果 map 內置了鎖機制,那麼每次訪問 map 時都需要進行加鎖和解鎖操作,這會增加程式的運行時間並降低性能。

此外,並不是所有的程式都需要在併發場景下使用 map,因此將鎖機制內置到 map 中會對那些不需要併發安全的程式造成不必要的開銷。

在實際使用過程中,開發人員可以根據程式的需求來選擇是否需要保證 map 的併發安全性,從而在性能和安全性之間做出權衡。

如何併發安全

接下來介紹三種併發安全的方式:

  1. 讀寫鎖
  2. 分片加鎖
  3. sync.Map

加讀寫鎖

第一種方法是使用讀寫鎖,這是最容易想到的一種方式。在讀操作時加讀鎖,在寫操作時加寫鎖。

package main

import (
    "fmt"
    "sync"
)

type SafeMap struct {
    sync.RWMutex
    Map map[string]string
}

func NewSafeMap() *SafeMap {
    sm := new(SafeMap)
    sm.Map = make(map[string]string)
    return sm
}

func (sm *SafeMap) ReadMap(key string) string {
    sm.RLock()
    value := sm.Map[key]
    sm.RUnlock()
    return value
}

func (sm *SafeMap) WriteMap(key string, value string) {
    sm.Lock()
    sm.Map[key] = value
    sm.Unlock()
}

func main() {
    safeMap := NewSafeMap()

    var wg sync.WaitGroup

    // 啟動多個goroutine進行寫操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            safeMap.WriteMap(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i))
        }(i)
    }

    wg.Wait()

    // 啟動多個goroutine進行讀操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d", i)))
        }(i)
    }

    wg.Wait()
}

在這個示例中,我們定義了一個 SafeMap 結構體,它包含一個 sync.RWMutex 和一個 map[string]string

定義了兩個方法:ReadMapWriteMap。在 ReadMap 方法中,我們使用讀鎖來保護對 map 的讀取操作。在 WriteMap 方法中,我們使用寫鎖來保護對 map 的寫入操作。

main 函數中,我們啟動了多個 goroutine 來進行讀寫操作,這些操作都是安全的。

分片加鎖

上例中通過對整個 map 加鎖來實現需求,但相對來說,鎖會大大降低程式的性能,那如何優化呢?其中一個優化思路就是降低鎖的粒度,不對整個 map 進行加鎖。

這種方法是分片加鎖,將這個 map 分成 n 塊,每個塊之間的讀寫操作都互不幹擾,從而降低衝突的可能性。

package main

import (
    "fmt"
    "sync"
)

const N = 16

type SafeMap struct {
    maps  [N]map[string]string
    locks [N]sync.RWMutex
}

func NewSafeMap() *SafeMap {
    sm := new(SafeMap)
    for i := 0; i < N; i++ {
        sm.maps[i] = make(map[string]string)
    }
    return sm
}

func (sm *SafeMap) ReadMap(key string) string {
    index := hash(key) % N
    sm.locks[index].RLock()
    value := sm.maps[index][key]
    sm.locks[index].RUnlock()
    return value
}

func (sm *SafeMap) WriteMap(key string, value string) {
    index := hash(key) % N
    sm.locks[index].Lock()
    sm.maps[index][key] = value
    sm.locks[index].Unlock()
}

func hash(s string) int {
    h := 0
    for i := 0; i < len(s); i++ {
        h = 31*h + int(s[i])
    }
    return h
}

func main() {
    safeMap := NewSafeMap()

    var wg sync.WaitGroup

    // 啟動多個goroutine進行寫操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            safeMap.WriteMap(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i))
        }(i)
    }

    wg.Wait()

    // 啟動多個goroutine進行讀操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d", i)))
        }(i)
    }

    wg.Wait()
}

在這個示例中,我們定義了一個 SafeMap 結構體,它包含一個長度為 N 的 map 數組和一個長度為 N 的鎖數組。

定義了兩個方法:ReadMapWriteMap。在這兩個方法中,我們都使用了一個 hash 函數來計算 key 應該存儲在哪個 map 中。然後再對這個 map 進行讀寫操作。

main 函數中,我們啟動了多個 goroutine 來進行讀寫操作,這些操作都是安全的。

有一個開源項目 orcaman/concurrent-map 就是通過這種思想來做的,感興趣的同學可以看看。

sync.Map

最後,在內置的 sync 包中(Go 1.9+)也有一個線程安全的 map,通過將讀寫分離的方式實現了某些特定場景下的性能提升。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map
    var wg sync.WaitGroup

    // 啟動多個goroutine進行寫操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            m.Store(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i))
        }(i)
    }

    wg.Wait()

    // 啟動多個goroutine進行讀操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            v, _ := m.Load(fmt.Sprintf("name%d", i))
            fmt.Println(v.(string))
        }(i)
    }

    wg.Wait()
}

有了官方的支持,代碼瞬間少了很多,使用起來方便多了。

在這個示例中,我們使用了內置的 sync.Map 類型來存儲鍵值對,使用 Store 方法來存儲鍵值對,使用 Load 方法來獲取鍵值對。

main 函數中,我們啟動了多個 goroutine 來進行讀寫操作,這些操作都是安全的。

總結

Go 語言中的 map 本身並不是併發安全的。

在多個 goroutine 同時訪問同一個 map 時,可能會出現併發不安全的現象。這是因為 Go 語言中的 map 並沒有內置鎖來保護對map的訪問。

儘管如此,我們仍然可以使用一些方法來實現 map 的併發安全。

一種方法是使用讀寫鎖,在讀操作時加讀鎖,在寫操作時加寫鎖。

另一種方法是分片加鎖,將這個 map 分成 n 塊,每個塊之間的讀寫操作都互不幹擾,從而降低衝突的可能性。

此外,在內置的 sync 包中(Go 1.9+)也有一個線程安全的 map,它通過將讀寫分離的方式實現了某些特定場景下的性能提升。

以上就是本文的全部內容,如果覺得還不錯的話歡迎點贊轉發關註,感謝支持。


參考文章:

推薦閱讀:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 哈嘍大家好,今天我們來獲取一下某個生活平臺網站數據,進行可視化分析。 採集58的數據可以使用Python的requests庫和beautifulsoup庫,數據可視化分析可以使用matplotlib庫和seaborn庫。下麵是一個簡單的例子: 1、首先導入需要使用的模塊 import request ...
  • 摘要:如果希望將 JSON 文件導入到 Redis 中,首先要做的就是連接到 redis 服務。 本文分享自華為雲社區《Python將JSON格式文件導入 redis,多種方法》,作者: 夢想橡皮擦 。 在導入前需要先確定你已經安裝 Redis,並且可以啟動相關服務。 windows 上啟動 red ...
  • 好久都沒有寫點東西了,是時候有點寫東西的必要了。 去年下年底離職了,躺了幾個月,最近又兜兜轉轉換了一家公司繼續當牛馬了,前段時間八股文背了好多,難受呀,不過我也趁著前段時間自己也整理了屬於我自己的八股文,有好幾萬字吧,哈哈哈,以後就不用到處去找八股文了。 說回正題,這個group_concat的問題 ...
  • 數據類型是電腦編程中將不同類型的數據值分類和定義的方式。 通過數據類型,可以確定數據的存儲方式和記憶體占用量,瞭解不同類型的數據進行各種運算的能力。 使用`pandas`進行數據分析時,最常用到的幾種類型是: 1. 字元串類型,各類文本內容都是字元串類型 2. 數值類型,包括整數和浮點數,可用於計算 ...
  • # 一、什麼是ByteBuf 我們前面說過,網路數據的基本單位總是位元組。Java NIO 提供了 ByteBuffer 作為它的位元組容器,但是這個類使用起來過於複雜,而且也有些繁瑣。**ByteBuffer 替代品是 ByteBuf**,一個強大的實現,既解決了 JDK API 的局限性,又為網路應 ...
  • 網游找Call的過程中難免會遇到不方便通過數據來找的或者僅僅查找數據根本找不到的東西,但是網游中一般的工程肯定要發給伺服器,比如你打怪,如果都是在本地處理的話就特別容易產生變態功能,而且不方便與其他玩家通信,所以找到了游戲發包的地方,再找功能就易如反掌了。 在游戲逆向過程中,通常會遇到下麵幾種情況的 ...
  • # Rust Tips 比較數值 ### 內容 - 比較與類型轉換 - 浮點類型比較 ### 可以用這些運算符比較數值 `> = <=` ### 無法比較不同類型的值 ```rust fn main() { let a: i32 = 10; let b: u16 = 100; if a < b { ...
  • 今天大概是對python的數據類型等基礎部分進行了簡單的瞭解,同時鞏固了C和C++中忽略的一些問題。 首先是對轉義字元的認識。之前沒有太在意過這些問題,一般只用到"\n"表示換行,因為在C和C++中,貌似換行必須用到"\n"。在python中當然也可以,不過python還有一個好處就是如果你將pri ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...