V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
xuletter2021
V2EX  ›  Go 编程语言

golang 协程读写上下文变量 一直为 0

  •  
  •   xuletter2021 · 2021-03-20 15:28:26 +08:00 · 3047 次点击
    这是一个创建于 1345 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如代码 1:运行过程中无论 sleep 多久,输出都是 0

    func main() {
    	var x int
    	go func() {
    		for {
    			x++
    		}
    	}()
    	time.Sleep(time.Duration(10) * time.Second)
    	fmt.Println("**************")
    	fmt.Println(x)
    	fmt.Println("**************")
    }
    

    如代码 2:在上面第六行加了点代码就输出就变了,一直不明白为什么

    func main() {
    	var x int
    	go func() {
    		for {
    			x++
    			// select()
    			// or
    			// fmt.Println("ddd")
    		}
    	}()
    	time.Sleep(time.Duration(1) * time.Second)
    	fmt.Println("**************")
    	fmt.Println(x)
    	fmt.Println("**************")
    }
    

    求大佬指点

    24 条回复    2021-03-22 11:35:37 +08:00
    lozzow
        1
    lozzow  
       2021-03-20 16:09:52 +08:00
    标记等个答案
    Orlion
        2
    Orlion  
       2021-03-20 16:21:17 +08:00 via Android
    你关闭优化跑一遍看看是不是出来了?😂
    xuletter2021
        3
    xuletter2021  
    OP
       2021-03-20 16:27:24 +08:00
    关闭内联优化 ```go build -gcflags "-N -l" testX.go```,结果还是一样
    darrh00
        4
    darrh00  
       2021-03-20 16:34:52 +08:00
    代码读写 x 变量有 data race, go build -race 后再跑一遍就知道原因了。
    plantparknet
        5
    plantparknet  
       2021-03-20 16:51:47 +08:00
    ```
    func main() {
    var x int
    go func(x *int) {
    for {
    *x ++
    }
    }(&x)
    time.Sleep(time.Duration(10) * time.Second)
    fmt.Println("**************")
    fmt.Println(x)
    fmt.Println("**************")
    }
    ```
    carlclone
        6
    carlclone  
       2021-03-20 17:01:15 +08:00
    我猜是被编译器优化掉了,把汇编代码输出出来看看
    dreasky
        7
    dreasky  
       2021-03-20 17:05:15 +08:00
    多个线程读写同一个资源加锁吧
    ```
    func main() {
    var x int64
    go func() {
    for {
    atomic.AddInt64(&x, 1)
    }
    }()
    time.Sleep(10 * time.Second)
    fmt.Println("**************")
    fmt.Println(atomic.LoadInt64(&x))
    fmt.Println("**************")
    }
    ```
    xuletter2021
        8
    xuletter2021  
    OP
       2021-03-20 17:16:05 +08:00
    @carlclone 嗯,我也想知道编译器如何处理的,数据竞争是确实的,但为什么这样就没有竞争了呢
    ```
    func main() {
    var x int
    go func() {
    for {
    x++
    fmt.Println("ddd")
    }
    }()
    time.Sleep(time.Duration(2) * time.Second)
    fmt.Println("**************")
    fmt.Println(x)
    fmt.Println("**************")
    }
    ```

    执行上面的代码
    ```
    ~/go/src/awesomeProject/test go run -race testX.go | grep -v 'ddd'
    **************
    676626
    **************
    ```
    dreasky
        9
    dreasky  
       2021-03-20 17:22:14 +08:00   ❤️ 1
    whee1
        10
    whee1  
       2021-03-20 17:22:56 +08:00   ❤️ 1
    这是未定义的行为。
    你要并发操作 x,需要它是原子的或者用 channel 传值,或者加锁。
    carlclone
        11
    carlclone  
       2021-03-20 17:23:09 +08:00
    汇编代码 : https://paste.ubuntu.com/p/67nDFqJXVN/ , 看最下面的 4 行,确实被优化掉了 d.go 11,12 行是 for 和 x++
    777777
        12
    777777  
       2021-03-20 17:24:01 +08:00
    调用 print 的时候会产生系统资源调用,所以没被优化
    jasonkayzk
        13
    jasonkayzk  
       2021-03-20 17:28:50 +08:00
    @carlclone 我尝试禁用编译优化:go build -gcflags '-N' main.go
    发现结果还是 0 !这是啥情况= =;
    whoami9894
        14
    whoami9894  
       2021-03-20 18:03:24 +08:00
    整个 goroutine 匿名函数被优化掉了
    0x0045 00069 (.\t.go:8) MOVQ "".&x+24(SP), AX
    0x004a 00074 (.\t.go:8) INCQ (AX)
    darrh00
        15
    darrh00  
       2021-03-20 18:08:48 +08:00
    都发生 data race 了,程序的行为就是未定义行为,跟输出值是不是 0 有任何关系? 以为输出值不是 0,程序就对了?
    RedBlackTree
        16
    RedBlackTree  
       2021-03-20 18:12:16 +08:00
    两个 goroutine,在两个线程、两个 CPU 上执行,你不对共享内存的读写进行同步操作,A 在写 A 的 cache 里的 x,B 在读 B 的 cache 里的 x,怎么可能有值呢?
    RedBlackTree
        17
    RedBlackTree  
       2021-03-20 18:13:19 +08:00
    操作系统没学好就算了,罚你今天晚上把 Go Memory Model 看三遍。
    treblex
        18
    treblex  
       2021-03-20 18:20:12 +08:00
    package main

    import (
    "fmt"
    "time"
    )

    func main() {
    var x = 0

    go func(_x *int) {
    *_x++
    }(&x)

    time.Sleep(time.Second * 3)
    fmt.Print(x)
    }
    Linxing
        19
    Linxing  
       2021-03-20 18:22:09 +08:00
    go 的并发模型了解下。
    Orlion
        20
    Orlion  
       2021-03-20 18:25:44 +08:00
    @xuletter2021 确实结果一样,不过关闭优化前后打印汇编结果还是有区别的,虽然关闭优化后的 go 出来的函数中还是没有 x++对应的汇编代码(不太能理解的...)。

    下面是我的猜测:

    在 1.14 之前协程调度是出让式而非抢占式的,如果这段代码在单核机器上运行,就有可能陷入到 for {...}的死循环中而主协程中的代码得不到调度执行,而你 for 循环中加入了 fmt.Pxxx 类的代码就能够使子协程出让执行权。

    另外这段代码还有可见性问题,子协程对全局变量的修改,主协程可能是看不到的。

    基于上面两个问题的考虑,编译器做出了“错误”优化,导致了你所看到的结果。
    lysS
        21
    lysS  
       2021-03-20 19:56:46 +08:00
    被优化了(伊,怎么感觉乖乖的)
    你把 // fmt.Println("ddd")的注释取消有可以了
    lewinlan
        22
    lewinlan  
       2021-03-20 20:06:58 +08:00 via Android
    楼上说的缓存问题的确值得学习,但应该不是这个问题的原因,10 秒怎么得也刷到 L3 了才对。
    看了下汇编,优化成了空循环了,所以应该是优化的问题。
    xfriday
        23
    xfriday  
       2021-03-20 22:46:32 +08:00
    这个问题我之前在 github 上提过 issue,go 的编译器会把这种情况下的修改的代码优化掉的,也只有 golang 会这么做,别的语言只是不保证可见性,但最终一定会读取到新值(并非所有场景都需要完全的一致性性),在 golang 里却永远读不到新值。
    sikasjc
        24
    sikasjc  
       2021-03-22 11:35:37 +08:00
    可以看这里,同样的问题,可以看汇编代码发现区别 https://www.zhihu.com/question/434964023
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2699 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 15:16 · PVG 23:16 · LAX 07:16 · JFK 10:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.