[Go 教程系列筆記] Mutex(互斥鎖)

在本教程中,我們將瞭解互斥鎖。我們還將學習如何使用互斥鎖和通道解決競爭問題。

臨界區

在講互斥鎖之前,瞭解併發編程中臨界區的概念非常重要。當程序同時運行時,多個 goroutine 同時訪問修改共享資源,修改共享資源的這段代碼稱爲臨界區。例如,假設我們要將變量 x 遞增1.

x = x + 1

只要上面的代碼被一個 goroutine 訪問,就不會有任何問題。

<!-- more -->

讓我們看看爲什麼當有多個 goroutine 同時運行時,這段代碼會失敗。爲簡單起見,我們假設有2個 goroutine 同時運行上面的代碼行。

在內部,上面的代碼行將由系統按下面的步驟執行。

  1. 獲取 x 的當前值
  2. 計算 x+1
  3. 將步驟2中計算的值分配給 x

當這三個步驟僅由一個 goroutine 進行時,一切都很順利。

讓我們討論當2個 goroutine 同時運行此代碼會發生什麼。下圖描繪了兩個 goroutine 同時訪問代碼行時可能發生的情況。

cs5

圖中,第一步協程1當前x值是0,計算x+1,然後系統切換上下文到協程2,第二步,協程2當前x值是0,計算x+1,這時系統又切換上下文到協程1,進行分配x值,然後又切換上下文到協程2,進行分配x值,最後,x的值還是1.

讓我們再看看可能發生的不同情況:

cs-6

在上面的場景中,協程1開始執行並完成3個步驟,這時x值是1,然後開始執行協程2,現在x的值已經是1了,在協程2執行完成,x的值就是2了。

因此,在這兩種情況下,你可以看到 x 的最終值是1或2取決於上下文切換的方式。這種類型的不良情況,其中程序的輸出取決於 goroutine 的執行順序,稱爲競爭條件

爲了避免競爭條件,可以通過使用 Mutex 實現。

Mutex 互斥

Mutex 用於提供鎖定機制,以確保在任何時間點只有一個 goroutine 在臨界區運行,已防止發生競爭條件。

sync 包中提供了 Mutex。Mutex 定義了兩個方法,即 LockUnlock,在 LockUnlock 之間將僅由一個 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 941final value of x 928final 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 + 1m.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 之間任何通信。因此互斥鎖是一種自然的選擇。

我的建議是根據問題選擇工具,不要試圖讓問題適應工具。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章