go - 这是因为go编译器优化了代码吗?

标签 go goroutine

package main

import "time"

func main() {
    i := 1
    go func() {
        for {
            i++
        }
    }()
    <-time.After(1 * time.Second)
    println(i)
}

输出总是1

但是对于 for 循环遍历很多次来说,1s 绝对足够了。

我认为闭包中的 imain 函数中的 i

请参阅下面的代码。

package main

import "time"

func main() {
    i := 1
    go func() {
        for {
            i++
            println("+1")
        }
    }()
    <-time.After(1 * time.Second)
    println(i)
}

经过多行“+1”后,输出正好是预期的一个大数字。

最佳答案

The Go Memory Model

Version of May 31, 2014

Introduction

The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.

Advice

Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.

To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages.

If you must read the rest of this document to understand the behavior of your program, you are being too clever.

Don't be clever.

Synchronization

var a string

func hello() {
  go func() { a = "hello" }()
  print(a)
}

the assignment to a is not followed by any synchronization event, so it is not guaranteed to be observed by any other goroutine. In fact, an aggressive compiler might delete the entire go statement.


通过递增i++ (i = i + 1) 对i 的赋值,后面没有任何同步事件,所以它不保证会被任何其他 goroutine 观察到。事实上,激进的编译器可能会删除整个 i++ 语句。

例如,

package main

import "time"

func main() {
    i := 1
    go func() {
        for {
            i++
        }
    }()
    <-time.After(1 * time.Millisecond)
    println(i)
}

输出:

1

协程缩减为:

"".main.func1 STEXT nosplit size=2 args=0x8 locals=0x0
    0x0000 00000 (elide.go:7)   TEXT    "".main.func1(SB), NOSPLIT, $0-8
    0x0000 00000 (elide.go:7)   FUNCDATA    $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
    0x0000 00000 (elide.go:7)   FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (elide.go:9)   JMP 0

编译器

for {
    i++
}

可以通过永久递增寄存器来实现,本质上是一个空操作 for 循环。

for { }

插入print语句后,

package main

import "time"

func main() {
    i := 1
    go func() {
        for {
            i++
            println("+1")
        }
    }()
    <-time.After(1 * time.Millisecond)
    println(i)
}

输出:

+1
+1
<< SNIP >>
+1
+1
432

goroutine 扩展为,

"".main.func1 STEXT size=81 args=0x8 locals=0x18
    0x0000 00000 (elide.go:7)   TEXT    "".main.func1(SB), $24-8
    0x0000 00000 (elide.go:7)   MOVQ    (TLS), CX
    0x0009 00009 (elide.go:7)   CMPQ    SP, 16(CX)
    0x000d 00013 (elide.go:7)   JLS 74
    0x000f 00015 (elide.go:7)   SUBQ    $24, SP
    0x0013 00019 (elide.go:7)   MOVQ    BP, 16(SP)
    0x0018 00024 (elide.go:7)   LEAQ    16(SP), BP
    0x001d 00029 (elide.go:7)   FUNCDATA    $0, gclocals·a36216b97439c93dafebe03e7f0808b5(SB)
    0x001d 00029 (elide.go:7)   FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x001d 00029 (elide.go:8)   MOVQ    "".&i+32(SP), AX
    0x0022 00034 (elide.go:9)   INCQ    (AX)
    0x0025 00037 (elide.go:10)  PCDATA  $0, $0
    0x0025 00037 (elide.go:10)  CALL    runtime.printlock(SB)
    0x002a 00042 (elide.go:10)  LEAQ    go.string."+1\n"(SB), AX
    0x0031 00049 (elide.go:10)  MOVQ    AX, (SP)
    0x0035 00053 (elide.go:10)  MOVQ    $3, 8(SP)
    0x003e 00062 (elide.go:10)  PCDATA  $0, $0
    0x003e 00062 (elide.go:10)  CALL    runtime.printstring(SB)
    0x0043 00067 (elide.go:10)  PCDATA  $0, $0
    0x0043 00067 (elide.go:10)  CALL    runtime.printunlock(SB)
    0x0048 00072 (elide.go:9)   JMP 29
    0x004a 00074 (elide.go:9)   NOP
    0x004a 00074 (elide.go:7)   PCDATA  $0, $-1
    0x004a 00074 (elide.go:7)   CALL    runtime.morestack_noctxt(SB)
    0x004f 00079 (elide.go:7)   JMP 0

goroutine 增加的复杂性意味着编译器不再考虑将寄存器专用于 i 的值。 i 的内存值增加,这使得更新对于 main goroutine 是可见的,具有数据竞争。

==================
WARNING: DATA RACE

Read at 0x00c420094000 by 
main goroutine:
  main.main()
      /home/peter/gopath/src/lucky.go:14 +0xac

Previous write at 0x00c420094000 by 
goroutine 5:
  main.main.func1()
      /home/peter/gopath/src/lucky.go:9 +0x4e

Goroutine 5 (running) created at:
  main.main()
      /home/peter/gopath/src/lucky.go:7 +0x7a
==================

为了您的预期结果,添加一些同步,

package main

import (
    "sync"
    "time"
)

func main() {
    mx := new(sync.Mutex)
    i := 1
    go func() {
        for {
            mx.Lock()
            i++
            mx.Unlock()
        }
    }()
    <-time.After(1 * time.Second)
    mx.Lock()
    println(i)
    mx.Unlock()
}

输出:

41807838

关于go - 这是因为go编译器优化了代码吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47244615/

相关文章:

multithreading - 何时使用非阻塞 >!/线程和阻塞>!!/带有 clojure core.async 的 goroutines

go - 了解从 channel 读取的行为

用于通过 RPC 传递匿名函数的 GobEncoder

json - 从 POST 解析 JSON

docker - 无法使用docker镜像在gitlab-ci中编译golang项目

concurrency - Go 如何决定何时在 goroutine 之间进行上下文切换?

go - 为什么 ReadRequest 使用 bufio.Reader?

go - 使用构建标签排除 Go 中的完整包

go - 在这个特定示例中,我应该在哪里关闭 channel ?

go - goroutine日程安排如何与GOMAXPROCS一起使用?