剖析Go的读写锁
package main
import (
"fmt"
"sync"
"time"
)
func main() {
rw := new(sync.RWMutex)
for i := 0; i < 2; i++ { // 建立两个写者
go func() {
for j := 0; j < 3; j++ {
rw.Lock()
// 写
rw.Unlock()
}
}()
}
for i := 0; i < 5; i++ { // 建立两个读者
go func() {
for j := 0; j < 3; j++ {
rw.RLock()
// 读
rw.RUnlock()
}
}()
}
time.Sleep(time.Second)
fmt.Println("Done")
}
PlayGround
一个(神奇)优秀的(大坑)特性
读者在读的时候,不能够假定别的读者也能够获得锁。因此,禁止读锁嵌套。
是不是有点儿绕?下面举个“七秒例”:?
- 第一秒:读者1在第1秒成功申请了读锁
- 第二秒:写者1在第2秒申请写锁,申请失败,阻塞,但它会防止新的读者获锁
- 第三秒:读者2在第3秒申请读锁,申请失败
- 第四秒:读者1释放读锁,写者1获得写锁
- 第五秒:写者1释放写锁,读者2获得读锁
- 第六秒:读者1再次申请读锁,申请成功,与读者2共享
- 第七秒:读者1、读者2释放读锁,结束
当写锁阻塞时,新的读锁是无法申请的,这可以有效防止写者饥饿。如果一个线程因为某种原因,导致得不到CPU运行时间,这种状态被称之为 饥饿。
然而,这种机制也禁止了读锁嵌套。读锁嵌套可能造成死锁:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
rw := new(sync.RWMutex)
var deadLockCase time.Duration = 1
go func() {
time.Sleep(time.Second * deadLockCase)
fmt.Println("Writer Try")
rw.Lock()
fmt.Println("Writer Fetch")
time.Sleep(time.Second * 1)
fmt.Println("Writer Release")
rw.Unlock()
}()
fmt.Println("Reader 1 Try")
rw.RLock()
fmt.Println("Reader 1 Fetch")
time.Sleep(time.Second * 2)
fmt.Println("Reader 2 Try")
rw.RLock()
fmt.Println("Reader 2 Fetch")
time.Sleep(time.Second * 2)
fmt.Println("Reader 1 Release")
rw.RUnlock()
time.Sleep(time.Second * 1)
fmt.Println("Reader 2 Release")
rw.RUnlock()
time.Sleep(time.Second * 2)
fmt.Println("Done")
}
读者1和读者2是嵌套关系,按照这种时间安排,上述程序会导致死锁。
而有些死锁的可怕之处就在于,它不一定会发生。假设上面程序中的time.Sleep都是随机的时间,那么这一段代码每次的结果有可能不一致,这会给Debug带来极大的困难。
吾闻读锁莫嵌套,写锁嵌套长已矣。(读锁嵌套了还有概率成功,写锁嵌套了100%完蛋?)
源码剖析
(源码具体内容、行数,以版本go version 1.8.1
为例。)
为了方便理解,可以把所有的if race.Enabled {...}
扔掉不看。接下来,我们重述“七秒例”。?
第一秒,读者1请求读锁。
Line41:
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_Semacquire(&rw.readerSem)
}
读者数量readerCount
开始是0,这个时候加1,变成了1,不符合判负条件所以跳出,成功获得读锁一枚。
第二秒,写者尝试获取写锁。第85行获取w的锁。不管这个读写锁有没有获取成功,先排斥别的写者。
Line85:
// First, resolve competition with other writers.
rw.w.Lock()
// Announce to readers there is a pending writer.
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// Wait for active readers.
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_Semacquire(&rw.writerSem)
}
刚才说了,一个写者阻塞在这里的时候,也不会让新的读者去读了,所以它干了一件非常坏的事情:
把readerCount变成了1-rwmutexMaxReaders。
这样就能卡住新来的读者了。
接下来,算出r等于1。这意味着有当前有写者存在。
因为有读者,所以写者卡在了信号量writerSem
上。但是它不甘心啊,心想“等完现在的这几个读者,我就要去写!”,它默默地把现在占有读锁的人记在了小本本rw.readerWait上。在本例子中,readerWait被设置为了1。
第三秒,读者2尝试获得读锁,它又来到了第41行,结果发现读者的数量是1-rwmutexMaxReaders,好吧,它只好卡在信号量readerSem
上。
第四秒,读者1调用RUnlock(),它首先把读者数量减一,毕竟自己已经不读了。
Line61:
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
// A writer is pending.
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem)
}
}
在读者数量减一的时候,它发现读者数量是负数,这回读者1明白了,有一个写者在等待写。估计读者1自己已经在这个写者的小本本readerWait上了,因此它把readerWait减一,表示自己不读了。这时候读者1发现自己就是最后一个读者了,所以赶紧祭出writerSem,让写者可以去写。
读者1释放了writerSem信号量以后,写者很快就收到了这个提醒,兴高采烈地获得了写锁,开始自己的写作生涯。
读者2还卡着呢…
第五秒,写者1写完了一稿便不想写了,调用Unlock()准备释放读锁。
Line114:
// Announce to readers there is no active writer.
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
// Unblock blocked readers, if any.
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem)
}
只见他重新为readerCount加上rwmutexMaxReaders,使他重新变为了正数。这个正数恰好也是阻塞的读者的数量。 接下来,写者按照这个读者的数量,释放了这么多的readerSem信号量,相当于将所有阻塞的读者一一唤醒。读者2在收到readerSem的那一刻喜极而泣,它终于可以读了。
第六秒,读者1又来了,它把读者数量加1,发现它是正数哎,写者现在又没来,它再次幸运地瞬间获得读锁,与读者2一起读了起来。
第七秒,读者1和读者2都释放了自己的读锁。至此,结束。
名词解释
中文 |
英文 |
解释 |
---|---|---|
信号量 (也称信号灯) |
Semaphore |
|
条件变量 |
Condition |
|
互斥量 |
Mutex |
参考文献
- Wikipedia: Semaphore (programming))
- hdu------(4300)Clairewd’s message(kmp)
- TensorFlow ML cookbook 第一章7、8节 实现激活功能和使用数据源
- Go语言struct类型详解
- spark1.x升级spark2如何升级及需要考虑的问题
- 使用 kubeadm 创建一个 kubernetes 集群
- Oracle 12c 多租户专题|CDB元数据内幕
- 深入分析golang多值返回以及闭包的实现
- Hadoop3.0扩展Yarn资源模型详解2:资源Profiles说明
- hdu------(1525)Euclid's Game(博弈决策树)
- Go语言指针访问结构体的方法
- Spring Boot & Spring Cloud 应用内存管理
- hdu----(1849)Rabbit and Grass(简单的尼姆博弈)
- 10分钟让你明白MySQL是如何利用索引的
- 扩展Yarn资源模型详解1
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Could not find artifact org.apache.tomcat.maven:tomcat7-maven-plugin:pom.....
- try{}catch{}finally{}运行后的执行结果
- MySQL死锁产生原因和解决方法
- 文件操作与文件夹操作
- 使用文件字节输入流FileInputStream读取文件
- 二进制与十进制与十六进制介绍+转换+图解
- Swagger 3.0 官方 starter 诞生了,其它的都可以扔了~
- 原码+反码+补码概述与示范
- HTTP客户端连接,选择HttpClient还是OkHttp?
- 数据类型(基本数据类型和引用数据类型)范围与字符转换,代码示例+个位十位百位相加面试题
- Scanner关键字的使用+代码介绍+注意事项
- 将一个txt文件,复制到另一个txt文件中(缓冲字节流(BufferedInputStream,BufferedOutputStream))
- java实现客户端服务端互发消息并接收
- 使用NIO实现非阻塞式(相对的)多人聊天室
- 三次握手与四次挥手+图解