當(dāng)前位置:首頁 > IT技術(shù) > 編程語言 > 正文

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

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

互斥是并發(fā)編程中最關(guān)鍵的概念之一。當(dāng)我們使用 goruntine 和channels 進(jìn)行并發(fā)編程時(shí),如果兩個(gè)?goruntine 嘗試同時(shí)訪問同一個(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)單。我們可以通過少量的單元測(cè)試有效地保證它每次都能按計(jì)劃工作。

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

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

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

互斥鎖和讀寫鎖

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

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

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

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

讓我們看一個(gè)沒有 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

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

第二次結(jié)果:493

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


在上面的例子中,我們聲明了一個(gè)名為 test 的計(jì)算結(jié)構(gòu)體,并通過 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ì)超過 500)的結(jié)果。 這背后的原因是兩個(gè) GoRoutine 有一定的概率在相同的內(nèi)存位置操作相同的變量,從而導(dǎo)致這種意外結(jié)果。 這個(gè)問題的解決方案是使用互斥鎖。


使用 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() 來保護(hù) test.sum 當(dāng)它被并發(fā)修改時(shí),即 test.sum++。

請(qǐng)記住,使用互斥鎖并非沒有后果,因?yàn)樗鼤?huì)影響應(yīng)用程序的性能,因此我們需要適當(dāng)有效地使用它。 如果你的 GoRoutines 只讀取共享數(shù)據(jù)而不寫入相同的數(shù)據(jù),那么競(jìng)爭(zhēng)條件就不會(huì)成為問題。 在這種情況下,您可以使用 RWMutex 代替 Mutex 來提高性能時(shí)間。


Defer 關(guān)鍵字

對(duì) Unlock() 使用 defer 關(guān)鍵字通常是一個(gè)好習(xí)慣。

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
}

在這種情況下,我們有多個(gè) if err!=nil 這可能會(huì)導(dǎo)致函數(shù)提前退出。 通過使用 defer,無論函數(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 放在要控制的字段上面,然后使用空格把字段分隔開來。

甚至可以把獲取鎖、釋放鎖、計(jì)數(shù)加一的邏輯封裝成一個(gè)方法。

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

開通會(huì)員,享受整站包年服務(wù)立即開通 >