Go 系列教程 —— 25. Mutex
Go 系列教程 —— 25. Mutex
這是一個建立於 2018-03-15 11:41:13 的文章,其中的資訊可能已經有所發展或是發生改變。
歡迎來到 Golang 系列教程的第 25 篇。
本教程我們學習 Mutex。我們還會學習怎樣通過 Mutex 和通道來處理競態條件(Race Condition)。
臨界區
在學習 Mutex 之前,我們需要理解併發程式設計中臨界區(Critical Section)的概念。當程式併發地執行時,多個 Go 協程不應該同時訪問那些修改共享資源的程式碼。這些修改共享資源的程式碼稱為臨界區。例如,假設我們有一段程式碼,將一個變數 x
自增 1。
x = x + 1
如果只有一個 Go 協程訪問上面的程式碼段,那都沒有任何問題。
但當有多個協程併發執行時,程式碼卻會出錯,讓我們看看究竟是為什麼吧。簡單起見,假設在一行程式碼的前面,我們已經執行了兩個 Go 協程。
在上一行程式碼的內部,系統執行程式時分為如下幾個步驟(這裡其實還有很多包括暫存器的技術細節,以及加法的工作原理等,但對於我們的系列教程,只需認為只有三個步驟就好了):
- 獲得 x 的當前值
- 計算 x + 1
- 將步驟 2 計算得到的值賦值給 x
如果只有一個協程執行上面的三個步驟,不會有問題。
我們討論一下當有兩個併發的協程執行該程式碼時,會發生什麼。下圖描述了當兩個協程併發地訪問程式碼行 x = x + 1
時,可能出現的一種情況。
我們假設 x
的初始值為 0。而協程 1 獲取 x
的初始值,並計算 x + 1
。而在協程 1 將計算值賦值給 x
之前,系統上下文切換到了協程 2。於是,協程 2 獲取了 x
的初始值(依然為 0),並計算 x + 1
。接著系統上下文又切換回了協程 1。現在,協程 1 將計算值 1 賦值給 x
,因此 x
等於 1。然後,協程 2 繼續開始執行,把計算值(依然是 1)複製給了 x
,因此在所有協程執行完畢之後,x
都等於 1。
現在我們考慮另外一種可能發生的情況。
在上面的情形裡,協程 1 開始執行,完成了三個步驟後結束,因此 x
的值等於 1。接著,開始執行協程 2。目前 x
的值等於 1。而當協程 2 執行完畢時,x
的值等於 2。
所以,從這兩個例子你可以發現,根據上下文切換的不同情形,x
的最終值是 1 或者 2。這種不太理想的情況稱為競態條件(Race Condition),其程式的輸出是由協程的執行順序決定的。
在上例中,如果在任意時刻只允許一個 Go 協程訪問臨界區,那麼就可以避免競態條件。而使用 Mutex 可以達到這個目的。
Mutex
Mutex 用於提供一種加鎖機制(Locking Mechanism),可確保在某時刻只有一個協程在臨界區執行,以防止出現競態條件。
Mutex 可以在 sync 包內找到。Mutex 定義了兩個方法:Lock 和 Unlock。所有在 Lock
和 Unlock
之間的程式碼,都只能由一個 Go 協程執行,於是就可以避免競態條件。
mutex.Lock()
x = x + 1
mutex.Unlock()
在上面的程式碼中,x = x + 1
只能由一個 Go 協程執行,因此避免了競態條件。
如果有一個 Go 協程已經持有了鎖(Lock),當其他協程試圖獲得該鎖時,這些協程會被阻塞,直到 Mutex 解除鎖定為止。
含有競態條件的程式
在本節裡,我們會編寫一個含有競態條件的程式,而在接下來一節,我們再修復競態條件的問題。
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()
,通知該函式已結束。
在上述程式的第 15 行,我們生成了 1000 個 increment
協程。每個 Go 協程併發地執行,由於第 8 行試圖增加 x
的值,因此多個併發的協程試圖訪問 x
的值,這時就會發生競態條件。
由於 playground 具有確定性,競態條件不會在 playground 發生,請在你的本地執行該程式。請在你的本地機器上多執行幾次,可以發現由於競態條件,每一次輸出都不同。我其中遇到的幾次輸出有 final value of x 941
、final value of x 928
、final value of x 922
等。
使用 Mutex
在前面的程式裡,我們建立了 1000 個 Go 協程。如果每個協程對 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 是一個結構體型別,我們在第 15 行建立了 Mutex
型別的變數 m
,其值為零值。在上述程式裡,我們修改了 increment
函式,將增加 x
的程式碼(x = x + 1
)放置在 m.Lock()
和 m.Unlock()
之間。現在這段程式碼不存在競態條件了,因為任何時刻都只允許一個協程執行這段程式碼。
於是如果執行該程式,會輸出:
final value of x 1000
在第 18 行,傳遞 Mutex 的地址很重要。如果傳遞的是 Mutex 的值,而非地址,那麼每個協程都會得到 Mutex 的一份拷貝,競態條件還是會發生。
使用通道處理競態條件
我們還能用通道來處理競態條件。看看是怎麼做的。
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 的緩衝通道,並在第 18 行將它傳入 increment
協程。該緩衝通道用於保證只有一個協程訪問增加 x
的臨界區。具體的實現方法是在 x
增加之前(第 8 行),傳入 true
給緩衝通道。由於緩衝通道的容量為 1,所以任何其他協程試圖寫入該通道時,都會發生阻塞,直到 x
增加後,通道的值才會被讀取(第 10 行)。實際上這就保證了只允許一個協程訪問臨界區。
該程式也輸出:
final value of x 1000
Mutex vs 通道
通過使用 Mutex 和通道,我們已經解決了競態條件的問題。那麼我們該選擇使用哪一個?答案取決於你想要解決的問題。如果你想要解決的問題更適用於 Mutex,那麼就用 Mutex。如果需要使用 Mutex,無須猶豫。而如果該問題更適用於通道,那就使用通道。:)
由於通道是 Go 語言很酷的特性,大多數 Go 新手處理每個併發問題時,使用的都是通道。這是不對的。Go 給了你選擇 Mutex 和通道的餘地,選擇其中之一都可以是正確的。
總體說來,當 Go 協程需要與其他協程通訊時,可以使用通道。而當只允許一個協程訪問臨界區時,可以使用 Mutex。
就我們上面解決的問題而言,我更傾向於使用 Mutex,因為該問題並不需要協程間的通訊。所以 Mutex 是很自然的選擇。
我的建議是去選擇針對問題的工具,而別讓問題去將就工具。:)
本教程到此結束。祝你愉快
相關文章
- 當 Go struct 遇上 MutexGoStructMutex
- Bootstrap系列 -- 25. 下拉選單分割線boot
- Go語言的互斥鎖MutexGoMutex
- 【C++11多執行緒入門教程】系列之互斥量mutexC++執行緒Mutex
- Go 併發程式設計之 MutexGo程式設計Mutex
- Go死鎖——當Channel遇上Mutex時GoMutex
- Go併發程式設計--Mutex/RWMutexGo程式設計Mutex
- Go 互斥鎖 Mutex 原始碼分析(二)GoMutex原始碼
- Go 標準庫 —— sync.Mutex 互斥鎖GoMutex
- go中sync.Mutex原始碼解讀GoMutex原始碼
- 【Go進階—併發程式設計】MutexGo程式設計Mutex
- Go 中的鎖原始碼實現:MutexGo原始碼Mutex
- GO: sync.Mutex 的實現與演進GoMutex
- golang pprof監控系列(2) —— memory,block,mutex 使用GolangBloCMutex
- 解密prompt系列25. RLHF改良方案之樣本標註:RLAIF & SALMON解密AI
- 這可能是最容易理解的 Go Mutex 原始碼剖析GoMutex原始碼
- Go語言中的互斥鎖和讀寫鎖(Mutex和RWMutex)GoMutex
- 25. 深淺複製
- golang pprof 監控系列(3) —— memory,block,mutex 統計原理GolangBloCMutex
- Go語言入門教程系列——函式、迴圈與分支Go函式
- Go 高效能系列教程之一:基準測試Go
- Go 高效能系列教程:讀懂 pprof 生成的報告Go
- oracle mutexOracleMutex
- Go 高效能系列教程之二:效能評估和分析Go
- Go 高效能系列教程之五:記憶體和垃圾回收Go記憶體
- Go 高效能系列教程之三:編譯器優化Go編譯優化
- [系列] Go - chan 通道Go
- Go系列之反射Go反射
- 25. Socket與粘包問題
- 體驗mutexMutex
- go泛型教程Go泛型
- Go 語言併發程式設計之互斥鎖詳解 sync.MutexGo程式設計Mutex
- go微服務系列(一) go micro入門Go微服務
- PHP轉Go系列:字串PHPGo字串
- react系列教程React
- Hudson教程系列
- Go 高效能系列教程之六:一些建議和實踐Go
- 25. 使用MySQL之使用觸發器MySql觸發器