面试官:说说RWMutex与Mutex的区别

2022-05-10 08:59:19 浏览数 (1)

上一篇文章我们介绍了怎么使用 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全栈」读者交流群成立了,聊天学习摸鱼为主,有一群有趣有料的小伙伴在等你哦!

0 人点赞