- Go 教程系列筆記 Interface 第一部分
- Go 教程系列筆記 Interface 第二部分
- Go 教程系列筆記 併發介紹
- Go 教程系列筆記 goroutine(協程)
- Go 教程系列筆記 Channel 通道
- Go 教程系列筆記 緩衝通道和工作池
- Go 教程系列筆記 Select
- Go 教程系列筆記 Mutex(互斥鎖)
- Go 教程系列筆記 結構而不是類-Go中的OOP
- Go 教程系列筆記 組合而不是繼承-Go 中的 OOP
- Go 教程系列筆記 多態-Go 中的 OOP
在本教程中,我們將瞭解互斥鎖。我們還將學習如何使用互斥鎖和通道解決競爭問題。
臨界區
在講互斥鎖之前,瞭解併發編程中臨界區的概念非常重要。當程序同時運行時,多個 goroutine 同時訪問修改共享資源,修改共享資源的這段代碼稱爲臨界區。例如,假設我們要將變量 x 遞增1.
x = x + 1
只要上面的代碼被一個 goroutine 訪問,就不會有任何問題。
<!-- more -->
讓我們看看爲什麼當有多個 goroutine 同時運行時,這段代碼會失敗。爲簡單起見,我們假設有2個 goroutine 同時運行上面的代碼行。
在內部,上面的代碼行將由系統按下面的步驟執行。
- 獲取 x 的當前值
- 計算 x+1
- 將步驟2中計算的值分配給 x
當這三個步驟僅由一個 goroutine 進行時,一切都很順利。
讓我們討論當2個 goroutine 同時運行此代碼會發生什麼。下圖描繪了兩個 goroutine 同時訪問代碼行時可能發生的情況。
圖中,第一步協程1當前x值是0,計算x+1
,然後系統切換上下文到協程2,第二步,協程2當前x值是0,計算x+1
,這時系統又切換上下文到協程1,進行分配x值,然後又切換上下文到協程2,進行分配x值,最後,x的值還是1.
讓我們再看看可能發生的不同情況:
在上面的場景中,協程1開始執行並完成3個步驟,這時x值是1,然後開始執行協程2,現在x的值已經是1了,在協程2執行完成,x的值就是2了。
因此,在這兩種情況下,你可以看到 x 的最終值是1或2取決於上下文切換的方式。這種類型的不良情況,其中程序的輸出取決於 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 已經 Lock
,如果一個新的 goroutine 試圖 Lock
,新的 goroutine 將會阻塞,直到 Mutex Unlock.
有競爭條件的程序
我們將編寫一個具有競爭條件的程序,在接下來的部分中我們將修復競爭條件。
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)
}
請在本地運行此程序,因爲操作是確定性的,操作上不會出現比賽條件。在本地計算機上多次運行此程序,您可以看到由於競爭條件,每次輸出都會有所不同。其中一些我所遇到的產出是final value of x 941
,final value of x 928
,final value of x 922
等。
使用互斥鎖解決競爭條件
在上面的程序中,我們產生了1000個Goroutines。如果每個都將x的值遞增1,則x的最終期望值應爲1000.在本節中,我們將使用互斥鎖修復上述程序中的競爭條件。
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是一個結構類型,在第 15 行我們創建了一個零值的變量m的Mutex類型。在上面的程序中,我們更改了increment
函數,以便增加x的代碼x = x + 1
在m.Lock()
和m.Unlock()
之間。現在這段代碼沒有任何競爭條件,因爲在任何時候只允許一個Goroutine執行這段代碼。
使用 channel 解決競爭條件
我們也可以使用通道解決競爭條件。讓我們看看如何實現的。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true
x = x + 1
<- ch
wg.Done()
}
func main() {
var w sync.WaitGroup
ch := make(chan bool, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, ch)
}
w.Wait()
fmt.Println("final value of x", x)
}
在上面的程序中,我們創建了一個緩衝容量1的通道,並將其傳遞給increment
的Goroutine。此緩衝通道用於確保只有一個Goroutine訪問增加x的代碼的關鍵部分。這是通過傳遞true到第 8行號中的緩衝通道來完成的,然後 x增加。由於緩衝通道的容量爲1,所有其他嘗試寫入此通道的Goroutines都會被阻塞,直到在第9行增加x後從該通道讀取該值。實際上,這隻允許一個Goroutine訪問臨界區。
這個程序也打印
final value of x 1000
Mutex vs Channel
我們使用互斥鎖和通道解決了競爭條件問題。那麼我們如何決定何時使用呢?答案在於你要解決的問題。如果你解決的問題更合適互斥鎖,那麼請繼續使用互斥鎖。如果需要,請不要猶豫使用互斥鎖。如果問題更適合通道,那麼使用它:)(沒有銀彈)
大多數 Go 新手嘗試使用通道解決每個併發問題,因爲它是該語言的一個很酷的功能。這是錯誤的,語言爲我們提供了使用 Mutex 和 Channel 的選擇,並且選擇任何一種都沒有錯。
一般情況下,當 goroutine 需要互相通信時使用通道,當只有一個 goroutine 應該訪問代碼的臨界區時使用互斥。
在我們上面的問題情況下,我寧願使用互斥鎖,因爲這個問題不需要 goroutine 之間任何通信。因此互斥鎖是一種自然的選擇。
我的建議是根據問題選擇工具,不要試圖讓問題適應工具。