什么時候需要用到鎖?
當程序中就一個線程的時候,是不需要加鎖的,但是通常實際的代碼不會只是單線程,所以這個時候就需要用到鎖了,那么關於鎖的使用場景主要涉及到哪些呢?
- 多個線程在讀相同的數據時
- 多個線程在寫相同的數據時
- 同一個資源,有讀又有寫
互斥鎖(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 ( count int lock 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 //最后的線程打印輸出
讀寫鎖(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 ~