天天看點

c語言 讀寫鎖,Go 互斥鎖(sync.Mutex)和 讀寫鎖(sync.RWMutex)

什麼時候需要用到鎖?

當程式中就一個線程的時候,是不需要加鎖的,但是通常實際的代碼不會隻是單線程,是以這個時候就需要用到鎖了,那麼關于鎖的使用場景主要涉及到哪些呢?

多個線程在讀相同的資料時

多個線程在寫相同的資料時

同一個資源,有讀又有寫

互斥鎖(sync.Mutex)

互斥鎖是一種常用的控制共享資源通路的方法,它能夠保證同時隻有一個 goroutine 可以通路到共享資源(同一個時刻隻有一個線程能夠拿到鎖)

先通過一個并發讀寫的例子示範一下,當多線程同時通路全局變量時,結果會怎樣?

package main

import ("fmt")

var count int

func main() {

for i := 0; i < 2; i++ {

go func() {

for i := 1000000; i > 0; i-- {

count ++

}

fmt.Println(count)

}()

}

fmt.Scanf("\n") //等待子線程全部結束

}

運作結果:

980117

1011352 //最後的結果基本不可能是我們想看到的:200000

修改代碼,在累加的地方添加互斥鎖,就能保證我們每次得到的結果都是想要的值

c語言 讀寫鎖,Go 互斥鎖(sync.Mutex)和 讀寫鎖(sync.RWMutex)
c語言 讀寫鎖,Go 互斥鎖(sync.Mutex)和 讀寫鎖(sync.RWMutex)

package main

import ("fmt"

"sync")

var (

countintlock sync.Mutex

)

func main() {

for i := 0; i < 2; i++{

go func() {

for i := 1000000; i > 0; i--{

lock.Lock()

count++lock.Unlock()

}

fmt.Println(count)

}()

}

fmt.Scanf("\n") //等待子線程全部結束

}

運作結果:1952533

2000000 //最後的線程列印輸出

View Code

讀寫鎖(sync.RWMutex)

在讀多寫少的環境中,可以優先使用讀寫互斥鎖(sync.RWMutex),它比互斥鎖更加高效。sync 包中的 RWMutex 提供了讀寫互斥鎖的封裝

讀寫鎖分為:讀鎖和寫鎖

如果設定了一個寫鎖,那麼其它讀的線程以及寫的線程都拿不到鎖,這個時候,與互斥鎖的功能相同

如果設定了一個讀鎖,那麼其它寫的線程是拿不到鎖的,但是其它讀的線程是可以拿到鎖

通過設定寫鎖,同樣可以實作資料的一緻性:

package main

import ("fmt"

"sync"

)

var (

count int

rwLock sync.RWMutex

)

func main() {

for i := 0; i < 2; i++ {

go func() {

for i := 1000000; i > 0; i-- {

rwLock.Lock()

count ++

rwLock.Unlock()

}

fmt.Println(count)

}()

}

fmt.Scanf("\n") //等待子線程全部結束

}

運作結果:

1968637

2000000

互斥鎖和讀寫鎖的性能對比

demo:制作一個讀多寫少的例子,分别開啟 3 個 goroutine 進行讀和寫,輸出最終的讀寫次數

1)使用互斥鎖:

package main

import (

"fmt"

"sync"

"time"

)

var (

count int

//互斥鎖

countGuard sync.Mutex

)

func read(mapA map[string]string){

for {

countGuard.Lock()

var _ string = mapA["name"]

count += 1

countGuard.Unlock()

}

}

func write(mapA map[string]string) {

for {

countGuard.Lock()

mapA["name"] = "johny"

count += 1

time.Sleep(time.Millisecond * 3)

countGuard.Unlock()

}

}

func main() {

var num int = 3

var mapA map[string]string = map[string]string{"nema": ""}

for i := 0; i < num; i++ {

go read(mapA)

}

for i := 0; i < num; i++ {

go write(mapA)

}

time.Sleep(time.Second * 3)

fmt.Printf("最終讀寫次數:%d\n", count)

}

