詳解Golang中的Mutex并發(fā)原語
前言
Go 語言以 高并發(fā) 著稱,其并發(fā)操作是重要特性之一。雖然并發(fā)可以提高程序性能和效率,但同時也可能帶來 競態(tài)條件 和 死鎖 等問題。為了避免這些問題,Go 提供了許多 并發(fā)原語,例如 Mutex、RWMutex、WaitGroup、Channel 等,用于實現(xiàn)同步、協(xié)調(diào)和通信等操作。
本文將著重介紹 Go 的 Mutex 并發(fā)原語,它是一種鎖類型,用于實現(xiàn)共享資源互斥訪問。
說明:本文使用的代碼基于的 Go 版本:1.20.1
Mutex
基本概念
Mutex 是 Go 語言中互斥鎖的實現(xiàn),它是一種同步機制,用于控制多個 goroutine 之間的并發(fā)訪問。當多個 goroutine 嘗試同時訪問同一個共享資源時,可能會導致數(shù)據(jù)競爭和其他并發(fā)問題,因此需要使用互斥鎖來協(xié)調(diào)它們之間的訪問。

在上述圖片中,我們可以將綠色部分看作是臨界區(qū)。當 g1 協(xié)程通過 mutex 對臨界區(qū)進行加鎖后,臨界區(qū)將會被鎖定。此時如果 g2 想要訪問臨界區(qū),就會失敗并進入阻塞狀態(tài),直到鎖被釋放,g2 才能拿到臨界區(qū)的訪問權(quán)。
結(jié)構(gòu)體介紹
type Mutex struct {
state int32
sema uint32
}字段:
state
state 是一個 int32 類型的變量,它存儲著 Mutex 的各種狀態(tài)信息(未加鎖、被加鎖、喚醒狀態(tài)、饑餓狀態(tài)),不同狀態(tài)通過位運算進行計算。
sema
sema 是一個信號量,用于實現(xiàn) Mutex 的等待和喚醒機制。
方法:
Lock()
Lock() 方法用于獲取 Mutex 的鎖,如果 Mutex 已經(jīng)被其他的 goroutine 鎖定,則 Lock() 方法會一直阻塞,直到該 goroutine 獲取到鎖為止。
UnLock()
Unlock() 方法用于釋放 Mutex 的鎖,將 Mutex 的狀態(tài)設(shè)置為未鎖定的狀態(tài)。
TryLock()
Go 1.18 版本以后,sync.Mutex 新增一個 TryLock() 方法,該方法為非阻塞式的加鎖操作,如果加鎖成功,返回 true,否則返回 false。
雖然 TryLock() 的用法確實存在,但由于其使用場景相對較少,因此在使用時應該格外謹慎。TryLock() 方法注釋如下所示:
// Note that while correct uses of TryLock do exist, they are rare, // and use of TryLock is often a sign of a deeper problem // in a particular use of mutexes.
代碼示例
我們先來看一個有并發(fā)安全問題的例子
package main
import (
"fmt"
"sync"
)
var cnt int
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10000; j++ {
cnt++
}
}()
}
wg.Wait()
fmt.Println(cnt)
}在這個例子中,預期的 cnt 結(jié)果為 10 * 10000 = 100000。但是由于多個 goroutine 并發(fā)訪問了共享變量 cnt,并且沒有進行任何同步操作,可能導致讀寫沖突(race condition),從而影響 cnt 的值和輸出結(jié)果的正確性。這種情況下,不能確定最終輸出的 cnt 值是多少,每次執(zhí)行程序得到的結(jié)果可能不同。
在這種情況下,可以使用互斥鎖(sync.Mutex)來保護共享變量的訪問,保證只有一個 goroutine 能夠同時訪問 cnt,從而避免競態(tài)條件的問題。修改后的代碼如下:
package main
import (
"fmt"
"sync"
)
var cnt int
var mu sync.Mutex
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10000; j++ {
mu.Lock()
cnt++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(cnt)
}
在這個修改后的版本中,使用互斥鎖來保護共享變量 cnt 的訪問,可以避免出現(xiàn)競態(tài)條件的問題。具體而言,在 cnt++ 操作前,先執(zhí)行 Lock() 方法,以確保當前 goroutine 獲取到了互斥鎖并且獨占了共享變量的訪問權(quán)。在 cnt++ 操作完成后,再執(zhí)行 Unlock() 方法來釋放互斥鎖,從而允許其他 goroutine 獲取互斥鎖并訪問共享變量。這樣,只有一個 goroutine 能夠同時訪問 cnt,從而確保了最終輸出結(jié)果的正確性。
易錯場景
忘記解鎖
如果使用 Lock() 方法之后,沒有調(diào)用 Unlock() 解鎖,會導致其他 goroutine 被永久阻塞。例如:
package main
import (
"fmt"
"sync"
"time"
)
var mu sync.Mutex
var cnt int
func main() {
go increase(1)
go increase(2)
time.Sleep(time.Second)
fmt.Println(cnt)
}
func increase(delta int) {
mu.Lock()
cnt += delta
}在上述代碼中,通常情況下,cnt 的結(jié)果應該為 3。然而沒有解鎖操作,其中一個 goroutine 被阻塞,導致沒有達到預期效果,最終輸出的 cnt 可能只能為 1 或 2。
正確的做法是使用 defer 語句在函數(shù)返回前釋放鎖。
func increase(delta int) {
mu.Lock()
defer mu.Unlock() // 通過 defer 語句在函數(shù)返回前釋放鎖
cnt += delta
}重復加鎖
重復加鎖操作被稱為可重入操作。不同于其他一些編程語言的鎖實現(xiàn)(例如 Java 的 ReentrantLock),Go 的 mutex 并不支持可重入操作,如果發(fā)生了重復加鎖操作,就會導致死鎖。例如:
package main
import (
"fmt"
"sync"
"time"
)
var mu sync.Mutex
var cnt int
func main() {
go increase(1)
go increase(2)
time.Sleep(time.Second)
fmt.Println(cnt)
}
func increase(delta int) {
mu.Lock()
mu.Lock()
cnt += delta
mu.Unlock()
}在這個例子中,如果在 increase 函數(shù)中重復加鎖,將會導致 mu 鎖被第二次鎖住,而其他 goroutine 將被永久阻塞,從而導致程序死鎖。正確的做法是只對需要加鎖的代碼段進行加鎖,避免重復加鎖。
基于 Mutex 實現(xiàn)一個簡單的線程安全的緩存
import "sync"
type Cache struct {
data map[string]any
mu sync.Mutex
}
func (c *Cache) Get(key string) (any, bool) {
c.mu.Lock()
defer c.mu.Unlock()
value, ok := c.data[key]
return value, ok
}
func (c *Cache) Set(key string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}上述代碼實現(xiàn)了一個簡單的線程安全的緩存。使用 Mutex 可以保證同一時刻只有一個 goroutine 進行讀寫操作,避免多個 goroutine 并發(fā)讀寫同一數(shù)據(jù)時產(chǎn)生數(shù)據(jù)不一致性的問題。
對于緩存場景,讀操作比寫操作更頻繁,因此使用 RWMutex 代替 Mutex 會更好,因為 RWMutex 允許多個 goroutine 同時進行讀操作,只有在寫操作時才會進行互斥鎖定,從而減少了鎖的競爭,提高了程序的并發(fā)性能。后續(xù)文章會對 RWMutex 進行介紹。
小結(jié)
本文主要介紹了 Go 語言中互斥鎖 Mutex 的概念、對應的字段和方法、基本使用和易錯場景,最后基于 Mutex 實現(xiàn)一個簡單的線程安全的緩存。
Mutex 是保證共享資源數(shù)據(jù)一致性的重要手段,但使用不當會導致性能下降或死鎖等問題。因此,在使用 Mutex 時需要仔細考慮代碼的設(shè)計和并發(fā)場景,發(fā)揮 Mutex 的最大作用。
到此這篇關(guān)于詳解Golang中的Mutex并發(fā)原語的文章就介紹到這了,更多相關(guān)Golang Mutex并發(fā)原語內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
通過Go channel批量讀取數(shù)據(jù)的示例詳解
批量處理的主要邏輯是:從 channel 中接收數(shù)據(jù),積累到一定數(shù)量或者達到時間限制后,將數(shù)據(jù)批量處理(例如發(fā)送到 Kafka 或者寫入網(wǎng)絡),下面我將展示一個從 Go channel 中批量讀取數(shù)據(jù),并批量發(fā)送到 Kafka 和批量寫入網(wǎng)絡數(shù)據(jù)的示例,需要的朋友可以參考下2024-10-10

