近日看了一篇 文章留拾,講到了用鎖的 panic 問題涕侈,但并沒有看懂羹奉,經(jīng)過多次測試秒旋,整理如下。
Golang 中的鎖
Golang 中的有兩種鎖诀拭,為 sync.Mutex
和 sync.RWMutex
迁筛。
-
sync.Mutex
的鎖只有一種鎖:Lock()
,它是絕對鎖耕挨,同一時間只能有一個鎖细卧。 -
sync.RWMutex
叫讀寫鎖,它有兩種鎖:RLock()
和Lock()
:-
RLock()
叫讀鎖筒占。它不是絕對鎖贪庙,可以有多個讀者同時獲取此鎖(調(diào)用mu.RLock
)。 -
Lock()
叫寫鎖翰苫,它是個絕對鎖止邮,就是說,如果一旦某人拿到了這個鎖奏窑,別人就不能再獲取此鎖了导披。
-
另外,有一種特性:
- 當寫鎖阻塞時埃唯,新的讀鎖是無法申請的撩匕。
即在 sync.RWMutex
的使用中,一個線程請求了他的寫鎖(mx.Lock()
)后墨叛,即便它還沒有取到該鎖(可能由于資源已被其他人鎖定)止毕,后面所有的讀鎖的申請模蜡,都將被阻塞,只有取寫鎖的請求得到了鎖且用完釋放后滓技,讀鎖才能去取哩牍。
這種特性可以有效防止寫者 饑餓。如果一個線程因為某種原因令漂,導致得不到CPU運行時間膝昆,這種狀態(tài)被稱之為 饑餓。
另外叠必,由上面的基礎(chǔ)又衍生出一些想法并測試了一下荚孵,結(jié)果如下:
- 讀寫鎖中的可讀鎖(
sync.RWMutex
的RLock()
)可以嵌套使用的,在一個線程中單獨來看纬朝,它不會有問題(但這是踩坑點)收叶。 - 互斥鎖(
sync.Mutex
和sync.RWMutex
的Lock()
)是不可以互相嵌套的,這是明顯的死鎖共苛。 -
sync.RWMutex
的Lock()
不可以使用與其RLock()
也不可以互相嵌套判没,這也是明顯的死鎖。
本篇文章的所有 嵌套 一詞均指同一個資源的鎖的嵌套隅茎。即澄峰,指一個 goroutine 在對某個資源上鎖(調(diào)用
(R)Lock()
)后解鎖 (調(diào)用(R)Unlock()
) 前,再次上鎖(調(diào)用(R)Lock()
)辟犀。(l.RLock()
->l.RLock()
->l.RUnlock()
->l.RUnlock()
)
當 死鎖 發(fā)生時俏竞,系統(tǒng)就會報一個運行時錯誤
fatal error: all goroutines are asleep - deadlock!
。可以這樣通俗地解釋這個錯誤發(fā)生的原因:一個 goroutine 請求的資源被他人鎖住堂竟,就等待它被釋放魂毁,但檢測到程序中沒有其他 goroutine 在執(zhí)行了,或者其他 goroutine 也都在等待這個鎖被某人釋放出嘹,這樣它就知道了自己永遠不會拿到這個鎖了席楚,便拋出了此死鎖的錯誤。
如下文的例子中在
10s passed
輸出后税稼,其才會報出死鎖的錯誤酣胀。
踩坑點
有些死鎖是很容易發(fā)現(xiàn)的,比如在 Lock()
自身的互相嵌套及 Lock()
與 RLock()
的互相嵌套娶聘。
但有一種情況的死鎖不容易發(fā)現(xiàn):在嵌套使用 RLock()
時闻镶,它本身一個協(xié)程不會報錯,但當其他 goroutine 在使用 Lock()
時丸升,則有可能發(fā)生死鎖铆农。
所以為避免踩到這種坑,最好的建議就是 不要嵌套地使用 RLock()
實例與解釋
package main
import (
"fmt"
"sync"
"time"
)
var l sync.RWMutex
func main() {
go readAndRead()
time.Sleep(1 * time.Second)
l.Lock()
fmt.Println("----------------- got lock")
l.Unlock()
time.Sleep(5 * time.Second)
}
func readAndRead() {
l.RLock()
fmt.Println("----------------- got rlock")
time.Sleep(10 * time.Second)
fmt.Println("----------------- 10s passed")
l.RLock()
fmt.Println("----------------- got 2nd rlock")
l.RUnlock()
l.RUnlock()
}
/* shell 執(zhí)行 `go run main.go` 的結(jié)果為:
----------------- got rlock
----------------- 10s passed
fatal error: all goroutines are asleep - deadlock!
...
*/
上面的實例就會發(fā)生死鎖,詳細解釋其死鎖的過程如下:
- A(goroutine
readAndRead()
)先獲取了讀鎖 - B (主程序)申請寫鎖的獲取墩剖,此時由于 A 加了讀鎖猴凹,因此寫鎖阻塞(等待 A 釋放讀鎖)
- 此時 A 中又申請了讀鎖(嵌套,A 還沒有釋放前一次獲取的讀鎖)
- 這時岭皂,由于 A 對讀鎖的申請一定會等待 B 獲取到鎖并釋放后才能得到郊霎,所以 A 和 B 都在等待鎖(A 第一次獲取到了但沒釋放,第二次的獲取卻排在了 B 的后面)爷绘。造成死鎖發(fā)生书劝。
代碼的解釋,這些條件保證了我上面的過程穩(wěn)定重現(xiàn)(可以試著打破這個過程看還會不會出錯):
-
readAndRead()
函數(shù)是在 goroutine 中執(zhí)行的土至,它會嵌套地獲取讀鎖购对。(A) - 主程序中會獲取寫鎖。(B)
- 為了保證 A 先獲取到讀鎖陶因,用了
time.Sleep(1 * time.Second)
骡苞,來切換時間片(或用runtime.Gosched()
),從而保證 A 和 B 兩個同時停在獲取鎖的狀態(tài)上楷扬。
總結(jié)
再次總結(jié)一下解幽,
- 正常情況下,在請求
Lock()
鎖時發(fā)現(xiàn)資源被鎖住了烘苹,無論是RLock()
鎖還是Lock()
鎖亚铁,它都會等待。 - 正常情況下螟加,在請求
RLock()
鎖時發(fā)現(xiàn)資源被Lock()
鎖住了,它會等待吞琐。發(fā)現(xiàn)是被RLock()
鎖住捆探,自己也可以讀取。(這個是用數(shù)字的原子操作來控制的站粟,原理見附的文章的源碼解釋) - 不要嵌套地去用
鎖
黍图,這樣則有可能發(fā)生死鎖,即大家(所有 goroutine)都在等待鎖的釋放奴烙,此時發(fā)生死鎖助被。
參考附:
注: 測試時注意 goroutine 的 panic 可能還沒發(fā)生,主程序就退出了(goroutine 的 panic 發(fā)生時切诀,會導致主程序也退出并輸出 panic 信息)揩环。