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

在上述圖片中,我們可以將綠色部分看作是臨界區(qū)。當(dāng) g1 協(xié)程通過 mutex 對臨界區(qū)進(jìn)行加鎖后,臨界區(qū)將會被鎖定。此時如果 g2 想要訪問臨界區(qū),就會失敗并進(jìn)入阻塞狀態(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)通過位運(yùn)算進(jìn)行計算。
sema
sema 是一個信號量,用于實(shí)現(xiàn) Mutex 的等待和喚醒機(jī)制。
方法:
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() 的用法確實(shí)存在,但由于其使用場景相對較少,因此在使用時應(yīng)該格外謹(jǐn)慎。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)
}在這個例子中,預(yù)期的 cnt 結(jié)果為 10 * 10000 = 100000。但是由于多個 goroutine 并發(fā)訪問了共享變量 cnt,并且沒有進(jìn)行任何同步操作,可能導(dǎo)致讀寫沖突(race condition),從而影響 cnt 的值和輸出結(jié)果的正確性。這種情況下,不能確定最終輸出的 cnt 值是多少,每次執(zhí)行程序得到的結(jié)果可能不同。
在這種情況下,可以使用互斥鎖(sync.Mutex)來保護(hù)共享變量的訪問,保證只有一個 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)
}
在這個修改后的版本中,使用互斥鎖來保護(hù)共享變量 cnt 的訪問,可以避免出現(xiàn)競態(tài)條件的問題。具體而言,在 cnt++ 操作前,先執(zhí)行 Lock() 方法,以確保當(dāng)前 goroutine 獲取到了互斥鎖并且獨(dú)占了共享變量的訪問權(quán)。在 cnt++ 操作完成后,再執(zhí)行 Unlock() 方法來釋放互斥鎖,從而允許其他 goroutine 獲取互斥鎖并訪問共享變量。這樣,只有一個 goroutine 能夠同時訪問 cnt,從而確保了最終輸出結(jié)果的正確性。
易錯場景
忘記解鎖
如果使用 Lock() 方法之后,沒有調(diào)用 Unlock() 解鎖,會導(dǎo)致其他 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é)果應(yīng)該為 3。然而沒有解鎖操作,其中一個 goroutine 被阻塞,導(dǎo)致沒有達(dá)到預(yù)期效果,最終輸出的 cnt 可能只能為 1 或 2。
正確的做法是使用 defer 語句在函數(shù)返回前釋放鎖。
func increase(delta int) {
mu.Lock()
defer mu.Unlock() // 通過 defer 語句在函數(shù)返回前釋放鎖
cnt += delta
}重復(fù)加鎖
重復(fù)加鎖操作被稱為可重入操作。不同于其他一些編程語言的鎖實(shí)現(xiàn)(例如 Java 的 ReentrantLock),Go 的 mutex 并不支持可重入操作,如果發(fā)生了重復(fù)加鎖操作,就會導(dǎo)致死鎖。例如:
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ù)中重復(fù)加鎖,將會導(dǎo)致 mu 鎖被第二次鎖住,而其他 goroutine 將被永久阻塞,從而導(dǎo)致程序死鎖。正確的做法是只對需要加鎖的代碼段進(jìn)行加鎖,避免重復(fù)加鎖。
基于 Mutex 實(shí)現(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
}上述代碼實(shí)現(xiàn)了一個簡單的線程安全的緩存。使用 Mutex 可以保證同一時刻只有一個 goroutine 進(jìn)行讀寫操作,避免多個 goroutine 并發(fā)讀寫同一數(shù)據(jù)時產(chǎn)生數(shù)據(jù)不一致性的問題。
對于緩存場景,讀操作比寫操作更頻繁,因此使用 RWMutex 代替 Mutex 會更好,因?yàn)?RWMutex 允許多個 goroutine 同時進(jìn)行讀操作,只有在寫操作時才會進(jìn)行互斥鎖定,從而減少了鎖的競爭,提高了程序的并發(fā)性能。后續(xù)文章會對 RWMutex 進(jìn)行介紹。
小結(jié)
本文主要介紹了 Go 語言中互斥鎖 Mutex 的概念、對應(yīng)的字段和方法、基本使用和易錯場景,最后基于 Mutex 實(shí)現(xiàn)一個簡單的線程安全的緩存。
Mutex 是保證共享資源數(shù)據(jù)一致性的重要手段,但使用不當(dāng)會導(dǎo)致性能下降或死鎖等問題。因此,在使用 Mutex 時需要仔細(xì)考慮代碼的設(shè)計和并發(fā)場景,發(fā)揮 Mutex 的最大作用。
到此這篇關(guān)于詳解Golang中的Mutex并發(fā)原語的文章就介紹到這了,更多相關(guān)Golang Mutex并發(fā)原語內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang 結(jié)構(gòu)體初始化時賦值格式介紹
這篇文章主要介紹了golang 結(jié)構(gòu)體初始化時賦值格式介紹,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
Golang實(shí)現(xiàn)將視頻按照時間維度剪切的工具
這篇文章主要為大家詳細(xì)介紹了如何利用Golang實(shí)現(xiàn)將視頻按照時間維度進(jìn)行剪切,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2022-12-12
Golang實(shí)現(xiàn)Redis事務(wù)深入探究
這篇文章主要介紹了Golang實(shí)現(xiàn)Redis事務(wù)深入探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
Golang優(yōu)雅保持main函數(shù)不退出的辦法
很多時候我們需要讓main函數(shù)不退出,讓它在后臺一直執(zhí)行,下面這篇文章主要給大家介紹了關(guān)于Golang優(yōu)雅保持main函數(shù)不退出的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
一文帶大家了解Go語言中的內(nèi)聯(lián)優(yōu)化
內(nèi)聯(lián)優(yōu)化是一種常見的編譯器優(yōu)化策略,通俗來講,就是把函數(shù)在它被調(diào)用的地方展開,這樣可以減少函數(shù)調(diào)用所帶來的開銷,本文主要為大家介紹了Go中內(nèi)聯(lián)優(yōu)化的具體使用,需要的可以參考下2023-05-05
使用gopkg.in/yaml.v3?解析YAML數(shù)據(jù)詳解
這篇文章主要為大家介紹了使用gopkg.in/yaml.v3?解析YAML數(shù)據(jù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
詳解Golang如何實(shí)現(xiàn)一個環(huán)形緩沖器
環(huán)形緩沖器(ringr?buffer)是一種用于表示一個固定尺寸、頭尾相連的緩沖區(qū)的數(shù)據(jù)結(jié)構(gòu),適合緩存數(shù)據(jù)流。本文將利用Golang實(shí)現(xiàn)一個環(huán)形緩沖器,需要的可以參考一下2022-09-09