運作結果:

最終讀寫次數:3766

2)使用讀寫鎖

package main

import (

"fmt"

"sync"

"time"

)

var (

count int

//讀寫鎖

countGuard sync.RWMutex

)

func read(mapA map[string]string){

for {

countGuard.RLock() //這裡定義了一個讀鎖

var _ string = mapA["name"]

count += 1

countGuard.RUnlock()

}

}

func write(mapA map[string]string) {

for {

countGuard.Lock() //這裡定義了一個寫鎖

mapA["name"] = "johny"

count += 1

time.Sleep(time.Millisecond * 3)

countGuard.Unlock()

}

}

func main() {

var num int = 3

var mapA map[string]string = map[string]string{"nema": ""}

for i := 0; i < num; i++ {

go read(mapA)

}

for i := 0; i < num; i++ {

go write(mapA)

}

time.Sleep(time.Second * 3)

fmt.Printf("最終讀寫次數:%d\n", count)

}

運作結果:

最終讀寫次數:8165

結果差距大概在 2 倍左右,讀鎖的效率要快很多!

關于互斥鎖的補充

互斥鎖需要注意的問題:

不要重複鎖定互斥鎖

不要忘記解鎖互斥鎖,必要時使用 defer 語句

不要在多個函數之間直接傳遞互斥鎖

死鎖: 目前程式中的主 goroutine 以及我們啟用的那些 goroutine 都已經被阻塞,這些 goroutine 可以被稱為使用者級的 goroutine 這就相當于整個程式已經停滞不前了,并且這個時候 go 程式會抛出如下的 panic:

fatal error: all goroutines are asleep - deadlock!

并且go語言運作時系統抛出自行抛出的panic都屬于緻命性錯誤,都是無法被恢複的,調用recover函數對他們起不到任何作用

Go語言中的互斥鎖是開箱即用的,也就是我們聲明一個sync.Mutex 類型的變量,就可以直接使用它了,需要注意:該類型是一個結構體類型,屬于值類型的一種,将它當做參數傳給一個函數,将它從函數中傳回,把它指派給其他變量,讓它進入某個管道,都會導緻他的副本的産生。并且原值和副本以及多個副本之間是完全獨立的,他們都是不同的互斥鎖,是以不應該将鎖通過函數的參數進行傳遞

關于讀寫鎖的補充

1、在寫鎖已被鎖定的情況下再次試圖鎖定寫鎖,會阻塞目前的goroutine

2、在寫鎖已被鎖定的情況下再次試圖鎖定讀鎖,也會阻塞目前的goroutine

3、在讀鎖已被鎖定的情況下試圖鎖定寫鎖,同樣會阻塞目前的goroutine

4、在讀鎖已被鎖定的情況下再試圖鎖定讀鎖,并不會阻塞目前的goroutine

對于某個受到讀寫鎖保護的共享資源,多個寫操作不能同時進行,寫操作和讀操作也不能同時進行,但多個讀操作卻可以同時進行

對寫鎖進行解鎖,會喚醒“所有因試圖鎖定讀鎖,而被阻塞的goroutine”, 并且這個通常會使他們都成功完成對讀鎖的鎖定(這個還不了解)

對讀鎖進行解鎖,隻會在沒有其他讀鎖鎖定的前提下,喚醒“因試圖鎖定寫鎖,而被阻塞的 goroutine” 并且隻會有一個被喚醒的 goroutine 能夠成功完成對寫鎖的鎖定,其他的 goroutine 還要在原處繼續等待,至于哪一個goroutine,那麼就要看誰等待的事件最長

解鎖讀寫鎖中未被鎖定的寫鎖, 會立即引發panic ,對其中的讀鎖也是如此,并且同樣是不可恢複的

參考連結:https://www.cnblogs.com/zhaof/p/8636384.html

ending ~