Go語言中互斥鎖與讀寫鎖,你知多少?

簡述

Golang中的鎖機制主要包含互斥鎖和讀寫鎖

互斥鎖

互斥鎖是傳統併發程序對共享資源進行控制訪問的主要手段。在Go中主要使用 sync.Mutex的結構體表示。

一個簡單的示例:

func mutex() {
var mu sync.Mutex
mu.Lock()
fmt.Println("locked")
mu.Unlock()
}

或者也可以使用defer來實現,這在整個函數流程中全部要加鎖時特別有用,還有一個好處就是可以防止忘記Unlock

func mutex() {
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
fmt.Println("locked")
}

互斥鎖是開箱即用的,只需要申明sync.Mutex即可直接使用

var mu sync.Mutex

互斥鎖應該是成對出現,在同步語句不可以再對鎖加鎖,看下面的示例:

func mutex() {
var mu sync.Mutex
mu.Lock()
fmt.Println("parent locked")
mu.Lock()

fmt.Println("sub locked")
mu.Unlock()
mu.Unlock()
}

此時則會出現fatal error: all goroutines are asleep - deadlock!錯誤

同樣,如果多次對一個鎖解鎖,則會出現fatal error: sync: unlock of unlocked mutex錯誤

func mutex() {
var mu sync.Mutex
mu.Lock()
fmt.Println("locked")
mu.Unlock()
mu.Unlock()
}

那麼在goroutine中是否對外部鎖加鎖呢?

func mutex() {
var mu sync.Mutex
fmt.Println("parent lock start")
mu.Lock()
fmt.Println("parent locked")
for i := 0; i <= 2; i++ {
go func(i int) {
fmt.Printf("sub(%d) lock start\\n", i)
mu.Lock()
fmt.Printf("sub(%d) locked\\n", i)
time.Sleep(time.Microsecond * 30)
mu.Unlock()
fmt.Printf("sub(%d) unlock\\n", i)
}(i)
}
time.Sleep(time.Second * 2)
mu.Unlock()
fmt.Println("parent unlock")
time.Sleep(time.Second * 2)
}

先看上面的函數執行結果

parent lock start
parent locked
sub(0) lock start

sub(2) lock start
sub(1) lock start
parent unlock // 必須等到父級先解鎖,後面則會阻塞
sub(0) locked // 解鎖後子goroutine才能執行鎖定
sub(0) unlock
sub(2) locked
sub(2) unlock
sub(1) locked
sub(1) unlock

為了方便調試,使用了time.Sleep()來延遲保證goroutine的執行 從結果中可以看出,當所有的goroutine遇到Lock時都會阻塞,而當main函數中的Unlock執行後,會有一個優先(無序)的goroutine來佔得鎖,其它的則再次進入阻塞狀態。

總結:

  • 互斥鎖必須成對出現
  • 同級別互斥鎖不能嵌套使用
  • 父級中如果存在鎖,當在goroutine中執行重複鎖定操作時goroutine將被阻塞,直到原互斥鎖解鎖,多個goroutine將會爭搶當前鎖資源,其它繼續阻塞。


Go語言中互斥鎖與讀寫鎖,你知多少?


讀寫鎖

讀寫鎖和互斥鎖不同之處在於,可以分別針對讀操作和寫操作進行分別鎖定,這樣對於性能有一定的提升。 讀寫鎖,對於多個寫操作,以及寫操作和讀操作之前都是互斥的這一點基本等同於互斥鎖。 但是對於同時多個讀操作之前卻非互斥關係,這也是相讀寫鎖性能高於互斥鎖的主要原因。

讀寫鎖也是開箱即用型的

var rwm = sync.RWMutex

讀寫鎖分為寫鎖和讀鎖:

  • 寫鎖定和寫解鎖
rwm.Lock()
rwm.Unlock()
  • 讀鎖定和讀解鎖
rwm.RLock()
rwm.RUnlock()

讀寫鎖的讀鎖和寫鎖不能交叉相互解鎖,否則會發生panic,如:

func rwMutex() {
var rwm sync.RWMutex

rwm.Lock()
fmt.Println("locked")
rwm.RUnlock()
}

fatal error: sync: RUnlock of unlocked RWMutex

對於讀寫鎖,同一資源可以同時有多個讀鎖定,如:

func rwMutex() {
var rwm sync.RWMutex

rwm.RLock()
rwm.RLock()
rwm.RLock()
fmt.Println("locked")
rwm.RUnlock()
rwm.RUnlock()
rwm.RUnlock()
}

但對於寫鎖定只能有一個(和互斥鎖相同),同時使用多個會產生deadlock的panic,如:

func rwMutex() {
var rwm sync.RWMutex

rwm.Lock()
rwm.Lock()
rwm.Lock()
fmt.Println("locked")
rwm.Unlock()
rwm.Unlock()
rwm.Unlock()
}

在goroutine中,寫解鎖會試圖喚醒所有想要進行讀鎖定而被阻塞的goroutine。

而讀解鎖會在已無任何讀鎖定的情況下,試圖喚醒一個想進行寫鎖定而被阻塞的goroutine。

下面看一個完整示例:

func rwMutex() {
var rwm sync.RWMutex

for i := 0; i <= 2; i++ {
go func(i int) {
fmt.Printf("go(%d) start lock\\n", i)
rwm.RLock()
fmt.Printf("go(%d) locked\\n", i)
time.Sleep(time.Second * 2)
rwm.RUnlock()
fmt.Printf("go(%d) unlock\\n", i)
}(i)
}
// 先sleep一小會,保證for的goroutine都會執行
time.Sleep(time.Microsecond * 100)
fmt.Println("main start lock")
// 當子進程都執行時,且子進程所有的資源都已經Unlock了
// 父進程才會執行
rwm.Lock()
fmt.Println("main locked")
time.Sleep(time.Second)
rwm.Unlock()
}
go(0) start lock
go(0) locked
go(1) start lock
go(1) locked
go(2) start lock
go(2) locked
main start lock
go(2) unlock
go(0) unlock
go(1) unlock
main locked

反覆執行上述示例中,可以看到,寫鎖定會阻塞goroutine 最開始先在main中sleep 100ms ,保證子的goroutine會全部執行,而每個子goroutine會sleep 2s。 此時會阻塞整個main進程,當所有子goroutine執行結束,讀解鎖後,main的寫鎖定才會執行。

再看一個讀鎖定示例:

func rwMutex5() {
var rwm sync.RWMutex

for i := 0; i <= 2; i++ {
go func(i int) {
fmt.Printf("go(%d) start lock\\n", i)
rwm.RLock()
fmt.Printf("go(%d) locked\\n", i)
time.Sleep(time.Second * 2)
rwm.RUnlock()
fmt.Printf("go(%d) unlock\\n", i)
}(i)
}

fmt.Println("main start lock")
rwm.RLock()
fmt.Println("main locked")
time.Sleep(time.Second * 10)
}
main start lock
main locked
go(1) start lock
go(1) locked
go(2) start lock
go(2) locked
go(0) start lock
go(0) locked
go(0) unlock
go(1) unlock
go(2) unlock

可以看到讀鎖定卻並不會阻塞goroutine。

總結:

  • 讀鎖定和寫鎖定對於寫操作都是互斥的
  • 讀鎖定支持多級嵌套,但寫鎖定無法嵌套執行
  • 如果有寫鎖定,當多個讀解鎖全部執行完成後,則會喚起執行寫鎖定
  • 寫鎖定會阻塞goroutine(在Lock()時和互斥鎖一樣,RLock()時先也是等到RUnlock()先執行,才有鎖定機會)



分享到:


相關文章: