【问题标题】:The variable in Goroutine not changed as expectedGoroutine 中的变量没有按预期改变
【发布时间】:2017-05-25 03:47:37
【问题描述】:

代码简单如下:

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 ++")";你会得到很多“Start ++”的输出和一些像“Count = 11111”这样的计数值。预期??
  3. 仅取消注释第 11 行“var l sync.Mutex”、第 15 行“l.Lock()”和第 18 行“l.Unlock()”并保留第 16 行的注释;你会得到像“Count = 111111111”这样的输出。预计。

所以...我在共享变量中的使用有问题...?我的问题:

  1. 为什么案例 1 的计数为 0?
  2. 如果情况 1 是预期的,为什么会发生情况 2?

环境: 1.go版本go1.8 linux/amd64 2. 3.10.0-123.el7.x86_64 3. CentOS Linux release 7.0.1406 (Core)

【问题讨论】:

  • 似乎工作:(https)goo.gl/Fa2JUC
  • @VonC:您没有检查数据竞争:警告:数据竞争。

标签: go goroutine


【解决方案1】:

您在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?

【讨论】:

    【解决方案2】:

    如果没有任何同步,您根本无法保证。

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

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

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

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

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

    只有第 16 行被注释/未注释的版本之间的差异可能只是因为运行时间,因为打印到终端可能会很慢。

    对于一个正确的程序,您需要在主程序睡眠之后和 fmt.Println 之前额外锁定互斥锁,然后再解锁。但是不能有关于输出的确定性期望,因为结果会随着机器/操作系统/...而变化。

    【讨论】:

    • 锁使程序正确,但它们不保证add 将在程序退出之前运行。在实践中add 将在睡眠期间运行,但语言规范或内存模型中并没有要求 goroutine 运行。
    • 我很确定 #1 是完全错误的,至少就 Go 而言。解锁互斥锁不会导致其他任意变量从 L1 复制到 RAM。变量可以在并行线程之间共享而无需任何锁定,这会导致竞争条件,但不会导致两个线程同时看到同一变量的不同值。
    • 你的权利,对不起。我会更新我的答案。我阅读了有关缓存一致性协议的信息,似乎没有任何商品硬件,正如我所描述的那样。我可以指出 mpi 和 hpc 集群/并行系统,但这可能是一个稻草人的论点,因为它似乎完全不适合 go。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-11
    • 2022-01-18
    • 1970-01-01
    • 2020-09-04
    • 1970-01-01
    • 2015-07-10
    相关资源
    最近更新 更多