一、什么是 Work Stealing
Go语言的 Work Stealing 机制是一种用于调度协程(Goroutines)的策略,有助于充分利用多核CPU,提高并发性能,降低锁竞争,从而使Go程序更高效地运行
Work Stealing 机制的核心思想:每个操作系统线程(M)都有一个本地任务队列,它会尽可能地先执行自己队列中的协程。当某个M的P队列为空,而其他P仍有任务时,该M会尝试从其他P中"偷"一些协程来执行,以实现负载均衡
二、Work Stealing 算法
当从本线程M 从绑定 P 本地 队列、全局G队列、Netpoller 都找不到可执行的 G,会从其它 P 里窃取G并放到当前P上面
1)如果全局队列有G,从全局队列窃取的G数量:N = min(len(GRQ)/GOMAXPROCS + 1, len(GRQ/2)) (根据GOMAXPROCS数量负载均衡)
2)如果 Netpoller 有G(网络IO被阻塞的G),从Netpoller窃取的G数量:N = 1
3)如果从其它P里窃取G,从其它P窃取的G数量:N = len(LRQ)/2(平分负载均衡)
4)如果尝试多次一直找不到需要运行的goroutine则进入睡眠状态,等待被其它工作线程唤醒
从其它P窃取G的源码见runtime/proc.go stealWork函数,窃取流程如下:
1)选择要窃取的P
2)从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 的优点
1)提高线程利用率:当线程M绑定的P⽆可运⾏的G时,尝试从其他P偷取G,减少空转
2)减少锁竞争:每个M都有自己的本地队列,避免了每次多线程访问全局队列时的锁竞争,提高了性能。
3)自动负载均衡:通过偷取其他M的任务,Work Stealing可以自动平衡不同线程的工作负载,提高系统整体的并发性能。
标签:goroutine,队列,Work,线程,Stealing,Go,窃取 From: https://www.cnblogs.com/beatle-go/p/17895147.html