锁存在的意义就是保护共享资源,或者说是保证共享资源此刻不变的,当两个及以上的goroutine使用共享资源时,就一定要使用锁,来保护共享资源。
互斥锁:加锁后,只能等解锁后才能使用共享资源
读写锁:这个有两套函数:写锁,读锁。
读写锁特点:可以并发读,不可以并发写,不可以读写。
互斥锁
1.Mutex的数据结构
互斥锁本质是一个结构体
type Mutex struct{
state int32
sema uint32
}
state 表示互斥锁的状态,比如是否被锁定
sema 表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量,从而唤醒其他等待信号量的协程
state 是 32 位的整型变量,内部实现时把该变量分成四份,用于记录Mutex的四种状态。
Locked:表示该 Mutex 是否已被锁定,0 没有被锁定,1 已经被锁定。
Worken: 表示 是否有协程已经被唤醒,0表示没有协程唤醒,1表示有协程唤醒,正在加锁过程中(后续会讲解)
Starving:表示该锁是否已处于饥饿状态,0表示没有处于饥饿状态,1表示处于饥饿状态,说明有协程阻塞了超过1ms(后续会讲解)
Waiter:表示阻塞等待锁的协程的数量,协程解锁时根据此值来判断是否需要释放信号量。
协程之间的抢锁实际上就是争抢给 Locked 赋值的权利,能给 Locked 置1,就说明抢锁成功。抢不到就阻塞等待 信号量,一旦持有了锁的协程解锁,那么等待锁的协程会被依次唤醒。
Mutex 向外提供两个方法,加锁,解锁
Lock():加锁方法
Unlock():解锁方法
并且每种方法执行的时候都各有两种情况
2.加解锁过程
1)简单加锁
假定当前只有一个协程在加锁,没有其他的协程。加锁过程中会检测 Locked 标志位是否为0,如果 Locked 值 为0,则直接将Locked置为1,代表加锁成功。其他位没有变化。
2)被阻塞加锁
假定现在有其他进程要进行抢锁处理,过程如下。
协程A进行加锁,之后,协程B也要尝试获取锁,这个时候协程B检测 Locked 标志位,发现为 1,则无法获得锁,此时Waiter 计数加1,协程B将被阻塞,等待Locked 变为 1 时唤醒。
3)简单解锁
假定没有其他协程阻塞,解锁过程如下。
协程A解锁时,会检测 Waiter,是否有协程等待加锁,当然现在这种情况下没有,Waiter计数位0,没有其他协程等待获取锁,所以解锁时不需要释放信号量,此时则直接置Locked 为 0 即可,无需释放信号量
4)解锁后释放信号量
协程A加锁之后,Locked 置1,此时协程B准备加锁,发现Locked 为 1(Mutex已经被抢占),此时协程B进入阻塞状态,且Waiter 计数 +1,此时协程A解锁时,一共有两步:1. 将Locked 置 0,2. 查看 Waiter >0,释放一个信号量,唤醒一个阻塞的协程,被唤醒的 协程B将 Locked位置1,于是,协程B获得锁。
3.自旋过程
加锁时,如果Locked为1,则说明有其他协程正在使用Mutex,此时尝试加锁的协程不会立即进入阻塞状态,而是等待一段时间,一直检测Locked位是否为0,这各过程称为自旋过程。
自选的时间很短,如果发现锁已经被释放了,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞。
自选的好处就是,当加锁失败时不必立即转入阻塞,有一定的机会获取到锁,这样可以避免协程切换。
1)什么是自旋
自旋对应的是 CPU 的 PAUSE 指令,相当于 CPU 空转,对程序来说“sleep”一会儿,这个时间很短,当前实现是30个时钟周期。
自选过程中会不断检测Locked位是否为0,连续两次探测间隔就是在执行这些PAUSE指令,它不同于sleep,不需要将协程转为睡眠状态。