近日看了一篇 文章,讲到了用锁的 panic 问题,但并没有看懂,经过多次测试,整理如下。
Golang 中的锁
Golang 中的有两种锁,为 sync.Mutex
和 sync.RWMutex
。
-
sync.Mutex
的锁只有一种锁:Lock()
,它是绝对锁,同一时间只能有一个锁。 -
sync.RWMutex
叫读写锁,它有两种锁:RLock()
和Lock()
:-
RLock()
叫读锁。它不是绝对锁,可以有多个读者同时获取此锁(调用mu.RLock
)。 -
Lock()
叫写锁,它是个绝对锁,就是说,如果一旦某人拿到了这个锁,别人就不能再获取此锁了。
-
另外,有一种特性:
- 当写锁阻塞时,新的读锁是无法申请的。
即在 sync.RWMutex
的使用中,一个线程请求了他的写锁(mx.Lock()
)后,即便它还没有取到该锁(可能由于资源已被其他人锁定),后面所有的读锁的申请,都将被阻塞,只有取写锁的请求得到了锁且用完释放后,读锁才能去取。
这种特性可以有效防止写者 饥饿。如果一个线程因为某种原因,导致得不到CPU运行时间,这种状态被称之为 饥饿。
另外,由上面的基础又衍生出一些想法并测试了一下,结果如下:
- 读写锁中的可读锁(
sync.RWMutex
的RLock()
)可以嵌套使用的,在一个线程中单独来看,它不会有问题(但这是踩坑点)。 - 互斥锁(
sync.Mutex
和sync.RWMutex
的Lock()
)是不可以互相嵌套的,这是明显的死锁。 -
sync.RWMutex
的Lock()
不可以使用与其RLock()
也不可以互相嵌套,这也是明显的死锁。
本篇文章的所有 嵌套 一词均指同一个资源的锁的嵌套。即,指一个 goroutine 在对某个资源上锁(调用
(R)Lock()
)后解锁 (调用(R)Unlock()
) 前,再次上锁(调用(R)Lock()
)。(l.RLock()
->l.RLock()
->l.RUnlock()
->l.RUnlock()
)
当 死锁 发生时,系统就会报一个运行时错误
fatal error: all goroutines are asleep - deadlock!
。可以这样通俗地解释这个错误发生的原因:一个 goroutine 请求的资源被他人锁住,就等待它被释放,但检测到程序中没有其他 goroutine 在执行了,或者其他 goroutine 也都在等待这个锁被某人释放,这样它就知道了自己永远不会拿到这个锁了,便抛出了此死锁的错误。
如下文的例子中在
10s passed
输出后,其才会报出死锁的错误。
踩坑点
有些死锁是很容易发现的,比如在 Lock()
自身的互相嵌套及 Lock()
与 RLock()
的互相嵌套。
但有一种情况的死锁不容易发现:在嵌套使用 RLock()
时,它本身一个协程不会报错,但当其他 goroutine 在使用 Lock()
时,则有可能发生死锁。
所以为避免踩到这种坑,最好的建议就是 不要嵌套地使用 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 执行 `go run main.go` 的结果为:
----------------- got rlock
----------------- 10s passed
fatal error: all goroutines are asleep - deadlock!
...
*/
上面的实例就会发生死锁,详细解释其死锁的过程如下:
- A(goroutine
readAndRead()
)先获取了读锁 - B (主程序)申请写锁的获取,此时由于 A 加了读锁,因此写锁阻塞(等待 A 释放读锁)
- 此时 A 中又申请了读锁(嵌套,A 还没有释放前一次获取的读锁)
- 这时,由于 A 对读锁的申请一定会等待 B 获取到锁并释放后才能得到,所以 A 和 B 都在等待锁(A 第一次获取到了但没释放,第二次的获取却排在了 B 的后面)。造成死锁发生。
代码的解释,这些条件保证了我上面的过程稳定重现(可以试着打破这个过程看还会不会出错):
-
readAndRead()
函数是在 goroutine 中执行的,它会嵌套地获取读锁。(A) - 主程序中会获取写锁。(B)
- 为了保证 A 先获取到读锁,用了
time.Sleep(1 * time.Second)
,来切换时间片(或用runtime.Gosched()
),从而保证 A 和 B 两个同时停在获取锁的状态上。
总结
再次总结一下,
- 正常情况下,在请求
Lock()
锁时发现资源被锁住了,无论是RLock()
锁还是Lock()
锁,它都会等待。 - 正常情况下,在请求
RLock()
锁时发现资源被Lock()
锁住了,它会等待。发现是被RLock()
锁住,自己也可以读取。(这个是用数字的原子操作来控制的,原理见附的文章的源码解释) - 不要嵌套地去用
锁
,这样则有可能发生死锁,即大家(所有 goroutine)都在等待锁的释放,此时发生死锁。
![](https://img.haomeiwen.com/i3491218/bb49b13683f3c84c.jpg)
![](https://img.haomeiwen.com/i3491218/c5c8b68cdc7294d0.jpg)
参考附:
注: 测试时注意 goroutine 的 panic 可能还没发生,主程序就退出了(goroutine 的 panic 发生时,会导致主程序也退出并输出 panic 信息)。
网友评论