Go 語言學習入門系列:互斥鎖和讀寫鎖

要知道的是在 Go 語言中,信道的地位非常高,面對併發問題,我們始終應該優先考慮使用信道。

但是如果通過信道解決不了的,不得不使用共享內存來實現併發編程的,那 Golang 中的鎖機制必須會使用

而在 Golang 裏有專門的方法來實現鎖,在 sync 包裏面。這個包有兩個很重要的鎖類型

  • 一個叫 Mutex, 利用它可以實現互斥鎖。
  • 一個叫 RWMutex,利用它可以實現讀寫鎖。

互斥鎖 :Mutex

使用互斥鎖(Mutex,全稱 mutual exclusion)是爲了來保護一個資源不會因爲併發操作而引起衝突導致數據不準確。

舉個栗子,就像下面這段代碼,我開啓了三個協程,每個協程分別往 count 這個變量加1000次 1,理論上看,最終的 count 值應試爲 3000。

package main

import (
    "fmt"
    "sync"
)

func add(count *int, wg *sync.WaitGroup) {
    for i := 0; i < 1000; i++ {
        *count = *count + 1
    }
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    count := 0
    wg.Add(3)
    go add(&count, &wg)
    go add(&count, &wg)
    go add(&count, &wg)

    wg.Wait()
    fmt.Println("count 的值爲:", count)
}

可運行多次的結果,都不相同

// 第一次
count 的值爲: 2854

// 第二次
count 的值爲: 2673

// 第三次
count 的值爲: 2840

原因就在於這三個協程在執行時,先讀取 count 再更新 count 的值,而這個過程並不具備原子性(同一時刻多個協程讀取了count,而某一時刻的count值是一樣的,同一時刻 count 就少加了),所以導致了數據的不準確。

解決這個問題的方法,就是給 add 這個函數加上 Mutex 互斥鎖,要求同一時刻,僅能有一個協程能對 count 操作。

在寫代碼前,先了解一下 Mutex 鎖的兩種定義方法

// 第一種
var lock *sync.Mutex
lock = new(sync.Mutex)

// 第二種
lock := &sync.Mutex{}

修改上面的代碼,如下所示

import (
    "fmt"
    "sync"
)

func add(count *int, wg *sync.WaitGroup, lock *sync.Mutex) {
    for i := 0; i < 1000; i++ {
        lock.Lock()
        *count = *count + 1 //必須是引用
        lock.Unlock()
    }
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    lock := &sync.Mutex{}
    count := 0
    wg.Add(3)
    go add(&count, &wg, lock)
    go add(&count, &wg, lock)
    go add(&count, &wg, lock)

    wg.Wait()
    fmt.Println("count 的值爲:", count)
}

此時,不管執行多少次,輸出都只有一個結果

count 的值爲: 3000

使用 Mutext 鎖雖然很簡單,但需要注意:

  • 同一協程裏,不要在尚未解鎖時再次使加鎖

  • 同一協程裏,不要對已解鎖的鎖再次解鎖

  • 加了鎖後,別忘了解鎖,必要時使用 defer 語句

讀寫鎖:RWMutex

Mutex 是一種傻瓜式的操作的鎖類型,加鎖解鎖加鎖解鎖,很簡單。但是簡單 同時意味着在某些特殊情況下有可能會造成時間上的浪費,導致程序性能低下。

舉個例子,我們平時去圖書館,要麼是去借書,要麼去還書,借書的流程繁鎖,沒有辦卡的還要讓管理員給你辦卡,因此借書通常都要排老長的隊,假設圖書館裏只有一個管理員,按照 Mutex(互斥鎖)的思想, 這個管理員同一時刻只能服務一個人,這就意味着,還書的也要跟借書的一起排隊。

可還書的步驟非常簡單,可能就把書給管理員掃下碼就可以走了。如果讓還書的人,跟借書的人一起排隊,那估計有很多人都不樂意了。因此,圖書館爲了提高整個流程的效率,就允許還書的人,不需要排隊,可以直接自助還書。圖書管將館裏的人分得更細了,對於讀者的不同需求提供了不同的方案。提高了效率。(或者是寄取快遞的栗子)

RWMutex,也是如此,它將程序對資源的訪問分爲讀操作和寫操作

  • 爲了保證數據的安全,它規定了當有人還在讀取數據(即讀鎖佔用)時,不允計有人更新這個數據(即寫鎖會阻塞)。有人在更新數據(即寫鎖佔用)的時候,不允許其他的協程讀取數劇(讀鎖將阻塞)和更新數據(即寫鎖阻塞)

  • 爲了保證程序的效率,多個人(線程)讀取數據(擁有讀鎖)時,互不影響不會造成阻塞,它不會像 Mutex 那樣只允許有一個人(線程)讀取同一個數據。

 

也就是說讀鎖與讀鎖兼容,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥,只有在鎖釋放後纔可以繼續申請互斥的鎖

理解了這個後,再來看看,如何使用 RWMutex?

定義一個 RWMuteux 鎖,同樣有兩種方法

// 第一種
var lock *sync.RWMutex
lock = new(sync.RWMutex)

// 第二種
lock := &sync.RWMutex{}

RWMutex 裏提供了兩種鎖,每種鎖分別對應兩個方法,爲了避免死鎖,兩個方法應成對出現,必要時請使用 defer。

  • 讀鎖:調用 RLock 方法開啓鎖,調用 RUnlock 釋放鎖

  • 寫鎖:調用 Lock 方法開啓鎖,調用 Unlock 釋放鎖(和 Mutex類似)

接下來,直接看一下例子吧

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    lock := &sync.RWMutex{}
    lock.Lock()

    for i := 0; i < 4; i++ {
        go func(i int) {
            fmt.Printf("第 %d 個協程準備開始... \n", i)
            lock.RLock()
            fmt.Printf("第 %d 個協程獲得讀鎖, sleep 1s 後,釋放鎖\n", i)
            time.Sleep(time.Second)
            lock.RUnlock()
        }(i)
    }

    time.Sleep(time.Second * 2)

    fmt.Println("準備釋放寫鎖,讀鎖不再阻塞")
    // 寫鎖一釋放,讀鎖就自由了
    lock.Unlock()

    // 由於會等到讀鎖全部釋放,才能獲得寫鎖
    // 因爲這裏一定會在上面 4 個協程全部完成才能往下走
    lock.Lock()
    fmt.Println("程序退出...")
    lock.Unlock()
}

輸出如下

第 1 個協程準備開始... 
第 0 個協程準備開始... 
第 3 個協程準備開始... 
第 2 個協程準備開始... 
準備釋放寫鎖,讀鎖不再阻塞
第 2 個協程獲得讀鎖, sleep 1s 後,釋放鎖
第 3 個協程獲得讀鎖, sleep 1s 後,釋放鎖
第 1 個協程獲得讀鎖, sleep 1s 後,釋放鎖
第 0 個協程獲得讀鎖, sleep 1s 後,釋放鎖
程序退出...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章