當前位置:首頁 > IT技術 > 編程語言 > 正文

Go 語言入門很簡單 -- 16. Go 并發(fā)互斥鎖 #私藏項目實操分享#
2021-12-13 17:42:40

Go 語言入門很簡單 -- 16. Go 并發(fā)互斥鎖 #私藏項目實操分享#_并發(fā)編程

互斥是并發(fā)編程中最關鍵的概念之一。當我們使用 goruntine 和channels 進行并發(fā)編程時,如果兩個?goruntine 嘗試同時訪問同一個內存位置的同一數據會發(fā)生競爭,有時候會產生意想不到的結果,通常很難調試,不符合日常要求,出現錯誤甚至很難修復。

生活場景

假設在生活中可能會發(fā)生的例子:有一個銀行系統(tǒng),我們可以從銀行余額中存款和取款。在一個單線程的同步程序中,這個操作很簡單。我們可以通過少量的單元測試有效地保證它每次都能按計劃工作。

然而,如果我們開始引入多個線程,在 Go?語言中使用多個 goroutine,我們可能會開始在我們的代碼中看到問題。

  1. 假如有一個余額為 1000 元的客戶。
  2. 客戶將 500 元存入他的賬戶。
  3. 一個 goroutine 會看到這個交易,讀取價值為 1000 ,并繼續(xù)將 500 添加到現有的余額中。(此時應該是 1500 的余額)
  4. 然而,在同一時刻,他拿 800 元來還分期付款的 iphone 13.
  5. 第二個程序在第一個程序能夠增加 500 元的額外存款之前,讀取了 1000 元的賬戶余額,并繼續(xù)從他的賬戶中扣除 800 元。(1000 - 800 = 200)
  6. 第二天,客戶檢查了他的銀行余額,發(fā)現他的賬戶余額減少到了 200 元,因為第二個程序沒有意識到第一筆存款,并在存款完成前做了扣除操作。

這就是一個線程競賽的例子,如果我們不小心落入這樣的代碼,我們的并發(fā)程序就會出現問題。

互斥鎖和讀寫鎖

互斥鎖,英文名 Mutex,顧名思義,就是相互排斥,是保護程序中臨界區(qū)的一種方式。

而臨界區(qū)是程序中需要獨占訪問共享資源的區(qū)域。互斥鎖提供了一種安全的方式來表示對這些共享資源的獨占訪問。

為了使用資源,channel 通過通信共享內存,而 Mutex 通過開發(fā)人員的約定同步訪問共享內存。?

Go 語言入門很簡單 -- 16. Go 并發(fā)互斥鎖 #私藏項目實操分享#_并發(fā)編程_02

讓我們看一個沒有 Mutex 的并發(fā)編程示例

package main

import (
"fmt"
"sync"
)

type calculation struct {
sum int
}

func main() {

test := calculation{}
test.sum = 0
wg := sync.WaitGroup{}
for i := 0; i < 500; i++ {
wg.Add(1)
go dosomething(&test, &wg)
}
wg.Wait()
fmt.Println(test.sum)
}

func dosomething(test *calculation, wg *sync.WaitGroup) {
test.sum++
wg.Done()
}

第一次結果為:491

Go 語言入門很簡單 -- 16. Go 并發(fā)互斥鎖 #私藏項目實操分享#_i++_03

第二次結果:493

[Running] go run "e:Coding WorkspacesLearningGoTheEasiestWayconcurrencymutexv0main.go"
493


在上面的例子中,我們聲明了一個名為 test 的計算結構體,并通過 for 循環(huán)產生了多個 GoRoutines,將 sum 的值加 1。(如果你對 GoRoutines 和 WaitGroup 不熟悉,請參考之前的教程)。 我們可能期望 for 循環(huán)后 sum 的值應該是 500。然而,這可能不是真的。 有時,您可能會得到小于 500(當然永遠不會超過 500)的結果。 這背后的原因是兩個 GoRoutine 有一定的概率在相同的內存位置操作相同的變量,從而導致這種意外結果。 這個問題的解決方案是使用互斥鎖。


使用 Mutex

package main

import (
"fmt"
"sync"
)

type calculation struct {
sum int
mutex sync.Mutex
}

func main() {

test := calculation{}
test.sum = 0
wg := sync.WaitGroup{}
for i := 0; i < 500; i++ {
wg.Add(1)
go dosomething(&test, &wg)
}

wg.Wait()
fmt.Println(test.sum)
}

func dosomething(test *calculation, wg *sync.WaitGroup) {
test.mutex.Lock()
test.sum++
test.mutex.Unlock()
wg.Done()
}

結果為:

[Running] go run "e:Coding WorkspacesLearningGoTheEasiestWayconcurrencymutexv0.1main.go"
500

在第二個示例中,我們在結構中添加了一個互斥鎖屬性,它是一種類型的 sync.Mutex。然后我們使用互斥鎖的 Lock() 和 Unlock() 來保護 test.sum 當它被并發(fā)修改時,即 test.sum++。

請記住,使用互斥鎖并非沒有后果,因為它會影響應用程序的性能,因此我們需要適當有效地使用它。 如果你的 GoRoutines 只讀取共享數據而不寫入相同的數據,那么競爭條件就不會成為問題。 在這種情況下,您可以使用 RWMutex 代替 Mutex 來提高性能時間。


Defer 關鍵字

對 Unlock() 使用 defer 關鍵字通常是一個好習慣。

func dosomething(test *calculation) error{
test.mutex.Lock()
defer test.mutex.Unlock()

err1 :=...
if err1 != nil {
return err1
}
err2 :=...
if err2 != nil {
return err2
}
// ... do more stuff ...
return nil
}

在這種情況下,我們有多個 if err!=nil 這可能會導致函數提前退出。 通過使用 defer,無論函數如何返回,我們都可以保證釋放鎖。 否則,我們需要將 Unlock() 放在函數可能返回的每個地方。 然而,這并不意味著我們應該一直使用 defer。 讓我們再看一個例子。

func dosomething(test *calculation){
test.mutex.Lock()
defer test.mutex.Unlock()

// modify the variable which requires mutex protect
test.sum =...

// perform a time consuming IO operation
http.Get()
}

在這個例子中,mutex 不會釋放鎖,直到耗時的函數(這里是 http.Get())完成。 在這種情況下,我們可以在 test.sum=... 行之后解鎖互斥鎖,因為這是我們操作變量的唯一地方。

總結

很多時候 Mutex 并不是單獨使用的,而是嵌套在 Struct 中使用,作為結構體的一部分,如果嵌入的 struct 有多個字段,我們一般會把 Mutex 放在要控制的字段上面,然后使用空格把字段分隔開來。

甚至可以把獲取鎖、釋放鎖、計數加一的邏輯封裝成一個方法。

本文摘自 :https://blog.51cto.com/y

開通會員,享受整站包年服務立即開通 >