一、前情提要
上一篇我们介绍了Mutex锁资源的模式及源码分析。mutex处于正常状态时,申请锁时与自旋状态的goroutine抢占锁失败时,会将对应的goroutine放在等待队列的头或者尾(若是唤醒的goroutine则是队头,若是新创建的goroutine则是队尾)等待唤醒;mutex处于饥饿状态时,申请锁时若没有锁资源则直接放在队尾。
1 | // 加锁 |
二、底层源码理解
相当于操作系统中P一个锁一个资源
1 | // SemacquireMutex is like Semacquire, but for profiling contended Mutexes. |
底层调用semacquire1,也是主要的处理接口。该接口的功能是:获取 sudog 和 semaRoot ,其中 sudog 是 g 放在等待队列里的包装对象,sudog 里会有 g 的信息和一些其他的参数, semaRoot 则是队列结构体,内部是二插平衡树,把和当前 g 关联的 sudog 放到 semaRoot 里,然后把 g 的状态改为等待,调用调度器执行别的 g,此时当前 g 就停止执行了。一直到被调度器重新调度执行,会首先释放 sudog 然后再去执行别的代码逻辑
1 | //go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex |
1、sudog
sudog运行时用来存放处于阻塞状态的 goroutine 的一个上层抽象,是用来实现用户态信号量的主要机制之一
例如当一个 goroutine 因为等待 channel 的数据需要进行阻塞时,sudog 会将 goroutine 及其用于等待数据的位置进行记录。阻塞的sudog组成一个树堆
1 | type sudog struct { |
2、semaRoot
一个 semaRoot 持有不同地址的 sudog 的树堆。每一个 sudog 可能反过来指向等待在同一个地址上的 sudog 的列表
1 | type semaRoot struct { |
3、Treap
Treap是一种平衡树,它较普通二叉查找树而言,每个节点被赋予了一个新的属性:优先级(没错就是类似优先队列的优先),对于Treap中的每个结点,除了它的权值满足二叉查找树的性质外,它的优先级还满足堆性质,也就是结点的优先级小于它所有孩子的优先级,也就是一个最小堆
所以从权值上看,Treap是一个二叉查找树;从优先级上看,Treap是一个堆。所以Treap=Tree+Heap
我们发现普通BST会不平衡是因为有序的数据会使查找路径退化成链,而随机数据使其退化的概率非常小。因此我们在Treap中赋予的这个优先级的值采用随机生成的办法,这样Treap的结构就趋于平衡了
如下图:黑色字体为优先级,旋转之后与原来的平衡树是一致的(3,7,9,11,16)。但是按照优先级进行旋转就会变成最小堆
在sudog中ticket作为优先级的字段,按照ticket进行旋转。
1 | s.ticket = fastrand() | 1 // ticket值随机生成 |
4、semacquire1具体流程
- 获取一个sudog,并添加goroutine的基本信息
- 获取一个semroot,也就是Treap的根。待将sudog插入该树中,后经过旋转达到新的平衡。
- 将当前goroutine设置为等待状态,与当前m解绑,让m执行其他goroutine
1 | func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int) { |
接下来看其中几个接口。
5、如何获得一个sudog
这里涉及到一个全局缓存池和 per-P 的缓存池。分配的时候per-P缓存池优先优先分配,当使用完毕后又再次归还给缓存池。 其遵循策略:
- 优先从 per-P 缓存中获取,如果 per-P 缓存为空,则从全局池抓取一半;
- 优先归还到 per-P 缓存,如果 per-P 缓存已满,则将 per-P 缓存的一半归还到全局池。
1 | func acquireSudog() *sudog { |
6、在semaRoot添加sudog节点
1 | func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) { |
至此,阻塞申请一个锁资源的流程大致就是如此。当前goroutine已于对应的M解绑,相当于放在一个缓存池里面,等待有资源释放。
三、总结
我们可以看到上面的一通操作相当于操作系统的sleep信号,但是我们并没有陷入系统调用,浪费CPU等资源。而是在用户态利用缓存池屏蔽内部调度器的存在,创建基于goroutine抽象的信号量。例如,当用户态代码使用互斥锁发生竞争时,能够让用户态代码依附的 goroutine 进行 sleep,将其进行集中存放,并在可用时候被 wakeup,并被重新调度。
这就是为什么golang可以性能这么好
下一篇:释放一个资源的流程