一文掌握Go語言並發編程必備的Mutex互斥鎖

在並發編程中,我們需要處理多個線程同時對共享資源的訪問問題。如果不加控制地同時訪問共享資源,就會導致競爭條件(Race Condition)問題,從而導致程序出現不可預知的錯誤。為瞭解決這個問題,Go 語言提供瞭 sync 包,其中包括 Mutex 互斥鎖、RWMutex 讀寫鎖等同步機制,本篇博客將著重介紹 Mutex 互斥鎖的基本原理。

1. Mutex 互斥鎖的基本概念

Mutex 是 Mutual Exclusion(互斥)的縮寫,用於保護共享資源。當一個goroutine 獲取瞭 Mutex 的鎖之後,其他的 goroutine 就無法再獲取到這個 Mutex 的鎖,直到這個 goroutine 釋放瞭這個 Mutex 的鎖,其他 goroutine 才能繼續嘗試獲取這個 Mutex 的鎖。

Mutex 互斥鎖包含兩個狀態:鎖定和未鎖定。當一個 goroutine 獲取瞭 Mutex 的鎖,Mutex 就處於鎖定狀態,其他的 goroutine 就隻能等待這個 goroutine 釋放這個 Mutex 的鎖,才能再次嘗試獲取這個 Mutex 的鎖。當一個 goroutine 釋放瞭 Mutex 的鎖,Mutex 就處於未鎖定狀態,此時其他的 goroutine 可以獲取這個 Mutex 的鎖。

Mutex 互斥鎖使用的是二進制信號量的概念,當 Mutex 處於鎖定狀態時,就相當於信號量為 0,其他 goroutine 隻能等待;當 Mutex 處於未鎖定狀態時,就相當於信號量為 1,其他 goroutine 可以嘗試獲取這個 Mutex 的鎖。

2. Mutex 互斥鎖的基本用法

Mutex 互斥鎖的基本用法如下:

package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
func main() {
    var wg sync.WaitGroup
    var mu sync.Mutex
    var count int
​
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            mu.Lock()
            defer mu.Unlock()
            count++
            fmt.Println(count)
            time.Sleep(time.Millisecond)
            wg.Done()
        }()
    }
​
    wg.Wait()
}

在上面的例子中,我們創建瞭 10 個 goroutine,每個 goroutine 都會獲取 Mutex 的鎖,對共享變量 count 進行自增操作,並打印 count 的值。在獲取 Mutex 的鎖後,我們使用 defer 語句來保證在 goroutine 結束時釋放 Mutex 的鎖。最後,我們使用 WaitGroup 來等待所有 goroutine 結束。

由於 Mutex 互斥鎖是排他性的,因此在同一時刻隻有一個 goroutine 可以獲取 Mutex 的鎖,其他的 goroutine 隻能等待。在上面的例子中,我們通過 Mutex 互斥鎖來保證瞭 count 的原子性操作,從而避免瞭競爭條件問題。

3. Mutex 互斥鎖的底層實現

Mutex 互斥鎖的底層實現使用瞭操作系統提供的原語(Primitive),如互斥量(Mutex)、臨界區(Critical Section)等。在不同的操作系統中,Mutex 互斥鎖的底層實現可能有所不同。

在 Linux 系統中,Mutex 互斥鎖的底層實現主要使用瞭 futex(Fast User-space Mutex)機制。futex 是 Linux 系統提供的一種快速用戶空間互斥量機制,可以實現用戶空間的原子操作。

Mutex 互斥鎖的底層實現主要包括兩個部分:等待隊列和鎖狀態。

3.1 等待隊列

當一個 goroutine 嘗試獲取 Mutex 的鎖時,如果 Mutex 已經被其他 goroutine 獲取,那麼這個 goroutine 就會進入等待隊列中等待。等待隊列是一個鏈表,每個節點代表一個等待 goroutine。

當一個 goroutine 釋放 Mutex 的鎖時,會喚醒等待隊列中的第一個 goroutine。喚醒的操作主要包括兩個步驟:

  • 將等待隊列中的第一個節點從鏈表中移除,並將其狀態設置為可運行(Runnable)狀態。
  • 將移除的節點中的 goroutine 添加到調度器的可運行隊列中,等待調度器將其調度執行。

3.2 鎖狀態

Mutex 互斥鎖的鎖狀態主要包括兩個部分:互斥標志和持有者標志。

