Go 併發程式設計之 Mutex

Meng小羽發表於2020-11-15

友情提示:此篇文章大約需要閱讀 5分鐘45秒,不足之處請多指教,感謝你的閱讀。 訂閱本站

我們比較常見的大型專案的設計中都會出現併發訪問問題,併發就是為了解決資料的準確性,保證同一個臨界區的資料只能被一個執行緒進行操作,日常中使用到的併發場景也是很多的:

  • 計數器:計數器結果不準確;
  • 秒殺系統:由於同一時間訪問量比較大,導致的超賣;
  • 使用者賬戶異常:同一時間支付導致的賬戶透支;
  • buffer 資料異常:更新 buffer 導致的資料混亂。

上面都是併發帶來的資料準確性的問題,決絕方案就是使用互斥鎖,也就是今天併發程式設計中的所要描述的 Mutex 併發原語。

實現機制

互斥鎖 Mutex 就是為了避免併發競爭建立的併發控制機制,其中有個“臨界區”的概念。

在併發程式設計過程中,如果程式中一部分資源或者變數會被併發訪問或者修改,為了避免併發訪問導致資料的不準確,這部分程式需要率先被保護起來,之後操作,操作結束後去除保護,這部分被保護的程式就叫做臨界區

使用互斥鎖,限定臨界區只能同時由一個執行緒持有,若是臨界區此時被一個執行緒持有,那麼其他執行緒想進入到這個臨界區的時候,就會失敗或者等待釋放鎖,持有此臨界區的執行緒退出,其他執行緒才有機會獲得這個臨界區。

go mutex 臨界區示意圖

Mutex 是 Go 語言中使用最廣泛的同步原語,也稱為併發原語,解決的是併發讀寫共享資源,避免出現資料競爭 data race 問題

基本使用

互斥鎖 Mutex 提供了兩個方法 Lock 和 Unlock:進入到臨界區使用 Lock 方法加鎖,退出臨界區使用 Unlock 方法釋放鎖 ?。

type Locker interface {
    Lock()
    Unlock()
}

func(m *Mutex)Lock()
func(m *Mutex)Unlock()

當一個 goroutine 呼叫 Lock 方法獲取到鎖後,其他 goroutine 會阻塞在 Lock 的呼叫上,直到當前獲取到鎖的 goroutine 釋放鎖。

接下來是一個計數器的例子,是由 100 個 goroutine 對計數器進行累加操作,最後輸出結果:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex
    countNum := 0

    // 確認輔助變數是否都執行完成
    var wg sync.WaitGroup

    // wg 新增數目要和 建立的協程數量保持一致
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                mu.Lock()
                countNum++
                mu.Unlock()
            }
        }()
    }
    wg.Wait()
    fmt.Printf("countNum: %d", countNum)
}

實際使用

很多時候 Mutex 並不是單獨使用的,而是巢狀在 Struct 中使用,作為結構體的一部分,如果嵌入的 struct 有多個欄位,我們一般會把 Mutex 放在要控制的欄位上面,然後使用空格把欄位分隔開來。

甚至可以把獲取鎖、釋放鎖、計數加一的邏輯封裝成一個方法。

package main
import (
    "fmt"
    "sync"
)

// 執行緒安全的計數器
type Counter struct {
    CounterType int
    Name        string

    mu    sync.Mutex
    count uint64
}

// 加一方法
func (c *Counter) Incr() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

// 取數值方法 執行緒也需要受保護
func (c *Counter) Count() uint64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

func main() {
    // 定義一個計數器
    var counter Counter

    var wg sync.WaitGroup
    wg.Add(100)

    for i := 0; i < 100; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                counter.Incr()
            }
        }()
    }
    wg.Wait()

    fmt.Printf("%d\n", counter.Count())
}

思考問題

Q:你已經知道,如果 Mutex 已經被一個 goroutine 獲取了鎖,其它等待中的 goroutine 們只能一直等待。那麼,等這個鎖釋放後,等待中的 goroutine 中哪一個會優先獲取 Mutex 呢?

A:FIFO,先來先服務的策略,Go 的 goroutine 排程中,會維護一個保障 goroutine 執行的佇列,當獲取到鎖的 goroutine 執行完臨界區的操作的時候,就會釋放鎖,在佇列中排在第一位置的 goroutine 會拿到鎖進行臨界區的操作。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Debug客棧

相關文章