日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Go?Map并發沖突預防與解決_Golang

作者:小馬別過河 ? 更新時間: 2023-01-14 編程語言

背景

關于 Go 語言的 Map,有兩個需要注意的特性:

  • Map 是并發讀寫不安全的,這是出于性能的考慮;
  • Map 并發讀寫導致的錯誤,無法使用 recover 捕獲。

后者意味著,只有出現并發讀寫的問題,服務就會掛掉。

這兩個特性可能大家都知道,可即使有這個共識,我還是見過這個問題導致的事故。

事故的大致情況是,一個人封裝了map的讀寫,沒有使用鎖。另一個人開協程讀寫 map。而測試環境請求量小,不一定會導致崩潰,于是,這個問題就留到生產環境才出現了。

除了靠開發者自覺和 code review,還能怎么預防這種情況呢?我覺得在單元測試加入并行測試也很重要。

并行單元測試

單元測試默認不是并發的,比如下面的單測,是可以通過的:

func TestConcurrent(t *testing.T) {
    var m = map[string]int{}
    // 寫 map
    t.Run("write", func(t *testing.T) {
      for i := 0; i < 10000; i++ {
        m["a"] = 1
      }
    })
    // 讀 map
    t.Run("read", func(t *testing.T) {
      for i := 0; i < 10000; i++ {
        _ = m["a"]
      }
    })
}

但是我們的期望是,上面的單測不通過,該如何解決呢?

testing.T 有一個 Parallel 方法,它表示當前測試會和其他測試并行運行。 如果參數有-test.count-test.cpu,一個測試可能運行多次,同個測試的多個運行實例,不會并行運行。

我們給上面的單測,加上t.Parallel():

func TestConcurrent(t *testing.T) {
    var m = map[string]int{}
    t.Run("write", func(t *testing.T) {
      // 加上并行
      t.Parallel()
      for i := 0; i < 10000; i++ {
        m["a"] = 1
      }
    })
    t.Run("read", func(t *testing.T) {
      // 加上并行
      t.Parallel()
      for i := 0; i < 10000; i++ {
        _ = m["a"]
      }
    })
}

這次執行就會報錯:

fatal error: concurrent map read and map write

支持并發的 Map

讓 Map 支持并發讀寫并不麻煩,常見的做法有:

  • 操作 map 的時候,加上讀寫鎖 sync.RWMutex
  • 使用 sync.Map。

sync.RWMutex 大家用得可能比較多。這里簡單給個demo。

sync.RWMutex

我們給上面的單測加上鎖,這次運行就能通過了。

func TestConcurrent(t *testing.T) {
	var m = map[string]int{}
	//  定義鎖,零值就可以使用
	var mu sync.RWMutex
	t.Run("write", func(t *testing.T) {
		t.Parallel()
		for i := 0; i < 10000; i++ {
			// 鎖
			mu.Lock()
			m["a"] = 1
			// 解鎖
			mu.Unlock()
		}
	})
	t.Run("read", func(t *testing.T) {
		t.Parallel()
		for i := 0; i < 10000; i++ {
			// 鎖
			mu.Lock()
			_ = m["a"]
			// 解鎖
			mu.Unlock()
		}
	})
}

本文的重點介紹一下Go標準庫自帶的,支持并發讀寫的 map:sync.Map

sync.Map

sync.Map 就是線程安全版的 map[interface{}]interface{},零值可以直接使用,值不能復制。它主要用于以下場景:

  • 當同一個 key 的值,寫少讀多的時候;
  • 但多個 goroutines 讀寫或修改一系列不同的key的時候。

上面兩種場景中,比起帶Mutex(或RWMutex)的map,sync.Map 會大大減少鎖的競爭。

sync.Map 提供的方法不多,這里列出一些。注意的是,any 是 go 1.18 中 interface{}的別名。

Store,設置 key-value。

func (m *Map) Store(key, value any)

Load, 根據 key 讀取 value。

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

Delete,刪除某個key。

func (m *Map) Delete(key any)

Range,遍歷所有key, 如果f返回false,會停止遍歷。

func (m *Map) Range(f func(key, value any) bool)

還有 LoadAndDelete(讀后刪除)、LoadOrStore(讀key,不存在時設置)。

我們給上面的單測,使用sync.Map,測試也可以通過。

func TestConcurrent(t *testing.T) {
	// 可以使用零值
	var m sync.Map
	t.Run("write", func(t *testing.T) {
		t.Parallel()
		for i := 0; i < 10000; i++ {
			// 寫
			m.Store("a", 1)
		}
	})
	t.Run("read", func(t *testing.T) {
		t.Parallel()
		for i := 0; i < 10000; i++ {
			// 讀
			v, ok := m.Load("a")
			if ok {
				_ = v.(int)
			}
		}
	})
}

參考

pkg.go.dev/sync#Map

原文鏈接:https://juejin.cn/post/7174771881911222327

欄目分類
最近更新