互斥標志表示 Mutex 的狀態,0 表示未鎖定,1 表示鎖定。互斥標志的原子操作主要使用瞭 Compare-and-Swap(CAS)指令。

持有者標志表示當前持有 Mutex 的 goroutine 的 ID。如果 Mutex 未被任何 goroutine 持有,那麼持有者標志為 0。持有者標志的原子操作主要使用瞭 Load Linked(LL)和 Store Conditional(SC)指令。

當一個 goroutine 嘗試獲取 Mutex 的鎖時,會先嘗試使用 CAS 指令將互斥標志從 0 改為 1。如果 CAS 指令成功,那麼這個 goroutine 就獲得瞭 Mutex 的鎖,並將持有者標志設置為當前 goroutine 的 ID。如果 CAS 指令失敗,那麼說明 Mutex 已經被其他 goroutine 獲取,這個 goroutine 就會進入等待隊列中等待。

當一個 goroutine 釋放 Mutex 的鎖時,會先將持有者標志設置為 0,然後再使用 LL 和 SC 指令將互斥標志從 1 改為 0。LL 指令用於加載互斥標志的值,SC 指令用於將互斥標志的值改為 0。LL 和 SC 指令是原子指令,可以保證操作的原子性。

4. Mutex 互斥鎖的註意事項

在使用 Mutex 互斥鎖時,需要註意以下幾點:

4.1 不要將 Mutex 作為函數或方法的參數傳遞

Mutex 是一個結構體類型,包含互斥標志和持有者標志等字段。當將 Mutex 作為函數或方法的參數傳遞時,會將 Mutex 的副本傳遞給函數或方法,而不是原始的 Mutex 實例。這樣做會導致不同的 goroutine 使用不同的 Mutex 實例,從而無法實現互斥。

正確的做法是將 Mutex 定義為一個全局變量,並在多個 goroutine 中共享這個全局變量。

4.2 不要在獲取 Mutex 的鎖時阻塞太久

當一個 goroutine 嘗試獲取 Mutex 的鎖時,如果 Mutex 已經被其他 goroutine 獲取,那麼這個 goroutine 就會進入等待隊列中等待。如果等待時間過長,會導致性能下降。

可以使用 TryLock() 方法嘗試獲取 Mutex 的鎖。TryLock() 方法會立即返回,如果獲取鎖成功返回 true,否則返回 false。

4.3 不要重復釋放 Mutex 的鎖

當一個 goroutine 釋放 Mutex 的鎖時,如果這個 goroutine 不是 Mutex 的持有者,那麼會導致 panic 異常。因此,在釋放 Mutex 的鎖時,需要確保當前 goroutine 是 Mutex 的持有者。

可以使用 defer 語句在獲取 Mutex 的鎖時自動註冊釋放鎖的操作,以確保在任何情況下都能正確釋放 Mutex 的鎖。

4.4 不要在鎖內部執行阻塞或耗時操作

當一個 goroutine 持有 Mutex 的鎖時,其他 goroutine 無法獲取 Mutex 的鎖,從而會導致阻塞。如果在 Mutex 的鎖內部執行阻塞或耗時操作,會導致其他 goroutine 長時間等待,從而影響性能。

可以將阻塞或耗時操作放到 Mutex 的鎖外部執行,以避免阻塞其他 goroutine。

5. 總結

本文介紹瞭 Go 語言中的 Mutex 互斥鎖,包括 Mutex 的基本用法、互斥鎖的底層實現和註意事項。Mutex 是 Go 語言中實現互斥的重要工具,可以保證多個 goroutine 之間的數據訪問安全。

在使用 Mutex 時,需要註意避免一些常見的錯誤,如將 Mutex 作為函數或方法的參數傳遞、在獲取 Mutex 的鎖時阻塞太久、重復釋放 Mutex 的鎖、在鎖內部執行阻塞或耗時操作等。

除瞭 Mutex 互斥鎖,Go 語言還提供瞭其他類型的鎖,如讀寫鎖(sync.RWMutex)、條件變量(sync.Cond)等,可以根據不同的場景選擇不同類型的鎖。

最後,希望你能夠通過本文對 Mutex 互斥鎖有一個更深的理解。

到此這篇關於一文掌握Go語言並發編程必備的Mutex互斥鎖的文章就介紹到這瞭,更多相關Go語言 Mutex互斥鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: