Go語(yǔ)言如何利用Mutex保障數(shù)據(jù)讀寫(xiě)正確
Go 并發(fā)場(chǎng)景下如何保障數(shù)據(jù)讀寫(xiě)正確?本文聊聊 Mutex 的用法。
Go 語(yǔ)言作為一個(gè)原生支持用戶(hù)態(tài)進(jìn)程(Goroutine)的語(yǔ)言,當(dāng)提到并發(fā)編程、多線程編程時(shí),往往都離不開(kāi)鎖這一概念。鎖是一種并發(fā)編程中的同步原語(yǔ)(Synchronization Primitives),它能保證多個(gè) Goroutine 在訪問(wèn)同一片內(nèi)存時(shí)不會(huì)出現(xiàn)競(jìng)爭(zhēng)條件(Race condition)等問(wèn)題。
本文,我會(huì)帶你詳細(xì)了解互斥鎖的實(shí)現(xiàn)機(jī)制,以及 Go 標(biāo)準(zhǔn)庫(kù)的互斥鎖 Mutex 的基本使用方法。后面會(huì)講解 Mutex 的具體實(shí)現(xiàn)原理、易錯(cuò)場(chǎng)景和一些拓展用法。 歡迎關(guān)注一下不迷路。
好了,我們先來(lái)看看互斥鎖的實(shí)現(xiàn)機(jī)制。
1、實(shí)現(xiàn)機(jī)制
互斥鎖 Mutex 是并發(fā)控制的一個(gè)基本手段,是為了避免并發(fā)競(jìng)爭(zhēng)建立的并發(fā)控制機(jī)制,其中有個(gè)“臨界區(qū)”的概念。
在并發(fā)編程過(guò)程中,如果程序中一部分資源或者變量會(huì)被并發(fā)訪問(wèn)或者修改,為了避免并發(fā)訪問(wèn)導(dǎo)致數(shù)據(jù)的不準(zhǔn)確,這部分程序需要率先被保護(hù)起來(lái),之后操作,操作結(jié)束后去除保護(hù),這部分被保護(hù)的程序就叫做 臨界區(qū)。
限定臨界區(qū)只能同時(shí)由一個(gè)線程持有。 當(dāng)臨界區(qū)由一個(gè)線程持有的時(shí)候,其它線程如果想進(jìn)入這個(gè)臨界區(qū),就會(huì)返回失敗,或者是等待。直到持有的線程退出臨界區(qū),其他線程才有機(jī)會(huì)獲得這個(gè)臨界區(qū)。如下圖:

Go mutex 臨界區(qū)示意圖
上圖互斥鎖就很好地解決了資源競(jìng)爭(zhēng)問(wèn)題,有人也把互斥鎖叫做排它鎖。那在 Go 標(biāo)準(zhǔn)庫(kù)中,它提供了 Mutex 來(lái)實(shí)現(xiàn)互斥鎖這個(gè)功能。
Go 語(yǔ)言在 sync 包中提供了用于同步的一些基本原語(yǔ),包括常見(jiàn)的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once 和 sync.Cond。這次主要講 Mutex。
接下來(lái)我們看看到底可以怎么使用 Mutex。
2、基本用法
在 Go 的標(biāo)準(zhǔn)庫(kù)中,package sync 提供了鎖相關(guān)的一系列同步原語(yǔ),這個(gè) package 還定義了一個(gè) Locker 的接口,Mutex 就實(shí)現(xiàn)了這個(gè)接口。
互斥鎖 Mutex 提供了兩個(gè)方法 Lock 和 Unlock:進(jìn)入到臨界區(qū)使用 Lock 方法加鎖,退出臨界區(qū)使用 Unlock 方法釋放鎖。
type Locker interface {
Lock()
Unlock()
}上面可以看出,Go 定義的鎖接口的方法集很簡(jiǎn)單,就是請(qǐng)求鎖(Lock)和釋放鎖(Unlock)這兩個(gè)方法,繼承了 Go 語(yǔ)言一貫的簡(jiǎn)潔風(fēng)格。
我們本文會(huì)介紹的 Mutex 以及后面會(huì)介紹的讀寫(xiě)鎖 RWMutex 都實(shí)現(xiàn)了 Locker 接口,所以首先我把這個(gè)接口介紹了,提前了解一下。
func(m *Mutex)Lock() func(m *Mutex)Unlock()
并發(fā)場(chǎng)景下,一個(gè) goroutine 調(diào)用 Lock 方法拿到鎖后,此時(shí)其他的 goroutine 會(huì)阻塞在 Lock 的調(diào)用上,一直等到當(dāng)前獲取到鎖的 goroutine 釋放鎖。
看到這兒,你可能會(huì)問(wèn),為啥一定要加鎖呢?那我們就說(shuō)一下在并發(fā)場(chǎng)景下不使用鎖的例子,看下會(huì)出現(xiàn)什么問(wèn)題。
舉一個(gè)計(jì)數(shù)器的例子,是由 10 個(gè) goroutine 對(duì)計(jì)數(shù)器進(jìn)行累加操作,每個(gè) goroutine 負(fù)責(zé)執(zhí)行 10 萬(wàn)次的加 1 操作,期望的結(jié)果是 1000000 (10 * 100000)。
package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
// 使用 WaitGroup 等待,創(chuàng)建 10 個(gè)goroutine
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i< 10;i++ {
go func() {
defer wg.Done()
// 對(duì)變量count執(zhí)行10次加1
for j := 0; j< 100000; j++ {
count++
}
}()
}
// 等待 10個(gè) goroutine完成
wg.Wait()
fmt.Printin("count:", count)
}每次運(yùn)行,都得到了不同的結(jié)果,所以是不會(huì)得到期望的 1000000。

那么這是為什么?
其實(shí),因?yàn)?count++ 不是一個(gè)原子操作,就可能有并發(fā)的問(wèn)題。
上述是并發(fā)訪問(wèn)共享數(shù)據(jù)的常見(jiàn)錯(cuò)誤,10 個(gè) goroutine 同時(shí)讀取到 count 的值為 9867,對(duì)值加 1,值變成 啦9868,然后把這個(gè)值覆蓋到 count,但是實(shí)際上此時(shí)我們?cè)黾拥目倲?shù)應(yīng)該是 10 才對(duì),這里卻只增加了 1,好多計(jì)數(shù)都被“吞”掉了。
3、race detector
很多時(shí)候,并發(fā)問(wèn)題隱藏得非常深,即使是有經(jīng)驗(yàn)的人,也不太容易發(fā)現(xiàn)或者 Debug 出來(lái)。
Go race detector , 一個(gè)檢測(cè)并發(fā)訪問(wèn)共享資源是否有問(wèn)題的工具,它可以幫助我們自動(dòng)發(fā)現(xiàn)程序有沒(méi)有 data race 的問(wèn)題。是基于 Google 的 C/C++ sanitizers 技術(shù)實(shí)現(xiàn)的,能夠監(jiān)測(cè)出內(nèi)存地址的訪問(wèn),當(dāng)代碼運(yùn)行時(shí),race detector 可以很好的監(jiān)控到共享變量的非同步訪問(wèn),出現(xiàn) race 的時(shí)候,能夠輸出警告的信息。
怎么用的呢?
在編譯、測(cè)試、運(yùn)行 Go 代碼的時(shí)候,加上 race 參數(shù),就有可能發(fā)現(xiàn)并發(fā)問(wèn)題。比如在上面的例子中,我們可以加上 race 參數(shù)運(yùn)行,檢測(cè)一下是不是有并發(fā)問(wèn)題。
go run -race main.go 就會(huì)輸出警告信息。

