采用这段代码:
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。编译器在此进行任何优化吗?
答案 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
中的代码):
x
已定义G
定义并调用Sleep
被叫Sleep
完成Println
(x = 0
)x++
x++
观察一些交错尝试:
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)
}
但是,仍然没有保证。要获得任何保证,请使用任何同步技术,例如渠道。