一尘不染

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

go

package main

import "time"

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

输出始终为1

但是,绝对可以使for循环多次遍历1s 。

我认为,i在闭包是imainFUNC。

请参见下面的代码。

package main

import "time"

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

在多行“ +1”之后,输出正好是预期的很大数目。


阅读 250

收藏
2020-07-02

共1个答案

一尘不染

记忆模型

2014年5月31日版本

介绍

Go内存模型指定了一种条件,在这种条件下,可以保证在一个goroutine中读取变量可以观察到在不同goroutine中写入同一变量所产生的值。

忠告

修改由多个goroutine同时访问的数据的程序必须序列化此类访问。

要序列化访问,请使用通道操作或其他同步原语(例如sync和sync / atomic包中的原语)保护数据。

如果您必须阅读本文档的其余部分以了解程序的行为,那么您就太聪明了。

别聪明

同步化

var a string

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

分配给a不会跟随任何同步事件,因此不能保证任何其他goroutine都会遵守它。实际上,积极的编译器可能会删除整个go语句。


i通过增量i++i = i + 1)分配给时,没有任何同步事件,因此不能保证任何其他goroutine都会遵守该事件。实际上,积极的编译器可能会删除整个i++语句。

例如,

package main

import "time"

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

输出:

1

goroutine简化为:

"".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会增加,这使得更新与数据竞争可见于maingoroutine。

==================
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
2020-07-02