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 Monad 2018-06-28 15:32:45 +08:00 编译器是可以优化的, 并且没有 memory barrier |
![]() | 2 wkc 2018-06-28 15:41:54 +08:00 ![]() `go run -race a.go` 就能看到预期变化了 |
![]() | 5 zsxzy 2018-06-28 16:09:14 +08:00 没有 volatile , 这个怎么解决 |
6 xiadada OP @Monad 再请教一下啊, 内存屏障和 cache 一致性有关系吗? 网上说写屏障会发消息让其他 cache 失效, 如果不设置屏障, 难道不会达成最终一致性吗? |
![]() | 8 Monad 2018-06-28 16:20:52 +08:00 @xiadada #6 在 i386 和 x86_64 上应该是会的 其它架构我就不清楚了 这里的主要原因其实还是没有屏障导致编译器优化掉了 |
![]() | 10 rrfeng 2018-06-28 16:31:48 +08:00 看不懂……有没有人详细解释一下 |
11 lostsquirrelX 2018-06-28 16:50:45 +08:00 按按 go tour 的说法有两种方式 一种是 chan 一种是 把你的变量和锁放在一个结构体里面 |
![]() | 12 finalsatan 2018-06-28 18:52:44 +08:00 |
![]() | 13 seaswalker 2018-06-28 18:58:49 +08:00 via iPhone 需要一个 compiler 屏障就行了呗 |
14 scnace 2018-06-28 19:06:23 +08:00 没看懂这个 lock 定义在这干啥。。。 |
16 CRVV 2018-06-28 19:11:47 +08:00 ![]() @polythene 楼主发的代码包含 data race,-race 打开了 data race detector,用来检查这个错误,为了检查错误关了相关的编译器优化 @lostsquirrelX 变量和锁不用放在一个结构体里,随便怎么放都行 曾经有一个和这事相关的 bug https://github.com/golang/go/issues/19182 |
17 scnace 2018-06-28 19:13:16 +08:00 https://golang.org/doc/articles/race_detector.html 其实 Go 文档还是挺详细的。。。 |
![]() | 18 xfriday 2018-06-28 21:23:57 +08:00 在 i +=1 下面添加一行 runtime.Gosched() 结果就是你期望的 |
19 gabon 2018-06-28 23:15:36 +08:00 via Android volatile |
![]() | 20 yangxin0 2018-06-28 23:33:18 +08:00 这个要从 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 2018-06-28 23:39:19 +08:00 gcc 有一个__sync_add_and_fetch 就主要用了 memory fence 和 instruction reorder 技术来保证 memory model 的一致性。 |
24 tempdban 2018-06-28 23:59:48 +08:00 via Android 内存屏障是解决顺序一致性的问题,怎么到了楼上的说法怎么全是解决 cache 一致性了。 |
![]() | 26 yangxin0 2018-06-29 00:06:24 +08:00 我的理解他这个问题就是一个顺序一致性问题,读 thread 读取 i 的时候,写 thread 可能正在进行一个非原子的+=1,这里就出现不一致。 |
27 6ufq0VLZn0DDkL80 2018-06-29 00:15:47 +08:00 有 race 的 Go 程序的行为是未定义行为,理论上出现什么情况都是正常的,你这个示例程序极好地显示了这一点。所以讨论为什么出现这种现象实际上没有任何意义,不要依赖这种行为。理论上这个程序一运行就自动打开一个游戏也是合理的,好像有一个版本的 GCC 对待未定义行为就是这样做的。 |
![]() | 29 styx 2018-06-29 00:27:03 +08:00 |
30 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 给优化掉了。 仅此而已。 |
![]() | 31 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 方法。 --- 当然我们都走远了,题主的问题是一个简单的问题。 |
![]() | 35 styx 2018-06-29 01:54:15 +08:00 via Android @tempdban 其实也不是记着 tso,因为 x86 的 tso 只允许 R-A-W 这一种 reorder,所以这种 sequential consistency violation 的例子是比较唯一的,就是各种 mutex 嘛。反倒是理解 store buffer 和 speculation execution 比较重要。 |
37 xiadada OP @cholerae 是的, 我在 https://stackoverflow.com 也问了这个问题, 就被他们这么教育了, 我对未定义行为的认识不够, 不过了解一下到底为什么这种未定义之后, 到底发生了什么, 为什么会这样, 还是挺好玩的, 要不然难受的慌. |
38 conoha 2018-06-29 12:19:27 +08:00 @xiadada 为什么都在关心 happens before...? happens before 发生在 i =0; x= i * 4; 值有依赖的情况,@CRVV 发的 github 才是正解啊,修 bug 前这个 routine 直接没有被调度到 |
39 xiadada OP @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 OP ![]() Looking at the assembly, the increment (and in fact the whole for loop) has been (over-)optimized away. |
![]() | 41 reus 2018-06-30 17:18:16 +08:00 for 循环当作 dead code 优化掉了 |