[譯] part25: golang Mutex 互斥鎖

在本教程中,咱們將瞭解互斥鎖Mutex。咱們還將學習如何使用Mutexchannel解決競態條件。golang

臨界區

在瞭解互斥鎖以前,先了解併發編程中臨界區的概念很是重要。當程序併發運行時,多個Goroutines不該該同時擁有修改共享內存的權限。修改共享內存的這部分代碼則稱爲臨界區。例如,假設咱們有一段代碼將變量 x 遞增 1。編程

x = x + 1  
複製代碼

若是上面一段代碼被一個Goroutine訪問,就不會有任何問題。併發

讓咱們看看爲何當有多個Goroutines併發運行時,這段代碼會失敗。爲簡單起見,咱們假設咱們有 2 個Goroutines併發運行上面的代碼行。函數

上面的代碼將按如下步驟執行(有更多技術細節涉及寄存器,如何添加任務等等,但爲了本教程的簡便,咱們假設都是第三步),工具

  1. 獲取當前x的值
  2. 計算 x + 1
  3. 把第二步計算的值賦給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包中提供了MutexMutex上定義了兩種方法,即鎖定LockUnLock。在LockUnLock的調用之間的任何代碼將只能由一個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)
}
複製代碼

Run in playgroud

在上面的程序中,第 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)
}
複製代碼

Run in playgroud

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)
}
複製代碼

Run in playgroud

在上面的程序中,咱們建立了一個容量爲 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解決遇到的併發問題,由於它是該語言的一個很酷的功能。這是錯的。該語言爲咱們提供了MutexChannel的選項,而且選擇任何一種都沒有錯。

通常狀況下,當Goroutine須要相互通訊時使用channel,當Goroutine只訪問代碼的臨界區時,使用Mutex

在咱們上面那些問題的狀況下,我寧願使用Mutex,由於這個問題不須要goroutines之間進行任何通訊。所以Mutex是一種天然的選擇。

個人建議是選擇工具去解決問題,而不要爲了工具去適應問題:)

相關文章
相關標籤/搜索