大家好,我是木川
一、什么是 Work Stealing
Go语言的 Work Stealing 机制是一种用于调度协程(Goroutines)的策略,有助于充分利用多核CPU,提高并发性能,降低锁竞争,从而使Go程序更高效地运行
Work Stealing 机制的核心思想:每个操作系统线程(M)都有一个本地任务队列,它会尽可能地先执行自己队列中的协程。当某个M的P队列为空,而其他P仍有任务时,该M会尝试从其他P中"偷"一些协程来执行,以实现负载均衡
二、Work Stealing 算法
![973667daecfbd492aef6df4a11e420ef.png](https://img-blog.csdnimg.cn/img_convert/973667daecfbd492aef6df4a11e420ef.png)
当从本线程M 从绑定 P 本地 队列、全局G队列、Netpoller 都找不到可执行的 G,会从其它 P 里窃取G并放到当前P上面
如果全局队列有G,从全局队列窃取的G数量:N = min(len(GRQ)/GOMAXPROCS + 1, len(GRQ/2)) (根据GOMAXPROCS数量负载均衡)
如果 Netpoller 有G(网络IO被阻塞的G),从Netpoller窃取的G数量:N = 1
如果从其它P里窃取G,从其它P窃取的G数量:N = len(LRQ)/2(平分负载均衡)
如果尝试多次一直找不到需要运行的goroutine则进入睡眠状态,等待被其它工作线程唤醒
从其它P窃取G的源码见runtime/proc.go stealWork函数,窃取流程如下:
选择要窃取的P
从P中偷走一半G
选择要窃取的P
窃取的实质就是遍历所有P,查看其运行队列是否有goroutine,如果有,则取其一半到当前工作线程的运行队列
为了保证公平性,遍历P时并不是按照数组下标顺序访问P,而是使用了一种伪随机的方式遍历allp中的每个P,防止每次遍历时使用同样的顺序访问allp中的元素
offset := uint32(random()) % nprocs
coprime := 随机选取一个小于nprocs且与nprocs互质的数
const stealTries = 4 // 最多重试4次
for i := 0; i < stealTries; i++ {
// 随机访问所有 P
for i := 0; i < nprocs; i++ {
p := allp[offset]
从p的运行队列偷取goroutine
if 偷取成功 {
break
}
offset += coprime
offset = offset % nprocs
}
}
可以看到只要随机数不一样,遍历P的顺序也不一样,但可以保证经过nprocs次循环,每个P都会被访问到
从P中偷走一半G
挑选出盗取的对象P之后,则调用 runtime/proc.go 函数runqsteal 盗取P的运行队列中的goroutine,runqsteal函数再调用runqgrap从P的本地队列尾部批量偷走一半的 G
func runqgrab(_p_ *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 {
for {
h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with other consumers
t := atomic.LoadAcq(&_p_.runqtail) // load-acquire, synchronize with the producer
n := t - h //计算队列中有多少个goroutine
n = n - n/2 //取队列中goroutine个数的一半
if n == 0 {
......
return ......
}
return n
}
}
三、 Work Stealing 的优点
提高线程利用率:当线程M绑定的P⽆可运⾏的G时,尝试从其他P偷取G,减少空转
减少锁竞争:每个M都有自己的本地队列,避免了每次多线程访问全局队列时的锁竞争,提高了性能。
自动负载均衡:通过偷取其他M的任务,Work Stealing可以自动平衡不同线程的工作负载,提高系统整体的并发性能。
最后给自己的原创 Go 面试小册打个广告,如果你从事 Go 相关开发,欢迎扫码购买,目前 10 元买断,加下面的微信发送支付截图额外赠送一份自己录制的 Go 面试题讲解视频
如果对你有帮助,帮我点一下在看或转发,欢迎关注我的公众号