Golang Go语言中的goroutine排队和调度问题
Golang Go语言中的goroutine排队和调度问题
package main
import (
“fmt”
“runtime”
“time”
)
func main() {
runtime.GOMAXPROCS(1)
num:= 258
for i := 0; i < num; i++ {
go func(v int) {
fmt.Printf("%d,", v)
}(i)
}
time.Sleep(time.Millisecond * 500)
}
这段代码,按照我的理解应该是先打印 257
,但是 有时候执行会先打印 0
(通常是首次编译运行的时候,为了复现,我每次都是删除原编译文件,重新生成,然后执行编译文件),
这是为什么?
更多关于Golang Go语言中的goroutine排队和调度问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
单核也有抢占啊,不一定是 257 那个抢到了呀
更多关于Golang Go语言中的goroutine排队和调度问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
goroutine 不保证顺序的
道理我都懂, 可是为啥应该 先打印 257
嘞
单核情况下,不是应该先执行 runnext ,然后本地队列,然后全局队列吗?
是的,我本地跑了你的代码,每次第一个输出的都是 257 。我的版本是( go version go1.17.1 windows/amd64 )
schedule 方法里有一段: 当全局队列有待执行的 goroutine 时,会通过 schedtick 保证有一定几率从全局队列上取 goroutine 来运行。有可能是这个机制导致先输出的 0 ,可以加个 log 看一下。
if gp == nil {
// Check the global runnable queue once in a while to ensure fairness.
// Otherwise two goroutines can completely occupy the local runqueue
// by constantly respawning each other.
if g.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(g.m.p.ptr(), 1)
unlock(&sched.lock)
}
}
(不知道怎么贴图片,就贴一下代码段,将就下)
本地队列长度是 256 ,因为是单核所以会先把所有的 goroutine 加入本地队列然后全局队列,进入 time.sleep 的时候,go 调度器开始工作
你把 fmt.Println 换成更可靠的代码试试,还有重新生成应该加个-a 就行了吧
https://www.v2ex.com/t/556075 之前问过, 开竞态检测-race 就会不一样
静态检测 是因为 goroutine 就随机了,runqput 里面,next 直接赋值 false ,所以不是按照先放 runnext 了…这个不是本文要讨论的。
我看的 runqput 源码,先放 runnext ,runnext 有值的话,把原来的值放到本地队列,本地队列 256 ,如果本地队列已经满了就换搬运前一半到全局队列中。
执行是,因为是先执行 runnext,所以首次是 257 。
首先有本地队列 local P, 全局队列 global P, 变量 runnext ,和本地队列最大容量 N (应该是 256 )。入队逻辑:
先是生成 G0, 然后被放入 runnext ;
然后 G1 来了,G0 被挤入 local P ,runnext 变成 G1 ;
。。。。。
消费时优先从 runnext 开始,
所以当所有生成 G 的个数 n 小于 N+1 时,打印输出为:n-1, 0, 1, … n-2
当生成 G 的个数大于 N+1 时,当 local P 和 runnext 中都占据满了 G 时;下一个 G 来时会触发”溢出操作“:
将 local P 的前一半放入 global P ,再在 global P后面 append 当前 runnext 中的 P
此种情况下消费时,还是优先从 runnext 开始,然后 local P ;但是此时 global P 不为空,当连续消费几个本地 G 后,会从 global P 中拿个 G 过来插队。
因此当生成 G 的个数 n 大于 N+1 时,打印顺序类似(假设只触发了一次溢出操作):n-1, N/2, N/2-1, N/2-2, 0, N/2-3 。。。
如果不止一个 M, 在消费完本地和全局的 G 后,还会从别的 M 偷 G 过来消费。
无论怎么说,限制 M 为 1 时,打印顺序应该是一定的。猜测你的打印顺序不相同可能是因为所有 G 还没有创建完成时就在消费了,在 gorountine 延时一下就可以稳定输出。
------------------------------------------
我在电脑上试了下,v1.17.3 ;打印顺序并不符合我的预期,当产生 G 的个数 n 小于等于 257 时,打印输出为:0 ,n-1, n-2,…1 ,很明显对 runnext 不是“挤出”了,而是如果 runnext 不为空就放入 local P 。大于 257 后就更复杂了。
但是无论怎样,打印输出都是稳定的。
面试要是问这种题纯属脑瘫,只要大概直到 GMP 是啥就行了。
首先 runnext 这个 257 没问题,输出 0 这个确实是 schedule 的机制go<br><br> if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {<br> lock(&sched.lock)<br> gp = globrunqget(_g_.m.p.ptr(), 1)<br> unlock(&sched.lock)<br> }<br><br>
每隔 60 次会从 globelq 获取一个执行,打印数据多一点,也可以看到 0 ,1 ,2 ,3 每隔 60 次打印出来
小于 257 必然是稳定的呀,首先 n-1 ,0,1 2…n-2, 大于 257 也是稳定的,源码看了就很容易发现了,我的环境也是 go 1.17.3
设置 runtime.GOMAXPROCS(1)是,P 的执行顺序都是稳定的
是的呀~~
针对Golang中goroutine的排队和调度问题,以下是我的专业回复:
在Golang中,goroutine的调度是由其运行时系统中的调度器(Scheduler)来完成的。调度器采用M:N调度模型,即将多个goroutine分配到少量OS线程中执行,以分散并发执行的负载,提高并发性能。
goroutine的排队主要发生在调度器的本地队列(Local Queue)和全局队列(Global Queue)中。当goroutine被创建时,它会被放到调度器的本地队列中;当本地队列中的goroutine数量达到一定阈值时,这些goroutine会被放到全局队列中,以便其他线程可以获取它们。调度器还会采用工作窃取(Work Stealing)策略,当一个线程的本地队列中没有goroutine时,它会尝试从全局队列或其他线程的本地队列中窃取goroutine来执行。
此外,Golang的调度器还采用了手动抢占(Preemption)策略,以防止一个goroutine长时间占用线程而导致其他goroutine饿死。从Go 1.14版本开始,调度器引入了异步抢占,即允许在任何安全点进行抢占。
总的来说,Golang的goroutine调度机制是一个高效且灵活的并发执行模型,能够充分利用系统资源,处理大量的并发任务。开发者在编写并发程序时,应尽量避免阻塞goroutine,并适当调整调度器的参数和配置,以达到最佳的性能。