golang 关于 goroutine 调度的问题

2018-06-30 15:58:39 +08:00
 zzhbbdbbd

昨天在学习 golang 的 goroutine 的时候,遇到了一个令我有点不解的问题。

	func main(){
    
    	runtime.GOMAXPROCS(1)
    
        waitGroup.Add(1)
        go func(){
            defer waitGroup.Done()
            for i := 0;i < 20;i++ {
                fmt.Println("hello")
            }
        }()

        waitGroup.Add(1)
        go func(){
            defer waitGroup.Done()
            for {
            }
        }()

        waitGroup.Wait()
    }
	

是这样的,这是在 main 里的一段代码, 我设置 GOMAXPROCS 为 1,也就是只有一个上下文(不知道对不对,按照某文 GMP 这里应该是 P 吧),一个 M 对应一个 P,M 是 OS thread 的抽象,在每个 M 上挂载一个 runqueue,这样的话,为什么是死循环的 goroutine 先入了 runqueue 然后得到了调度,hello 没有得到打印。

问题 1:难道 go 不会管哪个 goroutine 占取 P 的时间吗?为什么死循环的 goroutine 得到调度之后,一直占用 P,而没有让出给打印 hello 的 goroutine

问题 2:既然 goroutine 会被装入 runqueue,为什么是按声明的顺序倒序装入 runqueue 的,难道不是应该先装入打印 hello 的 goroutine 吗?然后得到调度吗? 为什么是倒序?

小弟初学 golang, 实在不解

2914 次点击
所在节点    Go 编程语言
18 条回复
reus
2018-06-30 17:11:19 +08:00
不用细究这个问题,一来实际不会出现这种代码,二来 1.12 会修正这个 bug。

答案:
1:goroutine 调度是 M 自己主动跳过去的,死循环了自然跳不过去,就一直占用 P。1.12 会用信号让 M 强制跳到信号处理过程,所以死循环不影响。
2:goroutine 的执行顺序不确定,应该认为它是随机的,不是说写在前面就应该先执行,没有保证的,所以不要依赖这个顺序,想要确定的顺序,就用线程同步机制,chan 或者锁等。
a7a2
2018-06-30 17:17:37 +08:00
1:
runtime.GOMAXPROCS(1)对应产出一个 m,一个 m 对应一个 p。
每个 P 会维护一个本地的 go routine 队列,一个 G 如果发生阻塞等事件会进行阻塞。(减少上下文切换浪费时间)

G 发生上下文切换条件:
系统调用;
读写 channel ;
gosched 主动放弃,会将 G 扔进全局队列;

而你的 for 不符合上面三个任 1 切换条件,所以阻塞。

