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

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

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

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

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

