初識Golang?Mutex互斥鎖的使用
前言
在學習操作系統(tǒng)的時候,我們應該都學習過臨界區(qū)、互斥鎖這些概念,用于在并發(fā)環(huán)境下保證狀態(tài)的正確性。比如在秒殺時,100 個用戶同時搶 10 個電腦,為了避免少賣或者超賣,就需要使用鎖來進行并發(fā)控制。在 Go語言 里面互斥鎖是 sync.Mutex ,我們本篇文章就來學習下為什么要使用互斥鎖、如何使用互斥鎖,以及使用時的常見問題。
為什么要使用互斥鎖
我們來看一個示例:我們起了 10000 個協(xié)程將變量 num 加1,因此肯定會存在并發(fā),如果我們不控制并發(fā),10000 個協(xié)程都執(zhí)行完后,該變量的值很大概率不等于 10000。
那么為什么會出現(xiàn)這個問題呢,原因是 num++ 不是原子操作,它會先讀取變量 num 當前值,然后對這個值 加1,再把結(jié)果保存到 num 中。例如 10 個 goroutine 同時運行到 num++ 這一行,可能同時讀取 num=1000,都加1后再保存, num=1001,這就與想要的結(jié)果不符。
package?main
import?(
?"fmt"
?"sync"
)
func?main()?{
?num?:=?0
?var?wg?sync.WaitGroup
?threadCount?:=?10000
?wg.Add(threadCount)
??
?for?i?:=?0;?i?<?threadCount;?i++?{
??go?func()?{
???defer?wg.Done()
???num++
??}()
?}
??
?wg.Wait()?//?等待?10000?個協(xié)程都執(zhí)行完
??fmt.Println(num)?//?9388(每次都可能不一樣)
}
我們?nèi)绻褂昧嘶コ怄i,可以保證每次進入臨界區(qū)的只有一個 goroutine,一個 goroutine 執(zhí)行完后,另一個 goroutine 才能進入臨界區(qū)執(zhí)行,最終就實現(xiàn)了并發(fā)控制。

