上一篇文章我们介绍了怎么使用 Mutex 来控制计数器在高并发情况下的准确性。
但是,很明显那种方式在性能上并不算完美。
有一种 10CM 的管道,突然被缩小到了 1CM 的感觉,非常影响性能。
那有没有更好的解决方案呢?
肯定是有的,那就是读写锁。
什么是读写锁?
我们在使用 MySQL 时,在高并发情况下,也会考虑将读写进行分离;
毕竟大部分都是在读数据,写数据的时候相比读时候是非常少的。
我们在处理线程也是这样的逻辑,上一节课的计数器,我可以把读和写分离开来;
让读可以并发进行,但是写时就必须限制只能进行“单线程”
操作。
RWMutex
在 Go 语言中,标准库有一个 RWMutex 模块,他就能支持读写分离。
RWMutex 对读锁不排斥,对写锁排斥,同一时刻只能有一个写锁持有但允许多个多读锁持有,因为多个读者并不会改变共享数据,但存在写者时数据会被改变,此时读者阻塞。
方法
- Lock/Unlock:写操作时调用的方法。 如果锁已经被 reader 或者 writer 持有,那么 Lock 方法会一直阻塞,直到能获取到锁; Unlock 则是配对的释放锁的方法。
- RLock/RUnlock:读操作时调用的方法。 如果锁已经被 writer 持有的话,RLock 方法会一直阻塞,直到能获取到锁,否则就直接返回; RUnlock 是 reader 释放锁的方法。
- RLocker:这个方法的作用是为读操作返回一个 Locker 接口的对象。 它的 Lock 方法会调用 RWMutex 的 RLock 方法; 它的 Unlock 方法会调用 RWMutex 的 RUnlock 方法。
这里面我们用的比较多的是前两者,第三个用得不是特别多。
上代码
我们现在对上一篇文章的代码进行调整:
代码语言:javascript复制type DownInfo struct {
DownCount int
sync.RWMutex
}
func NewDownInfo() *DownInfo {
return &DownInfo{}
}
// AddCount 使用写锁保护
func (this *DownInfo) AddCount() {
this.Lock()
this.DownCount = this.DownCount 1
this.Unlock()
}
// GetCount 使用读锁保护
func (this *DownInfo) GetCount() int {
this.RLock()
defer this.RUnlock()
return this.DownCount
}
我们把之前的 Mutex 换成了 RWMutex !
这样的话,我们读数据时就支持并发,而写操作依旧还是 “单线程” 的方式,性能一下就上去了!
读操作为啥还要加锁?
在高并发情况下,你并不知道你此时取的值,是否就是最新的值,所以我们需要加锁让每次取的值是最新的值。
适用场景:
如果你遇到可以明确区分 reader 和 writer goroutine 的场景,且有大量的并发读、少量的并发写,并且有强烈的性能需求,你就可以考虑使用读写锁 RWMutex 替换 Mutex。
RWMutex 原理
哎,又是原理...
读写锁的设计和实现分成三类。
- Read-preferring:读优先的设计可以提供很高的并发性; 但是,在竞争激烈的情况下可能会导致写饥饿。 这是因为,如果有大量的读,这种设计会导致只有所有的读都释放了锁之后,写才可能获取到锁。
- Write-preferring:写优先的设计意味着,如果已经有一个 writer 在等待请求锁的话,它会阻止新来的请求锁的 reader 获取到锁,所以优先保障 writer。 当然,如果有一些 reader 已经请求了锁的话,新请求的 writer 也会等待已经存在的 reader 都释放锁之后才能获取。 所以,写优先级设计中的优先权是针对新来的请求而言的。 这种设计主要避免了 writer 的饥饿问题。
- 不指定优先级:这种设计比较简单,不区分 reader 和 writer 优先级。 某些场景下这种不指定优先级的设计反而更有效,因为第一类优先级会导致写饥饿,第二类优先级可能会导致读饥饿,这种不指定优先级的访问不再区分读写,大家都是同一个优先级,解决了饥饿的问题。
Go 标准库中的 RWMutex 设计是 Write-preferring 方案,一个正在阻塞的 Lock 调用会排除新的 reader 请求到锁。
最容易入的坑
在使用读写锁的时候,最容易出现的坑就是 重入导致死锁
!
先来看一段代码:
代码语言:javascript复制func one(l *sync.RWMutex) {
fmt.Println("in one")
l.Lock()
two(l)
l.Unlock()
}
func two(l *sync.RWMutex) {
l.Lock()
fmt.Println("in two")
l.Unlock()
}
func main() {
l := &sync.RWMutex{}
one(l)
}
我们在 main 方法里面创建了一个读写锁,然后传给了 one 方法,然后 one 方法里面加了一个 Lock,然后调用 two 方法,two 方面里面又重复上 Lock。
这就是 重入导致死锁
!
但是这种错误,还是比较容易发现了,运行会报错!
另外,「Golang全栈」读者交流群成立了,聊天学习摸鱼为主,有一群有趣有料的小伙伴在等你哦!