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

goroutine 排队和调度问题

  •  
  •   xiangxihenli · 2021-12-30 18:27:42 +08:00 · 2102 次点击
    这是一个创建于 1069 天前的主题,其中的信息可能已经有所发展或是发生改变。
    
    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(通常是首次编译运行的时候,为了复现,我每次都是删除原编译文件,重新生成,然后执行编译文件),

    这是为什么?

    15 条回复    2022-02-08 16:01:38 +08:00
    Kinnice
        1
    Kinnice  
       2021-12-30 18:34:15 +08:00 via Android
    单核也有抢占啊,不一定是 257 那个抢到了呀
    vxyun
        2
    vxyun  
       2021-12-30 18:56:57 +08:00
    goroutine 不保证顺序的
    meiyoumingzi6
        3
    meiyoumingzi6  
       2021-12-30 19:09:00 +08:00
    道理我都懂, 可是为啥应该 `先打印 257` 嘞
    xiangxihenli
        4
    xiangxihenli  
    OP
       2021-12-30 19:12:06 +08:00
    @vxyun 单核情况下,不是应该先执行 runnext ,然后本地队列,然后全局队列吗?
    vxyun
        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)
    }
    }

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

    执行是,因为是先执行 runnext,所以首次是 257 。
    lysS
        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 是啥就行了。
    kiddingU
        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 次打印出来
    kiddingU
        13
    kiddingU  
       2022-01-26 15:33:45 +08:00
    @lysS 小于 257 必然是稳定的呀,首先 n-1 ,0,1 2.....n-2, 大于 257 也是稳定的,源码看了就很容易发现了,我的环境也是 go 1.17.3
    lysS
        14
    lysS  
       2022-01-26 17:45:38 +08:00
    @kiddingU 设置 runtime.GOMAXPROCS(1)是,P 的执行顺序都是稳定的
    kiddingU
        15
    kiddingU  
       2022-02-08 16:01:38 +08:00
    @lysS 是的呀~~
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1898 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 00:00 · PVG 08:00 · LAX 16:00 · JFK 19:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.