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
(通常是首次编译运行的时候,为了复现,我每次都是删除原编译文件,重新生成,然后执行编译文件),
这是为什么?
1
Kinnice 2021-12-30 18:34:15 +08:00 via Android
单核也有抢占啊,不一定是 257 那个抢到了呀
|
2
vxyun 2021-12-30 18:56:57 +08:00
goroutine 不保证顺序的
|
3
meiyoumingzi6 2021-12-30 19:09:00 +08:00
道理我都懂, 可是为啥应该 `先打印 257` 嘞
|
4
xiangxihenli OP @vxyun 单核情况下,不是应该先执行 runnext ,然后本地队列,然后全局队列吗?
|
5
vxyun 2021-12-30 20:15:14 +08:00
是的,我本地跑了你的代码,每次第一个输出的都是 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) } } (不知道怎么贴图片,就贴一下代码段,将就下) |
6
vxyun 2021-12-30 20:16:39 +08:00
@meiyoumingzi6 本地队列长度是 256 ,因为是单核所以会先把所有的 goroutine 加入本地队列然后全局队列,进入 time.sleep 的时候,go 调度器开始工作
|
7
0o0O0o0O0o 2021-12-30 21:32:14 +08:00 via iPhone
你把 fmt.Println 换成更可靠的代码试试,还有重新生成应该加个-a 就行了吧
|
8
Fitz 2021-12-31 10:42:55 +08:00
https://www.v2ex.com/t/556075 之前问过, 开竞态检测-race 就会不一样
|
9
xiangxihenli OP @Fitz 静态检测 是因为 goroutine 就随机了,runqput 里面,next 直接赋值 false ,所以不是按照先放 runnext 了...这个不是本文要讨论的。
|
10
xiangxihenli OP @Fitz 我看的 runqput 源码,先放 runnext ,runnext 有值的话,把原来的值放到本地队列,本地队列 256 ,如果本地队列已经满了就换搬运前一半到全局队列中。
执行是,因为是先执行 runnext,所以首次是 257 。 |
11
lysS 2021-12-31 17:01:09 +08:00
首先有本地队列 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 是啥就行了。 |
12
kiddingU 2022-01-26 15:08:18 +08:00
@vxyun 首先 runnext 这个 257 没问题,输出 0 这个确实是 schedule 的机制
```go if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 { lock(&sched.lock) gp = globrunqget(_g_.m.p.ptr(), 1) unlock(&sched.lock) } ``` 每隔 60 次会从 globelq 获取一个执行,打印数据多一点,也可以看到 0 ,1 ,2 ,3 每隔 60 次打印出来 |