Go 語言入門很簡單 -- 16. Go 并發互斥鎖 #私藏項目實操分享#
互斥是并發編程中最關鍵的概念之一。當我們使用 goruntine 和channels 進行并發編程時,如果兩個?goruntine 嘗試同時訪問同一個內存位置的同一數據會發生競爭,有時候會產生意想不到的結果,通常很難調試,不符合日常要求,出現錯誤甚至很難修復。

生活場景
假設在生活中可能會發生的例子:有一個銀行系統,我們可以從銀行余額中存款和取款。在一個單線程的同步程序中,這個操作很簡單。我們可以通過少量的單元測試有效地保證它每次都能按計劃工作。
然而,如果我們開始引入多個線程,在 Go?語言中使用多個 goroutine,我們可能會開始在我們的代碼中看到問題。
假如有一個余額為 1000 元的客戶。
客戶將 500 元存入他的賬戶。
一個 goroutine 會看到這個交易,讀取價值為 1000 ,并繼續將 500 添加到現有的余額中。(此時應該是 1500 的余額)
然而,在同一時刻,他拿 800 元來還分期付款的 iphone 13.
第二個程序在第一個程序能夠增加 500 元的額外存款之前,讀取了 1000 元的賬戶余額,并繼續從他的賬戶中扣除 800 元。(1000 - 800 = 200)
第二天,客戶檢查了他的銀行余額,發現他的賬戶余額減少到了 200 元,因為第二個程序沒有意識到第一筆存款,并在存款完成前做了扣除操作。
這就是一個線程競賽的例子,如果我們不小心落入這樣的代碼,我們的并發程序就會出現問題。
互斥鎖和讀寫鎖
互斥鎖,英文名 Mutex,顧名思義,就是相互排斥,是保護程序中臨界區的一種方式。
而臨界區是程序中需要獨占訪問共享資源的區域。互斥鎖提供了一種安全的方式來表示對這些共享資源的獨占訪問。
為了使用資源,channel 通過通信共享內存,而 Mutex 通過開發人員的約定同步訪問共享內存。
讓我們看一個沒有 Mutex 的并發編程示例
package mainimport ( "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 Workspaces\LearningGoTheEasiestWay\concurrency\mutex\v0\main.go"493
在上面的例子中,我們聲明了一個名為 test 的計算結構體,并通過 for 循環產生了多個 GoRoutines,將 sum 的值加 1。(如果你對 GoRoutines 和 WaitGroup 不熟悉,請參考之前的教程)。 我們可能期望 for 循環后 sum 的值應該是 500。然而,這可能不是真的。 有時,您可能會得到小于 500(當然永遠不會超過 500)的結果。 這背后的原因是兩個 GoRoutine 有一定的概率在相同的內存位置操作相同的變量,從而導致這種意外結果。 這個問題的解決方案是使用互斥鎖。
使用 Mutex
package mainimport ( "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 Workspaces\LearningGoTheEasiestWay\concurrency\mutex\v0.1\main.go"500
在第二個示例中,我們在結構中添加了一個互斥鎖屬性,它是一種類型的 sync.Mutex。然后我們使用互斥鎖的 Lock() 和 Unlock() 來保護 test.sum 當它被并發修改時,即 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 放在要控制的字段上面,然后使用空格把字段分隔開來。
甚至可以把獲取鎖、釋放鎖、計數加一的邏輯封裝成一個方法。
Go
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。