Mutex
简介
- 互斥锁,限制临界区同时只能由一个线程持有
- Mutex实现了Locker接口,对外提供两个接口,加锁用 Lock(), 退出加锁用 UnLock()
- Mutex不用初始化,直接声明就可以使用
- Mutex一般可以单独使用,也可以嵌入其他结构中进行封装,对外暴露接口。
实现
type Mutex struct {
state int32
sema uint32
}
演进
- 初版 使用一个 flag 来表示锁是否被持有,所有 goroutine 进入队列排队执行,先来先得
- 看似公平,但却不是最优。如果能将锁交给当前正在占用CPU的 goroutine,可以避免上下文切换。
- 给新人机会 让新来的 goroutine 与被唤醒的 goroutine 去竞争锁
- 修改之前的flag字段,赋予三层含义。最低位表示这个锁是否被持有,第二位表示是否有被唤醒的 goroutine,其余位用来表示正在等待此锁的 goroutine数量。
- 多给些机会 新来的 goroutine 与被唤醒的 goroutine 如果能获取到锁,不用挂起,可以节省很多上下文切换的时间,因此加入自旋让新来的多几次机会尝试获取锁。
- 解决饥饿 由于前边的种种机制,会带来饥饿问题,所以目前又加上了解决饥饿的方案
- 在 state 字段第三位 添加饥饿标志
- 正常模式和饥饿模式
- 正常模式下,waiter都是进入先进先出队列,被唤醒的waiter会和新来的 goroutine 进行竞争。如果被唤醒的waiter超过1ms获取不到锁,那么这个锁就进入饥饿模式
- 在饥饿模式下,该锁会退化为原始的先进先出队列,新来的goroutine只会加到队列末尾,锁释放出来后直接交给队列最前面的waiter。
- 如果该锁已经没有其他waiter或者其他waiter等待时间小于1ms,则退出饥饿模式
易错点
- Lock/Unlock不成对出现
- Unlock一个为加锁的Mutex会直接 panic
- Copy已经使用的Mutex
- Mutex的 state 字段是有状态的,会有复制一个已经加锁的锁的可能
- 重入
- Mutex是不可重入锁。Mutex根本就没有记录当前持有锁的goroutine,无法重入
- 死锁
- 产生死锁的四个条件:互斥、持有和等待、不可剥夺、环路等待
RWMutex
简介
- 读写锁,可以将串行读变为并发读。RWMutex在某一时刻只能由任意数量的 reader 持有或者由一个 writer 持有
- 读写锁对外提供了5个方法。
- Lock/Unlock:写操作时调用。如果锁已经被 reader 或者 writer持有,就会一直等待,直到获取到锁
- RLock/RUnlock:读操作时调用的方法。如果锁已经被 writer 持有,则会一直阻塞,直到获取到锁
- RLocker:该方法会返回一个 Locker 接口的对象,其Lock/Unlock方法均对应 RWMutex的 RLock/RUnlock
- RWMutex不用初始化,直接声明就可以使用
- 如果有读写明确区分,且大量并发读,少量并发写的场景,可以考虑使用RwMutex
实现
读写锁在很多语言都有实现。go语言的RWMutex是基于Mutex实现的
type RWMutex struct {
w Mutex // 写优先的读写锁
writerSem uint32 // 写信号量
readerSem uint32 // 读信号量
readerCount int32 // 当前 reader 的数量
readerWait int32 // 记录 writer 请求锁时,需要等待 reader 的数量
}
const rwmutexMaxReaders = 1 << 30 // reader 的最大数量
RLock/RUnlock的实现
- RLock 如果 readerCount为负数,说明当前有 writer在等待请求锁,该 reader需要阻塞等待唤醒。不为负数则成功获取到读锁
- RUnlock 释放读锁,对readerCount减一。如果 readerCount为负数,说明当前有 writer 在等待 读锁,需要调用 rUnlockSlow 方法,检验是不是所有reader都已经释放了,如果都已经释放,就可以通过 writerSem 信号量唤醒挂起的 writer
Lock的实现
- 一旦有 writer 获得了 RWMutex 内部的 互斥锁 Mutex,就会反转 readerCount字段,将原来的正数变为负数,表示当前已经有writer在等待该读写锁。(反转的方法是 减去 rwmutexMaxReaders 常数)
- 如果readerCount不为0,就说明当前有持有读锁的 reader,需要吧当前的 readerCount 赋值给 readerWait 保存下来。每当一个reader 释放读锁,readerWait字段就减一,直到所有 持有读锁的 reader 都释放完,才会唤醒这个 writer
Unlock的实现
- 当一个 writer 要释放锁时,会再次反转 readerCount 字段。
- 便利释放所有等待的 reader
- 释放内部的 mutex
易错点
- 不可复制
- 读写错是有状态的,内部还包含互斥锁,因此同样不可复制
- 重入导致死锁
- 读写锁内部基于互斥锁 实现对 writer 的并发访问,因此和Mutex一样存在重入问题
- 释放未加锁的RWMutex
- 同Mutex