今天是golang專題的第16篇文章,我們一起來聊聊golang當中的併發相關的一些使用。
雖然關於goroutine以及channel我們都已經介紹完了,但是關於併發的機制仍然沒有介紹結束。只有goroutine以及channel有時候還是不足以完成我們的問題,比如多個goroutine同時訪問一個變數的時候,我們怎麼保證這些goroutine之間不會互相沖突或者是影響呢?這可能就需要我們對資源進行加鎖或者是採取其他的操作了。
同步鎖
golang當中提供了兩種常用的鎖,一種是sync.Mutex另外一種是sync.RWMutex。我們先說說Mutex,它就是最簡單最基礎的同步鎖,當一個goroutine持有鎖的時候,其他的goroutine只能等待到鎖釋放之後才可以嘗試持有。而RWMutex是讀寫鎖的意思,它支援一寫多讀,也就是說允許支援多個goroutine同時持有讀鎖,而只允許一個goroutine持有寫鎖。當有goroutine持有讀鎖的時候,會阻止寫操作。當有goroutine持有寫鎖的時候,無論讀寫都會被堵塞。
我們使用的時候需要根據我們場景的特性來決定,如果我們的場景是讀操作多過寫操作的場景,那麼我們可以使用RWMutex。如果是寫操作為主,那麼使用哪個都差不多。
我們來看下使用的案例,假設我們當前有多個goroutine,但是我們只希望持有鎖的goroutine執行,我們可以這麼寫:
var lock sync.Mutex
for i := 0; i < 10; i++ {
go func() {
lock.Lock()
defer lock.Unlock()
// do something
}()
}
雖然我們用for迴圈啟動了10個goroutine,但是由於互斥鎖的存在,同一時刻只能有一個goroutine在執行。
RWMutex區分了讀寫鎖,所以我們一共會有4個api,分別是Lock, Unlock, RLock, RUnlock。Lock和Unlock是寫鎖的加鎖以及解鎖,而RLock和RUnlock自然就是讀鎖的加鎖和解鎖了。具體的用法和上面的程式碼一樣,我就不多贅述了。
全域性操作一次
在一些場景以及一些設計模式當中,會要求我們某一段程式碼只能執行一次。比如很著名的單例模式,就是將我們經常使用的工具設計成單例,無論執行的過程當中初始化多少次,得到的都是同一個例項。這樣做的目的是減去建立例項的時間,尤其是像是資料庫連線、hbase連線等這些例項建立的過程非常的耗時。
那我們怎麼在golang當中實現單例呢?
有些同學可能會覺得這個很簡單啊,我們只需要用一個bool型變數判斷一下初始化是否有完成不就可以了嗎?比如這樣:
type Test struct {}
var test Test
var flag = false
func init() Test{
if !flag {
test = Test{}
flag = true
}
return test
}
看起來好像沒有問題,但是仔細琢磨就會發現不對的地方。因為if判斷當中的語句並不是原子的,也就是說有可能同時被很多goroutine同時訪問。這樣的話有可能test這個變數會被多次初始化並且被多次覆蓋,直到其中一個goroutine將flag置為true為止。這可能會導致一開始訪問的goroutine獲得的test都各不相同,而產生未知的風險。
要想要實現單例其實很簡單,sync庫當中為我們提供了現成的工具once。它可以傳入一個函式,只允許全域性執行這個函式一次。在執行結束之前,其他goroutine執行到once語句的時候會被阻塞,保證只有一個goroutine在執行once。當once執行結束之後,再次執行到這裡的時候,once語句的內容將會被跳過,我們來結合一下程式碼來理解一下,其實也非常簡單。
type Test struct {}
var test Test
func create() {
test = Test{}
}
func init() Test{
once.Do(create)
return test
}
waitgroup
最後給大家介紹一下waitgroup的用法,我們在使用goroutine的時候有一個問題是我們在主程式當中並不知道goroutine執行結束的時間。如果我們只是要依賴goroutine執行的結果,當然可以通過channel來實現。但假如我們明確地希望等到goroutine執行結束之後再執行下面的邏輯,這個時候我們又該怎麼辦呢?
有人說可以用sleep,但問題是我們並不知道goroutine執行到底需要多少時間,怎麼能事先知道需要sleep多久呢?
為了解決這個問題,我們可以使用sync當中的另外一個工具,也就是waitgroup。
waitgroup的用法非常簡單,只有三個方法,一個是Add,一個是Done,最後一個是Wait。其實waitgroup內部儲存了當前有多少個goroutine在執行,當呼叫一次Add x的時候,表示當下同時產生了x個新的goroutine。當這些goroutine執行完的時候, 我們讓它呼叫一下Done,表示執行結束了一個goroutine。這樣當所有goroutine都執行完Done之後,wait的阻塞會結束。
我們來看一個例子:
sample := Sample{}
wg := sync.WaitGroup{}
go func() {
// 增加一個正在執行的goroutine
wg.Add(1)
// 執行完成之後Done一下
defer wg.Done()
sample.JoinUserFeature()
}()
go func() {
wg.Add(1)
defer wg.Done()
sample.JoinItemFeature()
}()
wg.Wait()
// do something
總結
上面介紹的這些工具和庫都是我們日常在併發場景當中經常使用的,也是一個golang工程師必會的技能之一。到這裡為止,關於golang這門語言的基本功能介紹就差不多了,後面將會介紹一些實際應用的內容,敬請期待吧。
今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。
- END -