昨天在学习 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, 实在不解
1
reus 2018-06-30 17:11:19 +08:00 3
不用细究这个问题,一来实际不会出现这种代码,二来 1.12 会修正这个 bug。
答案: 1:goroutine 调度是 M 自己主动跳过去的,死循环了自然跳不过去,就一直占用 P。1.12 会用信号让 M 强制跳到信号处理过程,所以死循环不影响。 2:goroutine 的执行顺序不确定,应该认为它是随机的,不是说写在前面就应该先执行,没有保证的,所以不要依赖这个顺序,想要确定的顺序,就用线程同步机制,chan 或者锁等。 |
2
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: 协程是栈操作,后放进去的先拿出来。 |
3
zzhbbdbbd OP @reus 谢谢解答, 大概懂了一些,但是有点不懂的是, 问题 2, 执行顺序是随机的, 但是我每次执行都是声明的第二个 goroutine 先执行(测试了很多遍),难道是有什么因素影响了它们的执行顺序吗?
|
5
a7a2 2018-06-30 17:59:11 +08:00
@zzhbbdbbd 不好意思,我上面第二个说错了。
少看了你代码中第二个 waitGroup.Add(1) ,按照网上说的 waitGroup 是没有顺序的 |
6
zzhbbdbbd OP @a7a2 但是为什么测试了很多遍,总是第二个 goroutine 先执行,这点我不明白,难道是有什么因素影响了它们的执行顺序吗?
|
7
reus 2018-06-30 18:07:51 +08:00
@zzhbbdbbd 不是说测试很多遍它就会一直这样,语言规范没有说必须是这个顺序,那编译器怎么实现都可以,因为都不违反规范。所以你要把它看作是随机的,不能依赖这种未确定的行为,不然很可能新版的编译器就会破坏你依赖的事实。有些项目不敢升级编译器版本,就是因为依赖了特定版本的编译器的行为,一升级就坏了。不是你自己测试很多遍你就能依赖它,编译器、操作系统、硬件等等不同,都有可能出现不同的结果。可以依赖的只有语言规范( https://golang.org/ref/spec ),编译器实现者是一定会遵守的。
|
8
reus 2018-06-30 18:16:12 +08:00
@zzhbbdbbd 编译器的某种行为,如果语言规范没有说,那就是未定义行为,如果你的程序依赖这种行为才能正确工作,那以后编译器改动了,这种行为和之前的不一样了,那你的程序崩了就是你自己的责任,编译器没有责任。语言规范定义了的,编译器实现得不对,那就是编译器实现者的责任。一个例子是,之前并发读写 map 不做同步,是不会报错的,但是某个版本之后,运行时直接就会 panic。规范没有说 map 是线程安全的,那编译器就可以这么做,因为并发读写不做同步,是未定义行为。你在旧版编译器测试很多次都不出错,不代表以后编译器就不会让你的程序出错。goroutine 的执行顺序,就是未定义行为,讨论它是顺序还是倒序,是毫无意义的。
|
9
reus 2018-06-30 18:19:53 +08:00 1
runtime.GOMAXPROCS(1)
这一行代码是没有任何意义的,goroutine 可能在任意地方发生调度,不是说你只用一个 P,你的程序就能保证什么。该上锁的还是得上锁,该同步的还是得同步。goroutine 不是协程,不要拿协程的性质来看待它。 不信的话,1.12 的调度器很可能教你做人… |
11
inkedawn 2018-06-30 21:18:58 +08:00
|
12
dbow 2018-06-30 21:27:12 +08:00
我解释一下这个现象
创建 goroutine 的 runtime.newproc 会把 g 放进 runq, 同时放进 p 的 runnext, 第一个 goroutine 先占 runnext, 然后第二个 goroutiner 把它踢了出来。 当调度发生,runq 出队的时候, 先考虑 p 的 runnext, 然后才会按照 runq 的队列顺序来。 |
13
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 } } } |
17
waibunleung 2018-09-21 14:37:14 +08:00
其实是 你的 死循环里面没有调用任何方法,就会在那个 goroutine 里面一直死循环,不信你调用个方法试试,然后将打印出来的东西记录到文本,你就会发现这样做之后就会发生调度了。( google 一下 goroutine 10ms 很多文章都讲到了,其实不明白你都知道 mpg 了为什么没有顺便看到 goroutine 10ms 这个抢占式的调度.....)至于执行顺序,我测试了一下,如果不用 waitGropup 的话,执行顺序是 主 goroutine ----》从上至下顺序执行 goroutine
|
18
waibunleung 2018-09-21 15:21:46 +08:00
前面说错了,再测试了一遍,即使是 runtime.GOMAXPROCS(1)
goroutine 的执行顺序也是随机的 |