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 1
`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 1
@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
cholerae 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 优化掉了
|