为什么即使有一个time.Sleep这个goroutine也不能运行?

时间:2020-04-03 17:21:40

标签: go goroutine go-scheduler

采用这段代码:

func main() {
    var x int
    go func() {
        for {
            x++
        }
    }()
    time.Sleep(time.Second)
    fmt.Println("x =", x)
}

为什么x最后等于0?我了解Go的调度程序需要time.Sleep()调用来提取goroutine,但为什么不这样做呢?

提示: :将time.Sleep()或对runtime.Gosched()的调用放在for循环中可修复此代码。但是为什么呢?

更新: :检查以下版本的相同代码:

func main() {
    var x int
    go func() {
        for i := 0; i < 10000; i++ {
            x++
        }
    }()
    time.Sleep(time.Second)
    fmt.Println("x =", x)
}

奇怪的是,现在执行了goroutine中的代码,并且x不再为0。编译器在此进行任何优化吗?

2 个答案:

答案 0 :(得分:6)

了解您在这里的要求很重要。 Go中没有保证该程序会做特别的事情,因为该程序无效。但是,作为对优化器的探索,有趣的是可以对当前的实现方式提供一些见识。任何依赖于此信息的程序都是非常脆弱且无效的,但这仍然是一种好奇。

我们可以编译程序,然后查看输出。我特别喜欢您提供的两个版本,因为它们可以让您看到差异。我已经使用Hopper进行了反编译(这些文件是使用go1.14 darwin / amd64编译的)。

在第二种情况下,goroutine看起来就像您认为的那样:

void _main.main.func1(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6) {
    rax = arg6;
    for (rcx = 0x0; rcx < 0x2710; rcx = rcx + 0x1) {
            *rax = *rax + 0x1;
    }
    return;
}

这没什么奇怪的。但是,您对第一种情况感到好奇呢?

_main.main.func1:
    goto _main.main.func1;

它变成noop。毫不夸张的说;这是程序集:

                     _main.main.func1:
000000000109d1b0         nop                                                    ; CODE XREF=_main.main.func1+1
000000000109d1b1         jmp        _main.main.func1                            ; _main.main.func1

这是怎么发生的?好了,编译器可以查看以下代码:

go func() {
    for {
        x++
    }
}()

它知道什么都不会读x。不可能读取x,因为x周围没有锁定,并且此goroutine永远不会终止。因此,在此例行程序完成之后,x 都无法读取。请参阅The Go Memory Model,以了解在某事之前或之后发生某事意味着什么。

“但是我确实读过x!”不,你不会。那将是无效的代码,并且编译器知道您没有编写无效的代码。当有竞赛探测器告诉您这是无效的时,谁会这样做?因此,由于编译器可以清楚地看到没有任何内容读取x,因此没有理由费心更新它。

在您的有限循环示例中,goroutine终止,因此之后可能会读取x。编译器不够聪明,无法注意到从未进行过 valid 读取,因此编译器无法尽其所能进行优化。也许将来的编译器将足够聪明,在两种情况下都输出0。也许将来的编译器将足够聪明,可以在第一种情况下完全删除您的无操作goroutine。

但是这里的关键是无限循环的情况是完全正确的,尽管效率比可能要低一些。非无限循环情况也完全正确,尽管效率可能要低得多。

答案 1 :(得分:2)

这是一个通用的多处理问题,不是goroutine或Go所特有的。

不能保证代码中语句的执行顺序。例如,以下顺序是可能的(假定“ G”是您的goroutine,而“ M”是main中的代码):

  1. M:x已定义
  2. M:G定义并调用
  3. M:Sleep被叫
  4. M:Sleep完成
  5. M:Printlnx = 0
  6. G:x++
  7. G:x++
  8. ...(一些次数,甚至是0)
  9. 程序结束

观察一些交错尝试:

package main

import (
    "fmt"
    "time"
)

func main() {
    var x int

    go func() {
        for {
            time.Sleep(time.Second) 
            x++
        }
    }()
    time.Sleep(5*time.Second)
    fmt.Println("x =", x)
}

但是,仍然没有保证。要获得任何保证,请使用任何同步技术,例如渠道。