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

2018-06-28 15:29:32 +08:00
 xiadada
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 一致性的话, 过一段时间就会看到变量发生了变化啊?

6489 次点击
所在节点    Go 编程语言
42 条回复
yangxin0
2018-06-28 23:39:19 +08:00
gcc 有一个__sync_add_and_fetch 就主要用了 memory fence 和 instruction reorder 技术来保证 memory model 的一致性。
tempdban
2018-06-28 23:58:12 +08:00
@yangxin0
mov i, %eax
add %eax, 1
mov %eax, I
yangxin0
2018-06-28 23:58:53 +08:00
@tempdban 噗写漏了。
tempdban
2018-06-28 23:59:48 +08:00
内存屏障是解决顺序一致性的问题,怎么到了楼上的说法怎么全是解决 cache 一致性了。
tempdban
2018-06-29 00:00:17 +08:00
@yangxin0 内存屏障不是解决 cache 一致性问题的
yangxin0
2018-06-29 00:06:24 +08:00
我的理解他这个问题就是一个顺序一致性问题,读 thread 读取 i 的时候,写 thread 可能正在进行一个非原子的+=1,这里就出现不一致。
cholerae
2018-06-29 00:15:47 +08:00
有 race 的 Go 程序的行为是未定义行为,理论上出现什么情况都是正常的,你这个示例程序极好地显示了这一点。所以讨论为什么出现这种现象实际上没有任何意义,不要依赖这种行为。理论上这个程序一运行就自动打开一个游戏也是合理的,好像有一个版本的 GCC 对待未定义行为就是这样做的。
conoha
2018-06-29 00:16:41 +08:00
@CRVV 大牛~
styx
2018-06-29 00:27:03 +08:00
@tempdban 是对的。x86 的 mfence 只解决 read-after-write 可能出现的 speculative/reorder 的情景,用于保证 sequential consistency。至于 @yangxin0 说的,跟 sequential consistency 没有关系,而且“另一个读取 print i 的时候可能在 mov 之后也可能在 add 之后”是完全合适也正确的访存行为。
tempdban
2018-06-29 00:51:02 +08:00
@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
给优化掉了。
仅此而已。
styx
2018-06-29 01:08:41 +08:00
@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 方法。

---
当然我们都走远了,题主的问题是一个简单的问题。
styx
2018-06-29 01:10:50 +08:00
@tempdban 抱歉,应该说在 x86 下你的例子是不会跑错的,因为 TSO。在 ARM 下你的例子应该就是合适的。
tempdban
2018-06-29 01:21:11 +08:00
@styx 是我考虑的不仔细,随手写的确实没考虑 x86 STORE 保序的问题。
tempdban
2018-06-29 01:31:34 +08:00
@styx 你不说我还真记不起来 TSO 这个事,得好好谢谢你。
styx
2018-06-29 01:54:15 +08:00
@tempdban 其实也不是记着 tso,因为 x86 的 tso 只允许 R-A-W 这一种 reorder,所以这种 sequential consistency violation 的例子是比较唯一的,就是各种 mutex 嘛。反倒是理解 store buffer 和 speculation execution 比较重要。
yangxin0
2018-06-29 10:25:56 +08:00
@styx
@tempdban 多谢回复,我再去看看相关资料。
xiadada
2018-06-29 11:49:18 +08:00
@cholerae 是的, 我在 https://stackoverflow.com 也问了这个问题, 就被他们这么教育了, 我对未定义行为的认识不够, 不过了解一下到底为什么这种未定义之后, 到底发生了什么, 为什么会这样, 还是挺好玩的, 要不然难受的慌.
conoha
2018-06-29 12:19:27 +08:00
@xiadada 为什么都在关心 happens before...? happens before 发生在 i =0; x= i * 4; 值有依赖的情况,@CRVV 发的 github 才是正解啊,修 bug 前这个 routine 直接没有被调度到
xiadada
2018-06-29 14:28:24 +08:00
@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 就会解决调度问题?
xiadada
2018-06-29 14:29:51 +08:00
Looking at the assembly, the increment (and in fact the whole for loop) has been (over-)optimized away.

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/466586

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX