什麼時候需要用到鎖?
當程式中就一個線程的時候,是不需要加鎖的,但是通常實際的代碼不會隻是單線程,是以這個時候就需要用到鎖了,那麼關于鎖的使用場景主要涉及到哪些呢?
多個線程在讀相同的資料時
多個線程在寫相同的資料時
同一個資源,有讀又有寫
互斥鎖(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
修改代碼,在累加的地方添加互斥鎖,就能保證我們每次得到的結果都是想要的值

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 ~