簡述
Golang中的鎖機制主要包含互斥鎖和讀寫鎖
互斥鎖
互斥鎖是傳統(tǒng)并發(fā)程序?qū)蚕碣Y源進行控制訪問的主要手段。在Go中主要使用 sync.Mutex的結(jié)構(gòu)體表示。
一個簡單的示例:
func mutex() { var mu sync.Mutex mu.Lock() fmt.Println("locked") mu.Unlock() }
或者也可以使用defer來實現(xiàn),這在整個函數(shù)流程中全部要加鎖時特別有用,還有一個好處就是可以防止忘記Unlock
func mutex() { var mu sync.Mutex mu.Lock() defer mu.Unlock() fmt.Println("locked") }
互斥鎖是開箱即用的,只需要申明sync.Mutex即可直接使用
var mu sync.Mutex
互斥鎖應該是成對出現(xiàn),在同步語句不可以再對鎖加鎖,看下面的示例:
func mutex() { var mu sync.Mutex mu.Lock() fmt.Println("parent locked") mu.Lock() fmt.Println("sub locked") mu.Unlock() mu.Unlock() }
此時則會出現(xiàn)fatal error: all goroutines are asleep - deadlock!錯誤
同樣,如果多次對一個鎖解鎖,則會出現(xiàn)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 startn", i) mu.Lock() fmt.Printf("sub(%d) lockedn", i) time.Sleep(time.Microsecond * 30) mu.Unlock() fmt.Printf("sub(%d) unlockn", i) }(i) } time.Sleep(time.Second * 2) mu.Unlock() fmt.Println("parent unlock") time.Sleep(time.Second * 2) }
先看上面的函數(shù)執(zhí)行結(jié)果
parent lock start parent locked sub(0) lock start sub(2) lock start sub(1) lock start parent unlock // 必須等到父級先解鎖,后面則會阻塞 sub(0) locked // 解鎖后子goroutine才能執(zhí)行鎖定 sub(0) unlock sub(2) locked sub(2) unlock sub(1) locked sub(1) unlock
為了方便調(diào)試,使用了time.Sleep()來延遲保證goroutine的執(zhí)行 從結(jié)果中可以看出,當所有的goroutine遇到Lock時都會阻塞,而當main函數(shù)中的Unlock執(zhí)行后,會有一個優(yōu)先(無序)的goroutine來占得鎖,其它的則再次進入阻塞狀態(tài)。
總結(jié):
- 互斥鎖必須成對出現(xiàn)
- 同級別互斥鎖不能嵌套使用
- 父級中如果存在鎖,當在goroutine中執(zhí)行重復鎖定操作時goroutine將被阻塞,直到原互斥鎖解鎖,多個goroutine將會爭搶當前鎖資源,其它繼續(xù)阻塞。

讀寫鎖
讀寫鎖和互斥鎖不同之處在于,可以分別針對讀操作和寫操作進行分別鎖定,這樣對于性能有一定的提升。 讀寫鎖,對于多個寫操作,以及寫操作和讀操作之前都是互斥的這一點基本等同于互斥鎖。 但是對于同時多個讀操作之前卻非互斥關(guān)系,這也是相讀寫鎖性能高于互斥鎖的主要原因。
讀寫鎖也是開箱即用型的
var rwm = sync.RWMutex
讀寫鎖分為寫鎖和讀鎖:
- 寫鎖定和寫解鎖
rwm.Lock() rwm.Unlock()
- 讀鎖定和讀解鎖
rwm.RLock() rwm.RUnlock()
讀寫鎖的讀鎖和寫鎖不能交叉相互解鎖,否則會發(fā)生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() }
但對于寫鎖定只能有一個(和互斥鎖相同),同時使用多個會產(chǎn)生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 lockn", i) rwm.RLock() fmt.Printf("go(%d) lockedn", i) time.Sleep(time.Second * 2) rwm.RUnlock() fmt.Printf("go(%d) unlockn", i) }(i) } // 先sleep一小會,保證for的goroutine都會執(zhí)行 time.Sleep(time.Microsecond * 100) fmt.Println("main start lock") // 當子進程都執(zhí)行時,且子進程所有的資源都已經(jīng)Unlock了 // 父進程才會執(zhí)行 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
反復執(zhí)行上述示例中,可以看到,寫鎖定會阻塞goroutine 最開始先在main中sleep 100ms ,保證子的goroutine會全部執(zhí)行,而每個子goroutine會sleep 2s。 此時會阻塞整個main進程,當所有子goroutine執(zhí)行結(jié)束,讀解鎖后,main的寫鎖定才會執(zhí)行。
再看一個讀鎖定示例:
func rwMutex5() { var rwm sync.RWMutex for i := 0; i <= 2; i++ { go func(i int) { fmt.Printf("go(%d) start lockn", i) rwm.RLock() fmt.Printf("go(%d) lockedn", i) time.Sleep(time.Second * 2) rwm.RUnlock() fmt.Printf("go(%d) unlockn", 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。
總結(jié):
- 讀鎖定和寫鎖定對于寫操作都是互斥的
- 讀鎖定支持多級嵌套,但寫鎖定無法嵌套執(zhí)行
- 如果有寫鎖定,當多個讀解鎖全部執(zhí)行完成后,則會喚起執(zhí)行寫鎖定
- 寫鎖定會阻塞goroutine(在Lock()時和互斥鎖一樣,RLock()時先也是等到RUnlock()先執(zhí)行,才有鎖定機會)