并發(fā)獲取鎖示意圖
package?main
import?(
?"fmt"
?"sync"
)
func?main()?{
?num?:=?0
?var?mutex?sync.Mutex??//?互斥鎖
?var?wg?sync.WaitGroup
?threadCount?:=?10000
?wg.Add(threadCount)
?for?i?:=?0;?i?<?threadCount;?i++?{
??go?func()?{
???defer?wg.Done()
???
???mutex.Lock()?//?加鎖
???num++?//?臨界區(qū)
???mutex.Unlock()?//?解鎖
???
??}()
?}
?wg.Wait()
?fmt.Println(num)?//?10000
}
如何使用互斥鎖
Mutex 保持 Go 一貫的簡潔風格,開箱即用,聲明一個變量默認是沒有加鎖的,加鎖使用 Lock() 方法,解鎖使用 Unlock() 方法。
使用方式一:直接聲明使用
這個在上例中已經(jīng)體現(xiàn)了,直接看上面的例子就好
使用方式二:封裝在其他結(jié)構(gòu)體中
我們可以將 Mutex 封裝在 struct 中,封裝成線程安全的函數(shù)供外部調(diào)用。比如我們封裝了一個線程安全的計數(shù)器,調(diào)用 Add() 就加一,調(diào)用Count() 返回計數(shù)器的值。
package?main
import?(
?"fmt"
?"sync"
)
type?Counter?struct?{
?num???int
?mutex?sync.Mutex
}
//?加一操作,涉及到臨界區(qū)?num,加鎖解鎖
func?(counter?*Counter)?Add()?{
?counter.mutex.Lock()
?defer?counter.mutex.Unlock()
?counter.num++
}
//?返回數(shù)量,涉及到臨界區(qū)?num,加鎖解鎖
func?(counter?*Counter)?Count()?int?{
?counter.mutex.Lock()
?defer?counter.mutex.Unlock()
?return?counter.num
}
func?main()?{
?threadCount?:=?10000
??
?var?counter?Counter
?var?wg?sync.WaitGroup
?
?wg.Add(threadCount)
?for?i?:=?0;?i?<?threadCount;?i++?{
??go?func()?{
???defer?wg.Done()
???counter.Add()
??}()
?}
?wg.Wait()?//?等待所有?goroutine?都執(zhí)行完
?fmt.Println(counter.Count())?//?10000
}
在 Go 中,map 結(jié)構(gòu)是不支持并發(fā)的,如果并發(fā)讀寫就會 panic
//?運行會 panic,提示 fatal error: concurrent map writes
func?main()?{
?m?:=?make(map[string]string)
?var?wait?sync.WaitGroup
?wait.Add(1000)
?for?i?:=?0;?i?<?1000;?i++?{
??item?:=?fmt.Sprintf("%d",?i)
??go?func()?{
???wait.Done()
???m[item]?=?item
??}()
?}
?wait.Wait()
}
基于 Mutex ,我們可以實現(xiàn)一個線程安全的 map:
import?(
?"fmt"
?"sync"
)
type?ConcurrentMap?struct?{
?mutex?sync.Mutex
?items?map[string]interface{}
}
func?(c?*ConcurrentMap)?Add(key?string,?value?interface{})?{
?c.mutex.Lock()
?defer?c.mutex.Unlock()
?c.items[key]?=?value
}
func?(c?*ConcurrentMap)?Remove(key?string)?{
?c.mutex.Lock()
?defer?c.mutex.Unlock()
?delete(c.items,?key)
}
func?(c?*ConcurrentMap)?Get(key?string)?interface{}?{
?c.mutex.Lock()
?defer?c.mutex.Unlock()
?return?c.items[key]
}
func?NewConcurrentMap()?ConcurrentMap?{
?return?ConcurrentMap{
??items:?make(map[string]interface{}),
?}
}
func?main()?{
?m?:=?NewConcurrentMap()
?var?wait?sync.WaitGroup
?wait.Add(1000)
?for?i?:=?0;?i?<?1000;?i++?{
??item?:=?fmt.Sprintf("%d",?i)
??go?func()?{
???wait.Done()
???m.Add(item,?item)
??}()
?}
?wait.Wait()
?fmt.Println(m.Get("100"))?//?100
}
當然,基于互斥鎖 Mutex 實現(xiàn)的線程安全 map 并不是性能最好的,基于讀寫鎖 sync.RWMutex 和 分片 可以實現(xiàn)性能更好的、線程安全的 map,開發(fā)中比較常用的并發(fā)安全 map 是 orcaman / concurrent-map(https://github.com/orcaman/concurrent-map)。
互斥鎖的常見問題
從上面可以看出,Mutex 的使用過程方法比較簡單,但還是有幾點需要注意:
1.Mutex是可以在 goroutine A 中加鎖,在 goroutine B 中解鎖的,但是在實際使用中,盡量保證在同一個 goroutine 中加解鎖。比如 goroutine A 申請到了鎖,在處理臨界區(qū)資源的時候,goroutine B 把鎖釋放了,但是 A 以為自己還持有鎖,會繼續(xù)處理臨界區(qū)資源,就可能會出現(xiàn)問題。
2.Mutex的加鎖解鎖基本都是成對出現(xiàn),為了解決忘記解鎖,可以使用 defer 語句,在加鎖后直接 defer mutex.Unlock();但是如果處理完臨界區(qū)資源后還有很多耗時操作,為了盡早釋放鎖,不建議使用 defer,而是在處理完臨界區(qū)資源后就調(diào)用 mutex.Unlock() 盡早釋放鎖。
//?邏輯復雜,可能會忘記釋放鎖
func?main()?{
?var?mutex?sync.Mutex
?mutex.Lock()
?if?***?{
??if?***?{
???//?處理臨界區(qū)資源
???mutex.Unlock()
???return
??}
??//?處理臨界區(qū)資源
??mutex.Unlock()
??return
?}
?//?處理臨界區(qū)資源
?mutex.Unlock()
?return
}
//?避免邏輯復雜忘記釋放鎖,使用?defer語句,成對出現(xiàn)
func?main()?{
?var?mutex?sync.Mutex
?mutex.Lock()
?defer?mutex.Unlock()
?if?***?{
??if?***?{
???//?處理臨界區(qū)資源
???return
??}
??//?處理臨界區(qū)資源
??return
?}
?//?處理臨界區(qū)資源
?return
}3.Mutex 不能復制使用
Mutex 是有狀態(tài)的,比如我們對一個 Mutex 加鎖后,再進行復制操作,會把當前的加鎖狀態(tài)也給復制過去,基于加鎖的 Mutex 再加鎖肯定不會成功。進行復制操作可能聽起來是一個比較低級的錯誤,但是無意間可能就會犯這種錯誤。
package?main
import?(
?"fmt"
?"sync"
)
type?Counter?struct?{
?mutex?sync.Mutex
?num???int
}
func?SomeFunc(c?Counter)?{
?c.mutex.Lock()
?defer?c.mutex.Unlock()
?c.num--
}
func?main()?{
?var?counter?Counter
?counter.mutex.Lock()
?defer?counter.mutex.Unlock()
?counter.num++
?//?Go都是值傳遞,這里復制了?counter,此時?counter.mutex?是加鎖狀態(tài),在?SomeFunc?無法再次加鎖,就會一直等待
?SomeFunc(counter)
}以上就是初識Golang Mutex互斥鎖的使用的詳細內(nèi)容,更多關(guān)于Golang Mutex互斥鎖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go實現(xiàn)自動解壓縮包以及讀取docx/doc文件內(nèi)容詳解
在開發(fā)過程中,我們常常需要處理壓縮包和文檔文件。本文將介紹如何使用Go語言自動解壓縮包和讀取docx/doc文件,需要的可以參考一下2023-03-03
簡單了解Go語言中函數(shù)作為值以及函數(shù)閉包的使用
這篇文章主要介紹了簡單了解Go語言中函數(shù)作為值以及函數(shù)閉包的使用,是golang入門學習中的基礎(chǔ)知識,需要的朋友可以參考下2015-10-10

