goroutine 排队和调度问题

2021-12-30 18:27:42 +08:00
 xiangxihenli

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(通常是首次编译运行的时候,为了复现,我每次都是删除原编译文件,重新生成,然后执行编译文件),

这是为什么?

1910 次点击
所在节点    Go 编程语言
15 条回复
Kinnice
2021-12-30 18:34:15 +08:00
单核也有抢占啊,不一定是 257 那个抢到了呀
vxyun
2021-12-30 18:56:57 +08:00
goroutine 不保证顺序的
meiyoumingzi6
2021-12-30 19:09:00 +08:00
道理我都懂, 可是为啥应该 `先打印 257` 嘞
xiangxihenli
2021-12-30 19:12:06 +08:00
@vxyun 单核情况下,不是应该先执行 runnext ,然后本地队列,然后全局队列吗?
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)
}
}

(不知道怎么贴图片,就贴一下代码段,将就下)
vxyun
2021-12-30 20:16:39 +08:00
@meiyoumingzi6 本地队列长度是 256 ,因为是单核所以会先把所有的 goroutine 加入本地队列然后全局队列,进入 time.sleep 的时候,go 调度器开始工作
0o0O0o0O0o
2021-12-30 21:32:14 +08:00
你把 fmt.Println 换成更可靠的代码试试,还有重新生成应该加个-a 就行了吧
Fitz
2021-12-31 10:42:55 +08:00
https://www.v2ex.com/t/556075 之前问过, 开竞态检测-race 就会不一样
xiangxihenli
2021-12-31 10:58:22 +08:00
@Fitz 静态检测 是因为 goroutine 就随机了,runqput 里面,next 直接赋值 false ,所以不是按照先放 runnext 了...这个不是本文要讨论的。
xiangxihenli
2021-12-31 11:05:03 +08:00
@Fitz 我看的 runqput 源码,先放 runnext ,runnext 有值的话,把原来的值放到本地队列,本地队列 256 ,如果本地队列已经满了就换搬运前一半到全局队列中。

执行是,因为是先执行 runnext,所以首次是 257 。
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 是啥就行了。
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 次打印出来
kiddingU
2022-01-26 15:33:45 +08:00
@lysS 小于 257 必然是稳定的呀,首先 n-1 ,0,1 2.....n-2, 大于 257 也是稳定的,源码看了就很容易发现了,我的环境也是 go 1.17.3
lysS
2022-01-26 17:45:38 +08:00
@kiddingU 设置 runtime.GOMAXPROCS(1)是,P 的执行顺序都是稳定的
kiddingU
2022-02-08 16:01:38 +08:00
@lysS 是的呀~~

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/825358

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX