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

Is this because the go compiler optimized the code?

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, [=13=]-8
    0x0000 00000 (elide.go:7)   FUNCDATA    [=13=], gclocals·2a5305abe05176240e61b8620e19a815(SB)
    0x0000 00000 (elide.go:7)   FUNCDATA    , 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), -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    , 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    [=18=], gclocals·a36216b97439c93dafebe03e7f0808b5(SB)
    0x001d 00029 (elide.go:7)   FUNCDATA    , 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  [=18=], [=18=]
    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    , 8(SP)
    0x003e 00062 (elide.go:10)  PCDATA  [=18=], [=18=]
    0x003e 00062 (elide.go:10)  CALL    runtime.printstring(SB)
    0x0043 00067 (elide.go:10)  PCDATA  [=18=], [=18=]
    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  [=18=], $-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

并发访问变量i需要同步:

synchronization is better done with channels or the facilities of the sync package. Share memory by communicating; don't communicate by sharing memory.

参考:https://golang.org/pkg/sync/atomic/


这很有趣,所以我正在分享我的实验:

0- 您的代码使用 time.Sleep(1 * time.Second)不推荐-不同步):

package main

import "time"

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

输出:

1

1- 使用 i := new(int)不推荐-不同步):

package main

import "time"

func main() {
    i := new(int)
    go func() {
        for {
            *i++
        }
    }()
    time.Sleep(1 * time.Second)
    println(*i)
}

输出(CPU:i7-7700K @ 4.2GHz):

772252413

2- 使用 atomic.AddInt64(&i, 1)(Package atomic provides low-level atomic memory primitives useful for implementing synchronization algorithms) 同步:

package main

import (
    "sync/atomic"
    "time"
)

func main() {
    i := int64(1)
    go func() {
        for {
            atomic.AddInt64(&i, 1) // free running counter
        }
    }()
    time.Sleep(1 * time.Second)
    println(atomic.LoadInt64(&i)) // sampling the counter
}

输出:

233008800

3- 使用 chan int 同步:

package main

import "time"

func main() {
    ch := make(chan int)
    go func() {
        timeout := time.NewTimer(1 * time.Second)
        defer timeout.Stop()
        i := 1
        for {
            select {
            case <-timeout.C:
                ch <- i
                return
            default:
                i++
            }
        }
    }()
    //time.Sleep(1 * time.Second)
    println(<-ch)
}

输出:

272702341

4- 使用 sync.WaitGroup 同步:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var done sync.WaitGroup
    done.Add(1)
    i := 1
    go func() {
        defer done.Done()
        timeout := time.NewTimer(1 * time.Second)
        defer timeout.Stop()
        for {
            select {
            case <-timeout.C:
                return
            default:
                i++
            }
        }
    }()
    done.Wait()
    fmt.Println(i)
}

输出:

261459418

5- 使用退出通道同步:

package main

import (
    "fmt"
    "time"
)

func main() {
    quit := make(chan struct{})
    i := 1
    go func() {
        for {
            i++
            select {
            case <-quit:
                return
            default:
            }
        }
    }()
    time.Sleep(1 * time.Second)
    quit <- struct{}{}
    fmt.Println(i)
}

输出:

277366276

6- 使用 sync.RWMutex 同步:

package main

import (
    "sync"
    "time"
)

func main() {
    var i rwm
    go func() {
        for {
            i.inc() // free running counter
        }
    }()
    time.Sleep(1 * time.Second)
    println(i.read()) // sampling the counter
}

type rwm struct {
    sync.RWMutex
    i int
}

func (l *rwm) inc() {
    l.Lock()
    defer l.Unlock()
    l.i++
}
func (l *rwm) read() int {
    l.RLock()
    defer l.RUnlock()
    return l.i
}

输出:

24271318

相关主题:

The Go Memory Model
There is no equivalent to volatile and register in Go
Does Go support volatile / non-volatile variables?