Go中的锁(一) | 青训营笔记

71 阅读2分钟

锁的基础是什么

atomic 操作

func main() {
	c := int32(0)
	for i := 0; i < 1000; i ++ {
		go func(p *int32) {
			*p ++;
		}(&c)
	}
}

Pasted image 20230521162404.png 结果大概率不会到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中有一个平衡二叉树用于协程排队

Pasted image 20230521164239.png

sema操作(uint32>0)

  • 获取锁 uint32-1, 获取成功
  • 释放锁 uint32+1, 释放成功

sema操作(uint32==0)

  • 获取锁 协程休眠 进入堆树等待
  • 释放锁 从堆树中取出一个协程, 唤醒
  • sema锁 退化成一个专门休眠的队列

总结

  • 原子操作是一种硬件层面加锁的机制
  • 数据类型和操作类型有限制
  • sema锁是runtimel的常用工具
  • sema经常被用作休眠队列

互斥锁

sync.Mutex的结构

Pasted image 20230521165513.png

正常模式

加锁

  • 尝试CAS直接加锁
  • 若无法直接获取, 进行多次自旋尝试
  • 多次尝试失败, 进入sema队列休眠

Pasted image 20230521165721.png 未获得锁的多次自旋尝试获取锁, 多次自旋失败之后, 就会休眠自己, 然后记录到平衡二叉树下

Pasted image 20230521165827.png

Pasted image 20230521165934.png 再有协程来试图获取锁, 自旋多次失败后就会加入到等待树中

Pasted image 20230521170053.png 对state的locked字段设置是通过CAS操作完成的

解锁

  • 尝试CAS直接解锁
  • 若发现有协程在sema中休眠, 唤醒一个协程

总结

  • mutex正常模式下: 自旋加锁 + sema休眠等待
  • mutex正常模式下, 可能有锁饥饿的问题

锁饥饿

  • 当前协程等待锁的时间超过了1ms, 切换到饥饿模式
  • 饥饿模式中, 不自旋, 新来的协程直接sema休眠
  • 饥饿模式中, 被唤醒的协程直接获取锁
  • 没有协程在队列中继续等待时, 回到正常模式

总结

  • 锁竞争严重时, 互斥锁进入饥饿模式
  • 界模式没有自旋等待, 有利于公平

使用经验

  • 减少锁的使用时间(细粒度锁)
  • 善用defer确保锁的释放