一道经常考的面试题

fox_lin · 2019-09-06 10:02:56 · 6387 次点击 · 预计阅读时间 1 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2019-09-06 10:02:56 的文章,其中的信息可能已经有所发展或是发生改变。

前段时间在找工作,也遇到一些不错的面试题,其中有一道很常见,记录一下,里面还有一点搞不明白的:

下面两段程序的输出是什么?

第一段:

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
    }
    wg.Wait()
}

第二段:

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            fmt.Println(n)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

很多面试题解析里面说第一段的10个goroutine输出全部是10,我对这个结论是一直持怀疑态度的,因为输出什么,取决于那个goroutine里面代码被执行时外层i循环到哪里,经我实测,也符合我自己的想法。

clipboard.png

但是那天那个面试官很肯定的说会全部输出一样的数,我忘记问他的理由是什么了。

关于第二段程序,乱序输出0-9,相信大家是没有异议的。总计有10^10种可能。针对第二段程序,那位面试官接着问了一个我觉得挺有水平的问题:这10^10种输出里面,肯定有一种是按顺序0-9依次输出的,能不能通过一些方法,让这段程序的输出顺序固定下来?这个问题我一时还真的抓不到要点了。。。后来在面试官不断的提点下,我才想到面试官的考点,不禁觉得这个面试官还是很有水平的。
第二段程序如何改动才能达到定序输出的效果呢?我们知道每个goroutine生成后,在P的本地G队列未满的时候,是依次加入到P的本地G队列里的,如果只有一个P可用,也就只有一个本地G队列存在,那么这些G的执行顺序其实是取决于P的G队列的顺序的,那么答案也就出来了,我们只要设置P的数量为1,即可达到定序输出的目的:

func main() {
    runtime.GOMAXPROCS(1)
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            fmt.Println(n)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

clipboard.png
不过这里我还是有一点不明白的是,9为什么是第一个被输出的?我猜大概是跟GMP调度有关的。目前还不明白,有知道的同学可以指点我一下,谢谢。

以上如有错误,欢迎指出。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:fox_lin

查看原文:一道经常考的面试题

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

6387 次点击  ∙  2 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
12 回复  |  直到 2022-08-18 21:46:15
invictus090
invictus090 · #1 · 6年之前

“每个goroutine生成后,在P的本地G队列未满的时候,是依次加入到P的本地G队列里的” 这句话中的P、G含义?

invictus090
invictus090 · #2 · 6年之前

P是逻辑处理器P0 G是程序中开的协程吧?

polaris
polaris · #3 · 6年之前

9 为什么是第一个输出的,我之前有专门有给星球的用户写文章讲解过。

ZeroOcyc
ZeroOcyc · #4 · 6年之前

runtime2.go 中关于 p 的定义: 其中 runnext 指针决定了下一个要运行的 g,根据英文的注释大致意思是说:

  • 1 . 如果 runnext 不为空且是就绪的,那么应该优先运行该 runnext 指向的 g 而不是运行队列中到达运行时间的 g。
  • 2 . 此时要被运行的 g 将继承当前 g 剩余的时间片继续执行。
  • 3 . 之所以这样做是为了消除将最后的 g 添加到队列末尾导致的潜在的调度延迟。

所以当设置 runtime.GOMAXPROCS(1) 时,此时只有一个 P,创建的 g 依次加入 P, 当最后一个即 i==9 时,加入的最后 一个 g 将会继承当前主 goroutinue 的剩余时间片继续执行,所以会先输出 9, 之后再依次执行 P 队列中其它的 g。

个人看源码找到的答案,如有错误请立刻指出。

type p struct {
    lock mutex

    id          int32
    status      uint32 // one of pidle/prunning/...
    link        puintptr
    schedtick   uint32     // incremented on every scheduler call
    syscalltick uint32     // incremented on every system call
    sysmontick  sysmontick // last tick observed by sysmon
    m           muintptr   // back-link to associated m (nil if idle)
    mcache      *mcache
    racectx     uintptr

    deferpool    [5][]*_defer // pool of available defer structs of different sizes (see panic.go)
    deferpoolbuf [5][32]*_defer

    // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
    goidcache    uint64
    goidcacheend uint64

    // Queue of runnable goroutines. Accessed without lock.
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr
    // runnext, if non-nil, is a runnable G that was ready'd by
    // the current G and should be run next instead of what's in
    // runq if there's time remaining in the running G's time
    // slice. It will inherit the time left in the current time
    // slice. If a set of goroutines is locked in a
    // communicate-and-wait pattern, this schedules that set as a
    // unit and eliminates the (potentially large) scheduling
    // latency that otherwise arises from adding the ready'd
    // goroutines to the end of the run queue.
    runnext guintptr
        ...
}
Yanwenjiepy
Yanwenjiepy · #5 · 6年之前

最后的P 是 放在 local queue 的头部的,所以第一个执行

ZeroOcyc
ZeroOcyc · #6 · 6年之前
YanwenjiepyYanwenjiepy #5 回复

最后的P 是 放在 local queue 的头部的,所以第一个执行

从执行角度看的确可以看作是放在了 runq 的头部

TryHenry
TryHenry · #7 · 6年之前

第一个循环,如果把10改为100或者1000,会有比较大的概率出现你的预期结果。

jon177
jon177 · #8 · 5年之前
func main() {
    runtime.GOMAXPROCS(1)
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)

        go func(p int) {
            fmt.Println(p)
            wg.Done()
        }(i)

    }
    wg.Wait()
}

将循环改为 1000 后,输出也是固定的,但是顺序很乱,求大佬解惑

//我的输出最后如下
75
76
77
131
78
79
80
81
...
768
769
770
771
901
jon177
jon177 · #9 · 5年之前
jon177jon177 #8 回复

``` func main() { runtime.GOMAXPROCS(1) var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func(p int) { fmt.Println(p) wg.Done() }(i) } wg.Wait() } ``` 将循环改为 1000 后,输出也是固定的,但是顺序很乱,求大佬解惑 ``` //我的输出最后如下 75 76 77 131 78 79 80 81 ... 768 769 770 771 901 ```

猜测 G 队列有大小,所以出现一段有顺序的,然后另一段有顺序的.但是其中又有 77 131 78 这种就奇怪了 验证中...

yacen
yacen · #10 · 5年之前

加个chan控制下

func main() {
    var wg sync.WaitGroup
    ch  := make(chan struct{})
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            <- ch
            fmt.Println(n)
            wg.Done()
        }(i)
        ch <- struct{}{}
    }
    close(ch)
    wg.Wait()
}
slclub
slclub · #11 · 4年之前

是go.runtime的知识,MPG。 也看是否有启动过goroutine。第一次运行估计都是10;

ueueq
ueueq · #12 · 3年之前

掘坟 https://zhuanlan.zhihu.com/p/413218471 为后人植树 重点在runnext

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传