V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
zzhbbdbbd
V2EX  ›  Go 编程语言

golang 关于 goroutine 调度的问题

  •  
  •   zzhbbdbbd ·
    mistricky · 2018-06-30 15:58:39 +08:00 · 3008 次点击
    这是一个创建于 2342 天前的主题,其中的信息可能已经有所发展或是发生改变。

    昨天在学习 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, 实在不解

    18 条回复    2018-09-21 15:21:46 +08:00
    reus
        1
    reus  
       2018-06-30 17:11:19 +08:00   ❤️ 3
    不用细究这个问题,一来实际不会出现这种代码,二来 1.12 会修正这个 bug。

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

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

    不信的话,1.12 的调度器很可能教你做人…
    zzhbbdbbd
        10
    zzhbbdbbd  
    OP
       2018-06-30 18:31:47 +08:00
    @reus 哇!懂了!思路清晰,谢谢大佬
    dbow
        12
    dbow  
       2018-06-30 21:27:12 +08:00
    我解释一下这个现象
    创建 goroutine 的 runtime.newproc 会把 g 放进 runq, 同时放进 p 的 runnext, 第一个 goroutine 先占 runnext, 然后第二个 goroutiner 把它踢了出来。 当调度发生,runq 出队的时候, 先考虑 p 的 runnext, 然后才会按照 runq 的队列顺序来。
    dbow
        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
    }
    }
    }
    zzhbbdbbd
        14
    zzhbbdbbd  
    OP
       2018-07-01 16:05:45 +08:00
    @dbow 感谢回答,runnext 是什么呢
    dbow
        15
    dbow  
       2018-07-01 16:07:02 +08:00
    @zzhbbdbbd #14 就是个指针,指向 g
    zzhbbdbbd
        16
    zzhbbdbbd  
    OP
       2018-07-01 17:17:20 +08:00
    @dbow 谢谢,我再去补补
    waibunleung
        17
    waibunleung  
       2018-09-21 14:37:14 +08:00
    其实是 你的 死循环里面没有调用任何方法,就会在那个 goroutine 里面一直死循环,不信你调用个方法试试,然后将打印出来的东西记录到文本,你就会发现这样做之后就会发生调度了。( google 一下 goroutine 10ms 很多文章都讲到了,其实不明白你都知道 mpg 了为什么没有顺便看到 goroutine 10ms 这个抢占式的调度.....)至于执行顺序,我测试了一下,如果不用 waitGropup 的话,执行顺序是 主 goroutine ----》从上至下顺序执行 goroutine
    waibunleung
        18
    waibunleung  
       2018-09-21 15:21:46 +08:00
    前面说错了,再测试了一遍,即使是 runtime.GOMAXPROCS(1)
    goroutine 的执行顺序也是随机的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1480 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 23:55 · PVG 07:55 · LAX 15:55 · JFK 18:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.