读写锁实际是对读写操作进行加锁。需要注意的是多个读操作之间不存在互斥关系,这样提高了对共享资源的访问效率。

先看一个例子:

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