互斥是并發(fā)編程中最關(guān)鍵的概念之一。當(dāng)我們使用 goruntine 和channels 進(jìn)行并發(fā)編程時(shí),如果兩個(gè)?goruntine 嘗試同時(shí)訪問(wèn)同一個(gè)內(nèi)存位置的同一數(shù)據(jù)會(huì)發(fā)生競(jìng)爭(zhēng),有時(shí)候會(huì)產(chǎn)生意想不到的結(jié)果,通常很難調(diào)試,不符合日常要求,出現(xiàn)錯(cuò)誤甚至很難修復(fù)。
生活場(chǎng)景
假設(shè)在生活中可能會(huì)發(fā)生的例子:有一個(gè)銀行系統(tǒng),我們可以從銀行余額中存款和取款。在一個(gè)單線程的同步程序中,這個(gè)操作很簡(jiǎn)單。我們可以通過(guò)少量的單元測(cè)試有效地保證它每次都能按計(jì)劃工作。
然而,如果我們開(kāi)始引入多個(gè)線程,在 Go?語(yǔ)言中使用多個(gè) goroutine,我們可能會(huì)開(kāi)始在我們的代碼中看到問(wèn)題。
- 假如有一個(gè)余額為 1000 元的客戶。
- 客戶將 500 元存入他的賬戶。
- 一個(gè) goroutine 會(huì)看到這個(gè)交易,讀取價(jià)值為 1000 ,并繼續(xù)將 500 添加到現(xiàn)有的余額中。(此時(shí)應(yīng)該是 1500 的余額)
- 然而,在同一時(shí)刻,他拿 800 元來(lái)還分期付款的 iphone 13.
- 第二個(gè)程序在第一個(gè)程序能夠增加 500 元的額外存款之前,讀取了 1000 元的賬戶余額,并繼續(xù)從他的賬戶中扣除 800 元。(1000 - 800 = 200)
- 第二天,客戶檢查了他的銀行余額,發(fā)現(xiàn)他的賬戶余額減少到了 200 元,因?yàn)榈诙€(gè)程序沒(méi)有意識(shí)到第一筆存款,并在存款完成前做了扣除操作。
這就是一個(gè)線程競(jìng)賽的例子,如果我們不小心落入這樣的代碼,我們的并發(fā)程序就會(huì)出現(xiàn)問(wèn)題。
互斥鎖和讀寫鎖
互斥鎖,英文名 Mutex,顧名思義,就是相互排斥,是保護(hù)程序中臨界區(qū)的一種方式。
而臨界區(qū)是程序中需要獨(dú)占訪問(wèn)共享資源的區(qū)域。互斥鎖提供了一種安全的方式來(lái)表示對(duì)這些共享資源的獨(dú)占訪問(wèn)。
為了使用資源,channel 通過(guò)通信共享內(nèi)存,而 Mutex 通過(guò)開(kāi)發(fā)人員的約定同步訪問(wèn)共享內(nèi)存。?
讓我們看一個(gè)沒(méi)有 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()
}
第一次結(jié)果為:491
第二次結(jié)果:493
[Running] go run "e:Coding WorkspacesLearningGoTheEasiestWayconcurrencymutexv0main.go"
493
在上面的例子中,我們聲明了一個(gè)名為 test 的計(jì)算結(jié)構(gòu)體,并通過(guò) for 循環(huán)產(chǎn)生了多個(gè) GoRoutines,將 sum 的值加 1。(如果你對(duì) GoRoutines 和 WaitGroup 不熟悉,請(qǐng)參考之前的教程)。 我們可能期望 for 循環(huán)后 sum 的值應(yīng)該是 500。然而,這可能不是真的。 有時(shí),您可能會(huì)得到小于 500(當(dāng)然永遠(yuǎn)不會(huì)超過(guò) 500)的結(jié)果。 這背后的原因是兩個(gè) GoRoutine 有一定的概率在相同的內(nèi)存位置操作相同的變量,從而導(dǎo)致這種意外結(jié)果。 這個(gè)問(wèn)題的解決方案是使用互斥鎖。
使用 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()
}
結(jié)果為:
[Running] go run "e:Coding WorkspacesLearningGoTheEasiestWayconcurrencymutexv0.1main.go"
500
在第二個(gè)示例中,我們?cè)诮Y(jié)構(gòu)中添加了一個(gè)互斥鎖屬性,它是一種類型的 sync.Mutex。然后我們使用互斥鎖的 Lock() 和 Unlock() 來(lái)保護(hù) test.sum 當(dāng)它被并發(fā)修改時(shí),即 test.sum++。
請(qǐng)記住,使用互斥鎖并非沒(méi)有后果,因?yàn)樗鼤?huì)影響應(yīng)用程序的性能,因此我們需要適當(dāng)有效地使用它。 如果你的 GoRoutines 只讀取共享數(shù)據(jù)而不寫入相同的數(shù)據(jù),那么競(jìng)爭(zhēng)條件就不會(huì)成為問(wèn)題。 在這種情況下,您可以使用 RWMutex 代替 Mutex 來(lái)提高性能時(shí)間。
Defer 關(guān)鍵字
對(duì) Unlock() 使用 defer 關(guān)鍵字通常是一個(gè)好習(xí)慣。
func dosomething(test *calculation) {
test.mutex.Lock()
defer test.mutex.Unlock()
err1 :=...
if err1 != nil {
return err1
}
err2 :=...
if err2 != nil {
return err2
}
// ... do more stuff ...
return nil
}
在這種情況下,我們有多個(gè) if err!=nil 這可能會(huì)導(dǎo)致函數(shù)提前退出。 通過(guò)使用 defer,無(wú)論函數(shù)如何返回,我們都可以保證釋放鎖。 否則,我們需要將 Unlock() 放在函數(shù)可能返回的每個(gè)地方。 然而,這并不意味著我們應(yīng)該一直使用 defer。 讓我們?cè)倏匆粋€(gè)例子。
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()
}
在這個(gè)例子中,mutex 不會(huì)釋放鎖,直到耗時(shí)的函數(shù)(這里是 http.Get())完成。 在這種情況下,我們可以在 test.sum=... 行之后解鎖互斥鎖,因?yàn)檫@是我們操作變量的唯一地方。
總結(jié)
很多時(shí)候 Mutex 并不是單獨(dú)使用的,而是嵌套在 Struct 中使用,作為結(jié)構(gòu)體的一部分,如果嵌入的 struct 有多個(gè)字段,我們一般會(huì)把 Mutex 放在要控制的字段上面,然后使用空格把字段分隔開(kāi)來(lái)。
甚至可以把獲取鎖、釋放鎖、計(jì)數(shù)加一的邏輯封裝成一個(gè)方法。
本文摘自 :https://blog.51cto.com/y