首頁 > 軟體

Go並行與鎖的兩種方式該如何提效詳解

2022-12-27 14:01:06

並行安全,就是多個並行體在同一段時間記憶體取同一個共用資料,共用資料能被正確處理。

很多語言的並行程式設計很容易在同時修改某個變數的時候,因為操作不是原子的,而出現錯誤計算,比如一個加法運算使用中的變數被修改,而導致計算結果出錯,典型的像統計商品庫存。

個人建議只要涉及到共用變數統統使用channel,因為channel原始碼中使用了互斥鎖,它是並行安全的。

我們可以不用,但不可以不瞭解,手中有糧心中不慌。

並行不安全的例子

陣列是並行不安全的,在例子開始前我們要知道append函數的行為:長度足夠在原陣列cap內追加函數,增加len,長度不夠則觸發擴容,申請新陣列cap增加一倍,賦值遷移。

所以在這個過程中,僅討論擴容操作的話可能存在同時申請並賦值的情況,導致漏掉某次擴容增加的資料。

var s []int

func appendValue(i int) {
	s = append(s, i)
}

func main() {
	for i := 0; i < 10000; i++ { //10000個協程同時新增切片
		go appendValue(i)
	}
    time.Sleep(2)
    fmt.Println(len(s))
}

比如上例,10000 個協程同時為切片增加資料,你可以嘗試執行一下,列印出來的一定不是 10000

  • 以上並行操作的同一個資源,專業名詞叫做臨界區
  • 因為並行操作存在資料競爭,導致資料值意外改寫,最後的結果與期待的不符,這種問題統稱為競態問題

常見於控制商品減庫存,控制餘額增減等情況。 那麼有什麼辦法解決競態問題呢?

  • 互斥鎖:讓存取某個臨界區的時候,只有一個 goroutine 可以存取。
  • 原子操作:讓某些操作變成原子的,這個後續討論。

這兩個思路貫穿了無數的高並行/分散式方案,區別是在一個程序應用中使用,還是藉助某些第三方手段來實現,比如中介軟體。獨孤九劍森羅萬象一定要牢牢記住。

互斥鎖

Go 語言中互斥鎖的用法如下:

var lock sync.Mutex //互斥鎖
lock.Lock() //加鎖
s = append(s, i)
lock.Unlock() //解鎖

在存取臨界區的前後加上互斥鎖,就可以保證不會出現並行問題。

我們修改還是上一個4.7.1的例子,為其增加互斥鎖。

var s []int
var lock sync.Mutex

appendValueSafe := func(i int) {
    lock.Lock()
    s = append(s, i)
    lock.Unlock()
}

for i := 0; i < 10000; i++ { //10000個協程同時新增切片
    go appendValueSafe(i)
}
time.Sleep(2)
fmt.Println(len(s))
  • 對共用變數s的寫入操作加互斥鎖,保證同一時刻只有一個goroutine修改內容。
  • 加鎖之後到解鎖之前的內容,同一時刻只有一個存取,無論讀寫。
  • 無論多少次都輸出10000,不會再出現競態問題。
  • 要注意:如果在寫的同時,有並行讀操作時,為了防止不要讀取到寫了一半資料,需要為讀操作也加鎖。

讀寫鎖

互斥鎖是完全互斥的,並行讀沒有修改的情況下是不會有問題的,也沒有必要在並行讀的時候加鎖不然效率會變低。

用法:

rwlock sync.RWMutex
//讀鎖
rwlock.RLock()
rwlock.RUnlock()

//寫鎖
rwlock.Lock()
rwlock.Unlock()

並行讀不互斥可以同時,在一個寫鎖獲取時,其他所有鎖都等待, 口訣:讀讀不互斥、讀寫互斥、寫寫互斥。具體測算速度的程式碼可以見4.7.3的原始碼,感興趣的可以改下注釋位置看下效率是有很明顯的提升的。

小結

  • 學習了幾個名詞:臨界區、競態問題、互斥鎖、原子操作、讀寫鎖。
  • 互斥鎖:sync.Mutex, 讀寫鎖:sync.RWMutex 都是 sync 包的。
  • 讀寫鎖比互斥鎖效率高。

問題:只加寫鎖可以嗎?為什麼?

總結

到此這篇關於Go並行與鎖的兩種方式該如何提效的文章就介紹到這了,更多相關Go並行與鎖提效內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com