读写锁实际是对读写操作进行加锁。需要注意的是多个读操作之间不存在互斥关系,这样提高了对共享资源的访问效率。
先看一个例子:
package main
import (
"sync"
"time"
)
var mu sync.RWMutex
func A() {
fmt.Println("a call rlock")
mu.RLock()
fmt.Println("a get rlock")
defer mu.RUnlock()
B()
}
func B() {
time.Sleep(time.Second*5)
C()
}
func C() {
fmt.Println("c call rlock")
mu.RLock()
fmt.Println("c get rlocked")
defer mu.RUnlock()
}
func main() {
go A()
time.Sleep(time.Second)
fmt.Println("main call wlock")
mu.Lock()
fmt.Println("main get wlock")
defer mu.Unlock()
}
观察这段代码,你觉得输出结果是什么?正常退出还是死锁?
没错,会死锁!
我们先看打印结果
可以看到,main和c函数在请求锁的时候都失败了,而此时锁被a占有,a又等待b返回,b等待c获得锁之后发返回,因此产生死锁。
下面解释下为什么mian和c为什么没有得到锁。
首先,我们要明确读写锁的基本特性
- 可以同时进行多个协程读操作,不允许写操作
- 只允许同时有一个协程进行写操作,不允许其他写操作和读操作
- 竞争读写锁的时候,写优先级高于读
将这三个特性联系起来分析下:
a获得读锁之后,在调用b时等待5s,此时main函数已经等待1s需要请求写锁,命中第一条。因此控制台不会打印main get wlock。此时main一直等待读锁释放。
c为需要读锁,我们知道写锁优先级高于读锁,在请求读写锁时,main的优先级高于c,命中第三条,因此c不会获得锁然后释放锁产生死循环。
在golagn中使用递归的方式使用读写锁极其容易造成死锁而引起panic,这种使用十分危险!当逻辑复杂,嵌套过深,这种问题排查十分困难,因此在实际应用中要避免这种递归使用。
“It should not be used for recursive read locking”
:P