Goroutine中的变量没有像预期的那样改变

时间:2017-05-25 03:47:38

标签: go goroutine

代码很简单如下:

package main

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

var count = uint64(0)

//var l sync.Mutex

func add() {
        for {
                //              l.Lock()
                //              fmt.Println("Start ++")
                count++
                //              l.Unlock()
        }
}

func main() {
        go add()
        time.Sleep(1 * time.Second)
        fmt.Println("Count =", count)
}

例:

  1. 运行代码而不更改,你将得到" Count = 0"。没想到??
  2. 仅取消注释第16行" fmt.Println(" Start ++")&#34 ;;你会得到很多" Start ++"和一些像Count" Count = 11111"的值。预期??
  3. 仅取消注释第11行" var l sync.Mutex",第15行" l.Lock()"和第18行" l.Unlock()"并保持第16行评论;你将获得类似" Count = 111111111"的输出。预期。
  4. 所以......我在共享变量中的使用有问题......?我的问题:

    1. 为什么案例1的计数为0?
    2. 如果预计案例1,为什么案例2会发生?
    3. ENV: 1.转到版本go1.8 linux / amd64 2. 3.10.0-123.el7.x86_64 3. CentOS Linux 7.0.1406版(核心版)

2 个答案:

答案 0 :(得分:6)

您在count上进行了数据竞争。结果未定义。

package main

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

var count = uint64(0)

//var l sync.Mutex

func add() {
    for {
        //              l.Lock()
        //              fmt.Println("Start ++")
        count++
        //              l.Unlock()
    }
}

func main() {
    go add()
    time.Sleep(1 * time.Second)
    fmt.Println("Count =", count)
}

输出:

$ go run -race racer.go
==================
WARNING: DATA RACE
Read at 0x0000005995b8 by main goroutine:
  runtime.convT2E64()
      /home/peter/go/src/runtime/iface.go:255 +0x0
  main.main()
      /home/peter/gopath/src/so/racer.go:25 +0xb9

Previous write at 0x0000005995b8 by goroutine 6:
  main.add()
      /home/peter/gopath/src/so/racer.go:17 +0x5c

Goroutine 6 (running) created at:
  main.main()
      /home/peter/gopath/src/so/racer.go:23 +0x46
==================
Count = 42104672
Found 1 data race(s)
$ 

参考文献:

Benign data races: what could possibly go wrong?

答案 1 :(得分:1)

没有任何同步,你根本就没有保证。

可能有多种原因,为什么你在第一种情况下看到'Count = 0':

  1. 你有一个多处理器或多核系统,一个单元(cpu或核心)正在愉快地转动for循环,而另一个单元睡眠一秒钟并打印你之后看到的线路。编译器生成机器代码是完全合法的,机器代码将值加载到某个寄存器中,并且只在for循环中增加该寄存器。当函数完成变量时,可以更新内存位置。在无限for循环的情况下,永远不会。正如您所说,程序员告诉编译器,通过省略任何同步,没有关于该变量的争用。

    在你的互斥锁版本中,同步原语告诉编译器, 可能有一些其他线程占用互斥锁,因此它需要在解锁互斥锁之前将寄存器中的值写回内存位置。至少有一个人可以这样想。真正发生的事情是解锁和后来的锁操作在两个go例程之间的关系之前引入了一个发生并且这给出了保证,我们将看到在另一个线程中的锁定操作之后在一个线程中解锁之前对变量的所有写入,如go memory model locks所述,无论如何实施。

  2. Go运行时调度程序根本不运行for循环,直到main go例程中的sleep完成。 (不太可能,但是,如果我没记错的话,不能保证不会发生这种情况。)可悲的是,关于调度程序如何工作的官方文档并不多,但它只能安排一个goroutine点,它不是真正的先发制人。这样做的后果是严重的。例如,你可以让你的程序在某些版本的go中永远运行,通过启动尽可能多的例程,就像你有核心一样,为循环做无限循环只增加一个变量。主要的例程(可以结束程序)没有剩下的核心,并且调度程序不能在无限循环中抢占一个go例程,只做简单的事情,比如递增一个变量。我不知道,如果现在改变了。

  3. 正如其他人所指出的那样,这是一场数据竞赛,谷歌搜索并了解它。

  4. 您的版本之间的区别只有第16行被注释/取消注释可能只是因为运行时间,因为打印到终端可能会非常慢。

    对于正确的程序,您需要在主程序之后和fmt.Println之前另外锁定互斥锁,然后解锁它。但是对输出没有确定性的期望,因为结果会因机器/ os /...而变化。