圖中會(huì)提示有并發(fā)問(wèn)題,會(huì)提示哪一個(gè) goroutine 在某一行對(duì)變量有寫(xiě)操作,同時(shí)也會(huì)提示哪個(gè) goroutine 在某一行對(duì)變量有讀操作,這就是并發(fā)操作時(shí)引起了 data race。
既然存在 data race 問(wèn)題,我們?cè)趺慈ソ鉀Q呢?接下來(lái)就講下 Mutex,它可以輕松地消除掉 data race。
package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
// 互斥鎖保護(hù)計(jì)數(shù)器
var mu sync.Mutex
// 輔助變量,用來(lái)確認(rèn)所有的goroutine都完成
var wg sync.WaitGroup
wg.Add(10)
// 啟動(dòng)10個(gè)gourontine
for i := 0;i< 10;i+++ {
go func() {
defer wg.Done()
for j := 0; j< 100000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
// 等待 10個(gè) goroutine完成
wg.Wait()
fmt.Printin("count:", count)
}運(yùn)行一下 go run -race main.go

你會(huì)發(fā)現(xiàn)輸出了期望值 1000000,data race 告警也沒(méi)有啦。
怎么樣,是不是很驚喜,使用 Mutex 是不是非常高效?
我們?cè)谌粘J褂弥校琈utex 會(huì)嵌入到其它 struct 中使用。
type Counter struct{
sync.Mutex
Count uint64
}
func main() {
var counter Counter
var wg sync.WaitGroup
wg.Add(10)
for i := 0;i< 10;i++ {
go func() {
defer wg.Done()
for j := 0; j < 100000; j++ {
counter.Lock()
counter.Count++
counter.Unlock()
}
}()
}
wg.Wait()
fmt.Println("count:", counter.Count)
}當(dāng)嵌入的 struct 有多個(gè)字段,我們會(huì)把 Mutex 放在要控制的字段上面,然后使用空格把字段分隔開(kāi)來(lái)。這樣寫(xiě)的話(huà),邏輯會(huì)更清晰,也更易于維護(hù)。
有時(shí)候,你還可以把獲取鎖、釋放鎖、計(jì)數(shù)加一的邏輯封裝成一個(gè)方法,對(duì)外不需要暴露鎖等邏輯。
//線程安全的計(jì)數(shù)器類(lèi)型
type Counter struct{
CounterType int
Name string
mu sync.Mutex
count uint64
}
func main() {
// 封裝一個(gè)計(jì)數(shù)器
var counter Counter
var wg sync.WaitGroup
wg.Add(10)
// 啟動(dòng) 10 個(gè) goroutine
for i := 0;i< 10;i++ {
go func() {
defer wg.Done( )
// 執(zhí)行 10 萬(wàn)次累加
for j := 0; j< 100000; j++ {
// 受到鎖保護(hù)的方法
counter.Incr()
}
}()
}
wg.Wait()
fmt.PrintIn(counter.Count())
}
// 加1的方法,內(nèi)部使用互斥鎖保護(hù)
func (c *Counter) Incr() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// 得到計(jì)數(shù)器的值,也需要鎖保護(hù)
func (c *Counter) Count() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}4、總結(jié)
本文介紹了并發(fā)問(wèn)題的背景知識(shí)、標(biāo)準(zhǔn)庫(kù)中 Mutex 的使用,通過(guò) Go race detector 工具發(fā)下并發(fā)場(chǎng)景下的問(wèn)題及解決方法。你肯定已經(jīng)了解了 Mutex 這個(gè)同步原語(yǔ)。
日常開(kāi)發(fā)中,在設(shè)計(jì)階段,我們就應(yīng)該需要考慮共享資源的并發(fā)問(wèn)題,當(dāng)然在初始階段有時(shí)候并不是很確定某個(gè)資源時(shí)否會(huì)唄共享,會(huì)隨著后續(xù)的迭代會(huì)顯現(xiàn)。雖遲但會(huì)到。當(dāng)你意識(shí)到這個(gè)問(wèn)題時(shí),就需要通過(guò)互斥鎖來(lái)解決啦。
其實(shí) Docker issue 37583、35517、32826、30696等、kubernetes issue 72361、71617等,都是后來(lái)發(fā)現(xiàn)的 data race 而采用互斥鎖 Mutex 進(jìn)行修復(fù)的。
5、思考問(wèn)題
Q: 當(dāng) Mutex 已經(jīng)被一個(gè) goroutine 獲取了鎖,其它的 goroutine 們只能一直等待。當(dāng)這個(gè)鎖釋放后,等待中的 goroutine 中哪一個(gè)會(huì)優(yōu)先獲取 Mutex 呢?
到此這篇關(guān)于Go語(yǔ)言如何利用Mutex保障數(shù)據(jù)讀寫(xiě)正確的文章就介紹到這了,更多相關(guān)Go語(yǔ)言Mutex保障數(shù)據(jù)讀寫(xiě)正確內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語(yǔ)言并發(fā)處理效率響應(yīng)能力及在現(xiàn)代軟件開(kāi)發(fā)中的重要性
這篇文章主要為大家介紹了Go語(yǔ)言并發(fā)處理的效率及響應(yīng)能力以及在現(xiàn)代軟件開(kāi)發(fā)中的重要性實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Golang中Gin數(shù)據(jù)庫(kù)表名前綴的三種方法
本文主要介紹了Golang中Gin數(shù)據(jù)庫(kù)表名前綴的三種方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
一文詳解Go語(yǔ)言io.LimitedReader類(lèi)型
這篇文章主要為大家介紹了Go語(yǔ)言io.LimitedReader類(lèi)型示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
Golang利用Template模板動(dòng)態(tài)生成文本
Go語(yǔ)言中的Go?Template是一種用于生成文本輸出的簡(jiǎn)單而強(qiáng)大的模板引擎,它提供了一種靈活的方式來(lái)生成各種格式的文本,下面我們就來(lái)看看具體如何使用Template實(shí)現(xiàn)動(dòng)態(tài)文本生成吧2023-09-09
Go語(yǔ)言基礎(chǔ)go install命令使用示例詳解
這篇文章主要為大家介紹了Go語(yǔ)言基礎(chǔ)go install命令的使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2021-11-11
利用Go語(yǔ)言快速實(shí)現(xiàn)一個(gè)極簡(jiǎn)任務(wù)調(diào)度系統(tǒng)
任務(wù)調(diào)度(Task Scheduling)是很多軟件系統(tǒng)中的重要組成部分,字面上的意思是按照一定要求分配運(yùn)行一些通常時(shí)間較長(zhǎng)的腳本或程序。本文將利用Go語(yǔ)言快速實(shí)現(xiàn)一個(gè)極簡(jiǎn)任務(wù)調(diào)度系統(tǒng),感興趣的可以了解一下2022-10-10

