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

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

臨界區

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

x = x + 1

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

<!-- more -->工具

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

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

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

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

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

cs5

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

讓咱們再看看可能發生的不一樣狀況:blog

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 之間任何通訊。所以互斥鎖是一種天然的選擇。

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

相關文章
相關標籤/搜索