锁的基础是什么
atomic 操作
func main() {
c := int32(0)
for i := 0; i < 1000; i ++ {
go func(p *int32) {
*p ++;
}(&c)
}
}
结果大概率不会到1000
使用atomic操作就可以稳定在1000
func main() {
c := int32(0)
for i := 0; i < 1000; i ++ {
go func(p *int32) {
// 汇编实现, 里面用到了LOCK指令(硬件锁)
atomic.AddInt32(p, 1);
}(&c)
}
}
- 原子操作是一种硬件层面加锁机制
- 保证操作一个变量的时候, 其它协程/线程无法访问
- 只能用于简单变量的简单操作
sema锁
- 信号量锁/信号锁
- 核心是一个uint32值, 含义是同时可并发的数量
- 每一个 sema锁都对应一个SemaRoot结构体
- SemaRoot中有一个平衡二叉树用于协程排队
sema操作(uint32>0)
- 获取锁 uint32-1, 获取成功
- 释放锁 uint32+1, 释放成功
sema操作(uint32==0)
- 获取锁 协程休眠 进入堆树等待
- 释放锁 从堆树中取出一个协程, 唤醒
- sema锁 退化成一个专门休眠的队列
总结
- 原子操作是一种硬件层面加锁的机制
- 数据类型和操作类型有限制
- sema锁是runtimel的常用工具
- sema经常被用作休眠队列
互斥锁
sync.Mutex的结构
正常模式
加锁
- 尝试CAS直接加锁
- 若无法直接获取, 进行多次自旋尝试
- 多次尝试失败, 进入sema队列休眠
未获得锁的多次自旋尝试获取锁, 多次自旋失败之后, 就会休眠自己, 然后记录到平衡二叉树下
再有协程来试图获取锁, 自旋多次失败后就会加入到等待树中
对state的locked字段设置是通过CAS操作完成的
解锁
- 尝试CAS直接解锁
- 若发现有协程在sema中休眠, 唤醒一个协程
总结
- mutex正常模式下: 自旋加锁 + sema休眠等待
- mutex正常模式下, 可能有锁饥饿的问题
锁饥饿
- 当前协程等待锁的时间超过了1ms, 切换到饥饿模式
- 饥饿模式中, 不自旋, 新来的协程直接sema休眠
- 饥饿模式中, 被唤醒的协程直接获取锁
- 没有协程在队列中继续等待时, 回到正常模式
总结
- 锁竞争严重时, 互斥锁进入饥饿模式
- 界模式没有自旋等待, 有利于公平
使用经验
- 减少锁的使用时间(细粒度锁)
- 善用defer确保锁的释放