【Go】一个数据竞争问题

2021-12-05  本文已影响0人  如雨随行2020

@[toc]

一、引入

观察如下程序

var val = 0

func main() {
    go add()
    go add()
    time.Sleep(1 * time.Second)
    fmt.Println(val)
}

func add() {
    for i := 0; i < 100; i++ {
        val++
    }
}

在Java中,如果两个线程同一个变量加100次,那么结果是未知的,但是绝大数情况下val的值是小于200的。但是运行结果却是200(我试了好几次都是这个结果)


二、解释


这是修改后的程序以及运行结果,似乎找到了原因。因为add()运行太快了,第一个协程运行完成后第二个协程才开始运行。加上Sleep后,两个协程能交替执行,才会出现数据竞争。

三、再深一步

  1. 为什么数据竞争这么“严重”
    理论上,5ms对于cpu来说是很长的,两个协程同时修改val概率没有这么大,所以结果在100~200直接,会更靠近200些。
  2. 上面是公司的笔记本运行结果,而在我的台式机上结果还是200,这是为什么

第一个问题,涉及到内存同步,参考链接:曹政谈重排。两个协程的操作大部分只写到各自的store buffer中,最终内存同步时,才会有大量自增是无效的。
第二个问题,我开始认为我的电脑是单核的,但是看配置却是


然后,我怀疑是我的Go环境没有配置正确(使用的默认值)

GOMAXPROCS:在Go 1.5版本之前,GOMAXPROCS参数的值默认是1

我没有设置GOMAXPROCS,而Go的版本是1.16,GOMAXPROCS没有设置的话,有几个核就会使用几个核,并且加上如下代码,结果显示并不是单核运行

    num := runtime.NumCPU()
    fmt.Println("使用cpu数量", num) // 使用cpu数量 12

然后,我又怀疑两个协程没有交替运行,于是我又改进了程序,将val改变的中间值,以及哪个协程修改打印了出来

var val = 0

func main() {
    num := runtime.NumCPU()
    fmt.Println("使用cpu数量", num)
    go add("A")
    go add("B")
    time.Sleep(1 * time.Second)
    fmt.Println("val的最终结果", val)
}

func add(proc string) {
    for i := 0; i < 100; i++ {
        val++
        fmt.Printf("execute process[%s] and val is %d\n", proc, val)
        time.Sleep(5 * time.Millisecond)
    }
}

但是结果如下



证明两个协程是交替运行的,我又迷惑了

四、结尾

感谢rustyx的解惑data-race question
上面程序有两个需要改进的地方

  1. 主协程sleep时间太短了,笔记本上运行结果118可能是因为add()协程没有运行完。
  2. val自增的次数(100)太少了,一个协程先跑完,另一个还没开始运行

于是,我又改进了代码

var val = 0

func main() {
    num := runtime.NumCPU()
    fmt.Println("使用cpu数量", num)
    var wg = sync.WaitGroup{}
    wg.Add(2)
    go add("A", &wg)
    go add("B", &wg)
    wg.Wait()
    fmt.Println("val的最终结果", val)
}
func add(proc string, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        val++
    }
}

这是笔记本上运行的一次结果



下面是台式机上运行的结果



终于,可以看到发生数据竞争了。当然,更多的运行结果还是2000
上一篇下一篇

猜你喜欢

热点阅读