[譯] part25: golang Mutex互斥鎖

咔嘰咔嘰發表於2019-04-04

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

臨界區

在瞭解互斥鎖之前,先了解併發程式設計中臨界區的概念非常重要。當程式併發執行時,多個Goroutines不應該同時擁有修改共享記憶體的許可權。修改共享記憶體的這部分程式碼則稱為臨界區。例如,假設我們有一段程式碼將變數 x 遞增 1。

x = x + 1  
複製程式碼

如果上面一段程式碼被一個Goroutine訪問,就不會有任何問題。

讓我們看看為什麼當有多個Goroutines併發執行時,這段程式碼會失敗。為簡單起見,我們假設我們有 2 個Goroutines併發執行上面的程式碼行。

上面的程式碼將按以下步驟執行(有更多技術細節涉及暫存器,如何新增任務等等,但為了本教程的簡便,我們假設都是第三步),

  1. 獲取當前x的值
  2. 計算 x + 1
  3. 把第二步計算的值賦給x

當這三個步驟僅由一個Goroutine進行時,結果沒什麼問題。

讓我們看看當兩個Goroutines併發執行此程式碼時會發生什麼。下圖描繪了當兩個Goroutines併發訪問程式碼行x = x + 1時可能發生的情況。

[譯] part25: golang Mutex互斥鎖

我們假設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。

現在讓我們看看可能發生的不同情況。

[譯] part25: golang Mutex互斥鎖

在上面的場景中,Goroutine 1開始執行並完成所有的三個步驟,因此x的值變為 1。然後Goroutine 2開始執行。現在x的值為 1,當Goroutine 2完成執行時,x的值為 2。

因此,在這兩種情況下,可以看到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是一種自然的選擇。

我的建議是選擇工具去解決問題,而不要為了工具去適應問題:)

相關文章