互斥是并發(fā)編程中最關鍵的概念之一。當我們使用 goruntine 和channels 進行并發(fā)編程時,如果兩個?goruntine 嘗試同時訪問同一個內存位置的同一數據會發(fā)生競爭,有時候會產生意想不到的結果,通常很難調試,不符合日常要求,出現錯誤甚至很難修復。
生活場景
假設在生活中可能會發(fā)生的例子:有一個銀行系統(tǒng),我們可以從銀行余額中存款和取款。在一個單線程的同步程序中,這個操作很簡單。我們可以通過少量的單元測試有效地保證它每次都能按計劃工作。
然而,如果我們開始引入多個線程,在 Go?語言中使用多個 goroutine,我們可能會開始在我們的代碼中看到問題。
- 假如有一個余額為 1000 元的客戶。
- 客戶將 500 元存入他的賬戶。
- 一個 goroutine 會看到這個交易,讀取價值為 1000 ,并繼續(xù)將 500 添加到現有的余額中。(此時應該是 1500 的余額)
- 然而,在同一時刻,他拿 800 元來還分期付款的 iphone 13.
- 第二個程序在第一個程序能夠增加 500 元的額外存款之前,讀取了 1000 元的賬戶余額,并繼續(xù)從他的賬戶中扣除 800 元。(1000 - 800 = 200)
- 第二天,客戶檢查了他的銀行余額,發(fā)現他的賬戶余額減少到了 200 元,因為第二個程序沒有意識到第一筆存款,并在存款完成前做了扣除操作。
這就是一個線程競賽的例子,如果我們不小心落入這樣的代碼,我們的并發(fā)程序就會出現問題。
互斥鎖和讀寫鎖
互斥鎖,英文名 Mutex,顧名思義,就是相互排斥,是保護程序中臨界區(qū)的一種方式。
而臨界區(qū)是程序中需要獨占訪問共享資源的區(qū)域。互斥鎖提供了一種安全的方式來表示對這些共享資源的獨占訪問。
為了使用資源,channel 通過通信共享內存,而 Mutex 通過開發(fā)人員的約定同步訪問共享內存。?
讓我們看一個沒有 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
第二次結果: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