- 原文地址:Part 25: Mutex
- 原文做者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
在本教程中,咱們將瞭解互斥鎖Mutex
。咱們還將學習如何使用Mutex
和channel
解決競態條件。golang
在瞭解互斥鎖以前,先了解併發編程中臨界區的概念很是重要。當程序併發運行時,多個Goroutines
不該該同時擁有修改共享內存的權限。修改共享內存的這部分代碼則稱爲臨界區。例如,假設咱們有一段代碼將變量 x 遞增 1。編程
x = x + 1
複製代碼
若是上面一段代碼被一個Goroutine
訪問,就不會有任何問題。併發
讓咱們看看爲何當有多個Goroutines
併發運行時,這段代碼會失敗。爲簡單起見,咱們假設咱們有 2 個Goroutines
併發運行上面的代碼行。函數
上面的代碼將按如下步驟執行(有更多技術細節涉及寄存器,如何添加任務等等,但爲了本教程的簡便,咱們假設都是第三步),工具
- 獲取當前
x
的值- 計算
x + 1
- 把第二步計算的值賦給
x
當這三個步驟僅由一個Goroutine
進行時,結果沒什麼問題。學習
讓咱們看看當兩個Goroutines
併發運行此代碼時會發生什麼。下圖描繪了當兩個Goroutines
併發訪問代碼行x = x + 1
時可能發生的狀況。ui
咱們假設x
的初始值爲 0。Goroutine 1
獲取x
的初始值,計算x + 1
,在它將計算值賦值給x
以前,系統切換到Goroutine 2
。如今Goroutine 2
獲取的x
的值仍爲 0,而後計算x + 1
。此時系統再次切回到Goroutine 1
。如今Goroutine 1
將其計算值 1 賦值給x
,所以x
變爲 1。而後Goroutine 2
再次開始執行而後賦值計算值,而後把 1 賦值給x
,所以在兩個Goroutines
執行後x
爲 1。spa
如今讓咱們看看可能發生的不一樣狀況。code
在上面的場景中,Goroutine 1
開始執行並完成全部的三個步驟,所以x
的值變爲 1。而後Goroutine 2
開始執行。如今x
的值爲 1,當Goroutine 2
完成執行時,x
的值爲 2。cdn
所以,在這兩種狀況下,能夠看到x
的最終值爲 1 或者 2,具體取決於協程如何切換。這種程序的輸出取決於Goroutines
的執行順序的狀況,稱爲競態條件。
在上面的場景中,若是在任什麼時候間點只容許一個Goroutine
訪問臨界區,則能夠避免競態條件。這能夠經過使用 Mutex 實現。
Mutex
互斥鎖Mutex
用於提供鎖定機制,以確保在任什麼時候間點只有一個Goroutine
運行代碼的臨界區,以防止發生競態條件。
sync
包中提供了Mutex
。Mutex
上定義了兩種方法,即鎖定Lock
和UnLock
。在Lock
和UnLock
的調用之間的任何代碼將只能由一個Goroutine
執行,從而避免競態條件。
mutex.Lock()
x = x + 1
mutex.Unlock()
複製代碼
在上面的代碼中,x = x + 1
將僅由一個Goroutine
執行。
若是一個Goroutine
已經持有鎖,當一個新的Goroutine
試圖獲取鎖的時候,新的Goroutine
將被阻塞直到互斥鎖被解鎖。
在本節中,咱們將編寫一個有競態條件的程序,在接下來的部分中咱們將修復競態條件。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup) {
x = x + 1
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w)
}
w.Wait()
fmt.Println("final value of x", x)
}
複製代碼
在上面的程序中,第 7 行的increment
函數將x
的值遞增 1,而後調用WaitGroup
上的Done
以通知main Goroutine
任務完成。
咱們在第 15 行生成 1000 個increment Goroutines
。這些Goroutines
中的每個都併發運行,當多個Goroutines
嘗試同時訪問x
的值,而且計算x + 1
時會出現競態條件。
最好在本地運行此程序,由於playgroud
是肯定性的不會出現競態條件。在本地計算機上屢次運行此程序,您能夠看到因爲競態條件,每次輸出都會不一樣。我遇到的一些輸出是final value of x 941, final value of x 928, final value of x 922
等等。
Mutex
解決競態條件在上面的程序中,咱們生成了 1000 個Goroutines
。若是每一個都將x
的值加 1,則x
的最終指望值應該爲 1000。在本節中,咱們將使用互斥鎖 Mutex
修復上述程序中的競態條件。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m)
}
w.Wait()
fmt.Println("final value of x", x)
}
複製代碼
Mutex
是一種結構類型,咱們在第一行中初始化了一個Mutex
類型的變量m
。 在上面的程序中,咱們修改了increment
函數,使x = x + 1
的代碼在m.Lock()
和m.Unlock()
之間。如今這段代碼沒有任何競態條件,由於在任什麼時候候只容許一個Goroutine
執行臨界區。
如今若是運行該程序,它將輸出,
final value of x 1000
複製代碼
在第 18 行傳遞互斥鎖的地址很是重要。若是經過值傳遞互斥鎖而不是地址傳遞,則每一個Goroutine
都將擁有本身的互斥鎖副本,那麼確定還會發生競態條件。
channel
解決競態條件咱們也可使用channel
解決競態條件。讓咱們看看這是如何完成的。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true
x = x + 1
<- ch
}
func main() {
var w sync.WaitGroup
ch := make(chan bool, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, ch)
wg.Done()
}
w.Wait()
fmt.Println("final value of x", x)
}
複製代碼
在上面的程序中,咱們建立了一個容量爲 1 的緩衝channel
,並將其傳遞給第 18 行的increment Goroutine
。 此緩衝channel
經過將true
傳遞給ch
來實現確保只有一個Goroutine
訪問臨界區的。在x
遞增以前,因爲緩衝channel
的容量爲 1,所以嘗試寫入此channel
的全部其餘Goroutines
都會被阻塞,直到第 10 行將ch
的值取出來。 使用這種方式,實現了只容許一個 Goroutine 訪問臨界區。
程序輸出,
final value of x 1000
複製代碼
Mutex
VS 通道channel
咱們使用互斥鎖Mutex
和通道channel
解決了競態條件問題。那麼咱們怎麼決定什麼時候使用哪一個?答案在於您要解決的問題。若是您嘗試解決的問題更適合互斥鎖Mutex
,那麼請繼續使用Mutex
。若是須要,請不要猶豫地使用Mutex
。若是問題彷佛更適合通道channel
,那麼使用它:)。
大多數 Go 新手嘗試使用channel
解決遇到的併發問題,由於它是該語言的一個很酷的功能。這是錯的。該語言爲咱們提供了Mutex
或Channel
的選項,而且選擇任何一種都沒有錯。
通常狀況下,當Goroutine
須要相互通訊時使用channel
,當Goroutine
只訪問代碼的臨界區時,使用Mutex
。
在咱們上面那些問題的狀況下,我寧願使用Mutex
,由於這個問題不須要goroutines
之間進行任何通訊。所以Mutex
是一種天然的選擇。
個人建議是選擇工具去解決問題,而不要爲了工具去適應問題:)