2:
协程是栈操作,后放进去的先拿出来。
zzhbbdbbd
2018-06-30 17:25:46 +08:00
@reus 谢谢解答, 大概懂了一些,但是有点不懂的是, 问题 2, 执行顺序是随机的, 但是我每次执行都是声明的第二个 goroutine 先执行(测试了很多遍),难道是有什么因素影响了它们的执行顺序吗?
zzhbbdbbd
2018-06-30 17:27:03 +08:00
@a7a2 谢谢你的回答, 但是 runqueue 不应该和它的名字一样是队列吗,为什么是栈操作
a7a2
2018-06-30 17:59:11 +08:00
@zzhbbdbbd 不好意思,我上面第二个说错了。
少看了你代码中第二个 waitGroup.Add(1) ,按照网上说的 waitGroup 是没有顺序的
zzhbbdbbd
2018-06-30 18:03:33 +08:00
@a7a2 但是为什么测试了很多遍,总是第二个 goroutine 先执行,这点我不明白,难道是有什么因素影响了它们的执行顺序吗?
reus
2018-06-30 18:07:51 +08:00
@zzhbbdbbd 不是说测试很多遍它就会一直这样,语言规范没有说必须是这个顺序,那编译器怎么实现都可以,因为都不违反规范。所以你要把它看作是随机的,不能依赖这种未确定的行为,不然很可能新版的编译器就会破坏你依赖的事实。有些项目不敢升级编译器版本,就是因为依赖了特定版本的编译器的行为,一升级就坏了。不是你自己测试很多遍你就能依赖它,编译器、操作系统、硬件等等不同,都有可能出现不同的结果。可以依赖的只有语言规范( https://golang.org/ref/spec ),编译器实现者是一定会遵守的。
reus
2018-06-30 18:16:12 +08:00
@zzhbbdbbd 编译器的某种行为,如果语言规范没有说,那就是未定义行为,如果你的程序依赖这种行为才能正确工作,那以后编译器改动了,这种行为和之前的不一样了,那你的程序崩了就是你自己的责任,编译器没有责任。语言规范定义了的,编译器实现得不对,那就是编译器实现者的责任。一个例子是,之前并发读写 map 不做同步,是不会报错的,但是某个版本之后,运行时直接就会 panic。规范没有说 map 是线程安全的,那编译器就可以这么做,因为并发读写不做同步,是未定义行为。你在旧版编译器测试很多次都不出错,不代表以后编译器就不会让你的程序出错。goroutine 的执行顺序,就是未定义行为,讨论它是顺序还是倒序,是毫无意义的。
reus
2018-06-30 18:19:53 +08:00
runtime.GOMAXPROCS(1)

这一行代码是没有任何意义的,goroutine 可能在任意地方发生调度,不是说你只用一个 P,你的程序就能保证什么。该上锁的还是得上锁,该同步的还是得同步。goroutine 不是协程,不要拿协程的性质来看待它。

不信的话,1.12 的调度器很可能教你做人…
zzhbbdbbd
2018-06-30 18:31:47 +08:00
@reus 哇!懂了!思路清晰,谢谢大佬
inkedawn
2018-06-30 21:18:58 +08:00
dbow
2018-06-30 21:27:12 +08:00
我解释一下这个现象
创建 goroutine 的 runtime.newproc 会把 g 放进 runq, 同时放进 p 的 runnext, 第一个 goroutine 先占 runnext, 然后第二个 goroutiner 把它踢了出来。 当调度发生,runq 出队的时候, 先考虑 p 的 runnext, 然后才会按照 runq 的队列顺序来。
dbow
2018-06-30 21:28:51 +08:00
你们看这个函数
func runqget(_p_ *p) (gp *g, inheritTime bool) {
// If there's a runnext, it's the next G to run.
for {
next := _p_.runnext
if next == 0 {
break
}
if _p_.runnext.cas(next, 0) {
return next.ptr(), true
}
}

for {
h := atomic.Load(&_p_.runqhead) // load-acquire, synchronize with other consumers
t := _p_.runqtail
if t == h {
return nil, false
}
gp := _p_.runq[h%uint32(len(_p_.runq))].ptr()
if atomic.Cas(&_p_.runqhead, h, h+1) { // cas-release, commits consume
return gp, false
}
}
}
zzhbbdbbd
2018-07-01 16:05:45 +08:00
@dbow 感谢回答,runnext 是什么呢
dbow
2018-07-01 16:07:02 +08:00
@zzhbbdbbd #14 就是个指针,指向 g
zzhbbdbbd
2018-07-01 17:17:20 +08:00
@dbow 谢谢,我再去补补
waibunleung
2018-09-21 14:37:14 +08:00
其实是 你的 死循环里面没有调用任何方法,就会在那个 goroutine 里面一直死循环,不信你调用个方法试试,然后将打印出来的东西记录到文本,你就会发现这样做之后就会发生调度了。( google 一下 goroutine 10ms 很多文章都讲到了,其实不明白你都知道 mpg 了为什么没有顺便看到 goroutine 10ms 这个抢占式的调度.....)至于执行顺序,我测试了一下,如果不用 waitGropup 的话,执行顺序是 主 goroutine ----》从上至下顺序执行 goroutine
waibunleung
2018-09-21 15:21:46 +08:00
前面说错了,再测试了一遍,即使是 runtime.GOMAXPROCS(1)
goroutine 的执行顺序也是随机的

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

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

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

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

© 2021 V2EX