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

go 一个线程写, 另外一个线程读, 为什么不能保证最终一致性?

  •  2
     
  •   xiadada · 352 天前 · 4277 次点击
    这是一个创建于 352 天前的主题,其中的信息可能已经有所发展或是发生改变。
    package main
    
    import (
        "fmt"
        "runtime"
        "sync"
        "time"
    )
    
    var lock sync.RWMutex
    var i = 0
    
    func main() {
        runtime.GOMAXPROCS(2)
        go func() {
            for {
                fmt.Println("i am here", i)
                time.Sleep(time.Second)
            }
        }()
        for {
            i += 1
        }
    }
    

    结果始终是 0, 考虑 cpu cache 一致性的话, 过一段时间就会看到变量发生了变化啊?

    第 1 条附言  ·  352 天前
    感谢 @Monad 大神,还有另一位提出 race 的小伙伴

    我终于解决这个问题了.

    这段代码跟 cpu cache 没什么关系, 主要是被编译器错误的优化掉了.

    go tool objdump -s "main\.main" example
    可以看到根本没有执行 add 操作.

    但是,当我们用 go build -race 去编译的时候

    o build -race example.go
    go tool objdump -s "main\.main" example

    清晰的 example.go:18 0x10e820a 83c077 ADDL $0x77, AX

    这种情况下, cache 的一致性协议会让其他 go 协程最终读到变化的值.

    至于内存屏障是说, 在 cache 最终一致性之前, cpu 为了性能, 有一个缓存, 这个时候是有脏读出现的. 如果你并发去写, 不用内存屏障, 就会遇到问题.(大概是这么个意思)
    42 回复  |  直到 2019-04-27 16:07:07 +08:00
        1
    Monad   352 天前
    编译器是可以优化的, 并且没有 memory barrier
        2
    wkc   352 天前   ♥ 1
    `go run -race a.go` 就能看到预期变化了
        3
    xiadada   352 天前
    @wkc 确实,还没这么用过
        4
    xiadada   352 天前
    @Monad 我看了一下汇编代码, 确实是被编译器编译掉了, 他可能认为`i`没有变化, 根本没有 Add 过.
        5
    zsxzy   352 天前
    没有 volatile , 这个怎么解决
        6
    xiadada   352 天前
    @Monad 再请教一下啊, 内存屏障和 cache 一致性有关系吗? 网上说写屏障会发消息让其他 cache 失效, 如果不设置屏障, 难道不会达成最终一致性吗?
        7
    xiadada   352 天前
    @zsxzy go 用更上一层的读写锁
        8
    Monad   352 天前
    @xiadada #6 在 i386 和 x86_64 上应该是会的 其它架构我就不清楚了 这里的主要原因其实还是没有屏障导致编译器优化掉了
        9
    polythene   352 天前
    @wkc 请教一下,这里的“-race ”做了什么神奇的操作导致 i 发生了变化?
        10
    rrfeng   352 天前
    看不懂……有没有人详细解释一下
        11
    lostsquirrelX   352 天前
    按按 go tour 的说法有两种方式
    一种是 chan
    一种是 把你的变量和锁放在一个结构体里面
        12
    finalsatan   352 天前
        13
    seaswalker   352 天前 via iPhone
    需要一个 compiler 屏障就行了呗
        14
    scnace   352 天前
    没看懂这个 lock 定义在这干啥。。。
        15
    WildCat   352 天前
    @scnace +1 同样没看懂
        16
    CRVV   352 天前
    @polythene
    楼主发的代码包含 data race,-race 打开了 data race detector,用来检查这个错误,为了检查错误关了相关的编译器优化

    @lostsquirrelX
    变量和锁不用放在一个结构体里,随便怎么放都行

    曾经有一个和这事相关的 bug
    https://github.com/golang/go/issues/19182
        17
    scnace   352 天前
    https://golang.org/doc/articles/race_detector.html 其实 Go 文档还是挺详细的。。。
        18
    xfriday   352 天前
    在 i +=1 下面添加一行 runtime.Gosched() 结果就是你期望的
        19
    gabon   352 天前 via Android
    volatile
        20
    yangxin0   352 天前
    这个要从 memory model 说起。i += 1 其实是两个指令:
    mov i, %eax
    add %eax, 1
    所以当你在 for { i += 1}的时候存在两个 instructions, 而另一个读取 print i 的时候可能在 mov 之后也可能在 add 之后。所以你这个一致性要是不增加 memory fence 基本无解。

    解法有几种:
    1、原子 add
    2、chan 传递数据
    3、mutex 或者 rwlock
        21
    yangxin0   352 天前
    gcc 有一个__sync_add_and_fetch 就主要用了 memory fence 和 instruction reorder 技术来保证 memory model 的一致性。
        22
    tempdban   352 天前 via Android
    @yangxin0
    mov i, %eax
    add %eax, 1
    mov %eax, I
        23
    yangxin0   352 天前
    @tempdban 噗写漏了。
        24
    tempdban   352 天前 via Android
    内存屏障是解决顺序一致性的问题,怎么到了楼上的说法怎么全是解决 cache 一致性了。
        25
    tempdban   352 天前 via Android
    @yangxin0 内存屏障不是解决 cache 一致性问题的
        26
    yangxin0   352 天前
    我的理解他这个问题就是一个顺序一致性问题,读 thread 读取 i 的时候,写 thread 可能正在进行一个非原子的+=1,这里就出现不一致。
        27
    cholerae   352 天前
    有 race 的 Go 程序的行为是未定义行为,理论上出现什么情况都是正常的,你这个示例程序极好地显示了这一点。所以讨论为什么出现这种现象实际上没有任何意义,不要依赖这种行为。理论上这个程序一运行就自动打开一个游戏也是合理的,好像有一个版本的 GCC 对待未定义行为就是这样做的。
        28
    conoha   352 天前
    @CRVV 大牛~
        29
    styx   352 天前
    @tempdban 是对的。x86 的 mfence 只解决 read-after-write 可能出现的 speculative/reorder 的情景,用于保证 sequential consistency。至于 @yangxin0 说的,跟 sequential consistency 没有关系,而且“另一个读取 print i 的时候可能在 mov 之后也可能在 add 之后”是完全合适也正确的访存行为。
        30
    tempdban   352 天前
    @yangxin0 看来我说的不够详细,内存屏障是解决 LOAD/STORE 乱序的问题。
    例如这种情况:
    a = (char *) melloc();
    dev.buff = a;
    mb();
    dev.flag = 1;
    很好理解吧,填 buff,置 flag。
    另一个线程发现 dev.flag == 1 就开始取 buff。
    但是 cpu 的执行单元是乱序的(注意:假定编译器得到的顺序是对的,这里还有个 Optimization Barrier 的问题),如果不加屏障就可能是这样:
    dev.flag = 1;
    dev.buff = a;
    另一个线程发现 flag 置 1 了去读 buff,此时 buff 指针可能还没来得及填,直接一个段错误歇菜了。
    内存屏障实际作用是:保证 MFENCE 指令前的 LOAD/STORE,一定在 MFENCE 指令之后的 LOAD/STORE 指令之前完成。
    回到你的理解:
    写 thread 可能正在进行一个非原子的+=1
    首先他只有一个线程在加,就算不是原子加那也不会影响别人读数,最多读的不是准确值,但是绝不会一直是 0。
    要是有多个线程再加同一个数,就算不是原子加,最后肯定有 core 会成功写到 cache 上的,也不会一直是 0。

    题主说的真没错,不是什么高深的问题,就仅仅是编译器把
    i += 1
    给优化掉了。
    仅此而已。
        31
    styx   352 天前
    @tempdban 唉,前面还说你结论是对的。你的这个例子确实是错的,你这里两个都是 store,x86 的 TSO 是保证 store 顺序的,所以另一个线程看到了 flag==1 一定能看到 buff==a,因为 store buffer 是按顺序刷到 cache 里去的。正确的关于 mfence 的例子应该是:
    Thread 1:
    a = 1
    // mfence
    if (b == 0) {
    enter_critical_section()
    }

    Thread 2:
    b = 1
    // mfence
    if (a == 0) {
    enter_critical_section()
    }

    如果不加 fence,则会出现两个线程同时进入 critical section 的情景,这是 Dijkstra 最早提出的 mutex 方法。

    ---
    当然我们都走远了,题主的问题是一个简单的问题。
        32
    styx   352 天前   ♥ 1
    @tempdban 抱歉,应该说在 x86 下你的例子是不会跑错的,因为 TSO。在 ARM 下你的例子应该就是合适的。
        33
    tempdban   352 天前
    @styx 是我考虑的不仔细,随手写的确实没考虑 x86 STORE 保序的问题。
        34
    tempdban   352 天前 via Android
    @styx 你不说我还真记不起来 TSO 这个事,得好好谢谢你。
        35
    styx   352 天前 via Android
    @tempdban 其实也不是记着 tso,因为 x86 的 tso 只允许 R-A-W 这一种 reorder,所以这种 sequential consistency violation 的例子是比较唯一的,就是各种 mutex 嘛。反倒是理解 store buffer 和 speculation execution 比较重要。
        36
    yangxin0   351 天前
    @styx
    @tempdban 多谢回复,我再去看看相关资料。
        37
    xiadada   351 天前
    @cholerae 是的, 我在 https://stackoverflow.com 也问了这个问题, 就被他们这么教育了, 我对未定义行为的认识不够, 不过了解一下到底为什么这种未定义之后, 到底发生了什么, 为什么会这样, 还是挺好玩的, 要不然难受的慌.
        38
    conoha   351 天前
    @xiadada 为什么都在关心 happens before...? happens before 发生在 i =0; x= i * 4; 值有依赖的情况,@CRVV 发的 github 才是正解啊,修 bug 前这个 routine 直接没有被调度到
        39
    xiadada   351 天前
    @conoha 不是一码事啊老哥, 用 atomic 还打印 0 说明是程序 bug. 我没有用, 会出现竞态, 编译器直接把 Add 这个操作优化没了. 不是没有调度的问题

    ```
    package main

    import (
    "fmt"
    "os"
    "runtime"
    "time"
    )

    var a uint64 = 0

    func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    fmt.Println(runtime.NumCPU(), runtime.GOMAXPROCS(0))

    go func() {
    for {
    a += 1
    // just do something
    _ = make(chan os.Signal)
    }
    }()

    for {
    fmt.Println(a)
    time.Sleep(time.Second)
    }
    }

    ```
    加一句 make, 就可以不是 0 了, 难道加了 make 就会解决调度问题?
        40
    xiadada   351 天前   ♥ 1
    Looking at the assembly, the increment (and in fact the whole for loop) has been (over-)optimized away.
        41
    reus   350 天前
    for 循环当作 dead code 优化掉了
        42
    ms2008   49 天前
    @xiadada 能贴个 stackoverflow 的链接吗?
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1811 人在线   最高记录 5043   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 21ms · UTC 01:28 · PVG 09:28 · LAX 18:28 · JFK 21:28
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1