Go語言 | 併發設計中的同步鎖與waitgroup用法

TechFlow2019發表於2020-09-14

今天是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 -

原文連結,求個關注

相關文章