开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
上篇文章中我们已经阐述了 Go 语言中的一个同步原语言 Mutex我们通常用在共享资源的时候使用其来保证安全性。上篇我们通过 Mutex 来保证有且只有一个 goroutine 访问我们的共享资源,在某些场景下有点“浪费”。在读多写少的场景下,会有很长一段时间没有进行写操作,使用 Mutex 对性能会有影响。
其实我们可以区分读写操作,今天我们介绍另外一个 RWMutex。
若一个 goroutine 在读操作中持有来锁,此时其他的 goroutine 就不需要在等待,可以并发的访问共享的资源(变量),从而使串行变成并行读操作,提高读的性能。当写操作的 goroutine 拿到锁时,其他的读、写操作的 goroutine,此时会阻塞,需要等待写的 goroutine 释放锁。
1、RWMutex (什么是 RWMutex)
在 Go 语言标准库中 RWMutex 是一个 reader/writer 互斥锁,RWMutex 它不限制资源的并发读,但是读写、写写操作无法并行执行。
读 | 写 | |
---|---|---|
读 | Y | N |
写 | N | N |
还是举一个计数器的例子聊一下,使用 10 个 goroutine 进行读操作,每次都 sleep 1ms,另一个 goroutine 进行写操作,每秒进行一次写操作。因为读操作可以并行执行,写操作时只允许一个线程执行,这正是 readers-writers 问题。
上面例子中,Incr 方法会修改计数器的值,在写操作时会使用 Lock/Unlock 进行保护。 Count 方法会读取当前计数器的值,在读操作时会使用 RLock/RUnlock 方法进行保护。
Incr 方法每秒调用一次,在竞争锁的过程中频率还是比较低的,10 个 goroutine 每毫秒会执行一次查找,通过读写锁,可以提高程序的性能,因为可以并发的执行读写。若过使用 Mutex,在多个 reader 并发读的时候需要排队进行获取锁,自然在性能上没有 RWMutex 并发读的性能好。
2、RWMutex 实现原理
在 Go 语言中,RWMutex 是基于互斥锁、变量、信号量等并发原语来实现的。Go 标准库中的 RWMutex 是基于 Mutex 实现的。
2.1 结构体
RWMutex 中总共包含以下 5 个字段。
其中:
- 字段 w:复用互斥锁提供的能力;
- 字段 readerCount:记录当前 reader 的数量(以及是否有 writer 竞争锁);
- readerWait和:记录 writer 请求锁时需要等待 read 完成的 reader 的数量;
- writerSem 和 readerSem:都是为了阻塞设计的信号量,分别用于写等待读和读等待写:
2.2 读锁
看下 RLock 和 RUnlock 方法。
-
如果该方法返回负数 — 其他 Goroutine 获得了写锁,当前 Goroutine 就会调用 runtime.sync_runtime_SemacquireMutex 陷入休眠等待锁的释放;
-
如果该方法的结果为非负数 — 没有 Goroutine 获得写锁,当前方法会成功返回;
当调用 RUnlock 时,我们需要将 Reader 的计数减去 1(第 8 行),根据 AddInt32 的返回值不同会分别进行处理:
- 当返回值是负数,表示有一个或者多个正在执行写操作,此时会调用 rUnlockSlow 方法,检查 reader 是不是都释放读锁;
- 当返回值是非负数,读锁直接解锁成功;
所以,rUnlockSlow 将持有锁的 reader 计数减少 1 的时候,会检查既有的 reader 是不是都已经释放了锁,如果都释放了锁,就会唤醒 writer,让 writer 持有锁。
2.3 写锁
当资源的使用者想要获取写锁时,需要调用 Lock方法。
当一个 writer 获得了内部的互斥锁,会反转 readerCount 字段,把它从原来的正整数 readerCount(>=0) 修改为负数(readerCount-rwmutexMaxReaders),让这个字段保持两个含义(既保存了 reader 的数量,又表示当前有 writer)。
- 第 3 行,调用 Lock 阻塞后续的写操作;
- 当 readerCount 不为 0,说明当前有其他的 reader 持有读锁,RWMutex 会把当前的 readerCount 赋值给 readerWait 字段保存下来(第 7 行),此时这个 wirter 会阻塞进入等待状态(第 8 行)。
- 当 reader 释放读锁时,方法时),readerWait 字段就减 1,当所有活跃的 reader 都释放了读锁,才会唤醒这个 writer。
写锁的释放会调用 Unlock 方法。
当一个 writer 释放锁,会对 readerCount 字段进行反转,减去 rwmutexMaxReaders 变为负数,所以反转方法就是给它增加 rwmutexMaxReaders 这个常数值。
当 writer 要释放锁啦,之后新的 reader,不会再阻塞。
在 RWMutex 的 Unlock 返回之前,需要把内部的互斥锁释放。释放完毕后,其他的 writer 才可以继续竞争这把锁。
在 Lock 方法中,先获取内部的互斥锁接着修改其他字段,但在 Unlock 中,却是先修改其他字段接着才会释放内部的互斥锁,这样才能保证字段的修改的同时会受到互斥锁的保护。
到这里我们就完整学习了 RWMutex 的概念和实现原理。小结一下,读写互斥锁在互斥锁之上提供了额外的更细粒度的控制,能够在读操作远远多于写操作时提升性能。
3、RWMutex 的 2 个踩坑点
坑 1:不可复制
当读写锁正在使用时,它的字段会有一些状态,这个时候你去复制时,会把其字段对应的状态复制过来。当原来锁释放时,不会修改你复制出来的这个锁,会导致复制的锁用不会被释放。
坑 2:释放未加锁的 RWMutex
在 读写锁中,Lock 与 Unlock 的调用是成队出现的,RLock 和 RUnLock 也是。Lock 和 RLock 缺少的调用会导致锁未正确被释放,可能会出现死锁。但是 Unlock 和 RUnlock 会导致 panic 。在生产环境中是不允许的。切记一定要成对出现哦。