Golang Go语言中 一个线程写, 另外一个线程读, 为什么不能保证最终一致性?
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 一致性的话, 过一段时间就会看到变量发生了变化啊?
Golang Go语言中 一个线程写, 另外一个线程读, 为什么不能保证最终一致性?
更多关于Golang Go语言中 一个线程写, 另外一个线程读, 为什么不能保证最终一致性?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
编译器是可以优化的, 并且没有 memory barrier
更多关于Golang Go语言中 一个线程写, 另外一个线程读, 为什么不能保证最终一致性?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
go run -race a.go
就能看到预期变化了
确实,还没这么用过
我看了一下汇编代码, 确实是被编译器编译掉了, 他可能认为i
没有变化, 根本没有 Add 过.
没有 volatile , 这个怎么解决
再请教一下啊, 内存屏障和 cache 一致性有关系吗? 网上说写屏障会发消息让其他 cache 失效, 如果不设置屏障, 难道不会达成最终一致性吗?
go 用更上一层的读写锁
#6 在 i386 和 x86_64 上应该是会的 其它架构我就不清楚了 这里的主要原因其实还是没有屏障导致编译器优化掉了
请教一下,这里的“-race ”做了什么神奇的操作导致 i 发生了变化?
看不懂……有没有人详细解释一下
按按 go tour 的说法有两种方式
一种是 chan
一种是 把你的变量和锁放在一个结构体里面
需要一个 compiler 屏障就行了呗
没看懂这个 lock 定义在这干啥。。。
+1 同样没看懂
楼主发的代码包含 data race,-race 打开了 data race detector,用来检查这个错误,为了检查错误关了相关的编译器优化
变量和锁不用放在一个结构体里,随便怎么放都行
曾经有一个和这事相关的 bug
https://github.com/golang/go/issues/19182
https://golang.org/doc/articles/race_detector.html 其实 Go 文档还是挺详细的。。。
在 i +=1 下面添加一行 runtime.Gosched() 结果就是你期望的
volatile
这个要从 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
gcc 有一个__sync_add_and_fetch 就主要用了 memory fence 和 instruction reorder 技术来保证 memory model 的一致性。
mov i, %eax
add %eax, 1
mov %eax, I
噗写漏了。
内存屏障是解决顺序一致性的问题,怎么到了楼上的说法怎么全是解决 cache 一致性了。
内存屏障不是解决 cache 一致性问题的
我的理解他这个问题就是一个顺序一致性问题,读 thread 读取 i 的时候,写 thread 可能正在进行一个非原子的+=1,这里就出现不一致。
有 race 的 Go 程序的行为是未定义行为,理论上出现什么情况都是正常的,你这个示例程序极好地显示了这一点。所以讨论为什么出现这种现象实际上没有任何意义,不要依赖这种行为。理论上这个程序一运行就自动打开一个游戏也是合理的,好像有一个版本的 GCC 对待未定义行为就是这样做的。
大牛~
是对的。x86 的 mfence 只解决 read-after-write 可能出现的 speculative/reorder 的情景,用于保证 sequential consistency。至于 说的,跟 sequential consistency 没有关系,而且“另一个读取 print i 的时候可能在 mov 之后也可能在 add 之后”是完全合适也正确的访存行为。
看来我说的不够详细,内存屏障是解决 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
给优化掉了。
仅此而已。
唉,前面还说你结论是对的。你的这个例子确实是错的,你这里两个都是 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 方法。
—
当然我们都走远了,题主的问题是一个简单的问题。
抱歉,应该说在 x86 下你的例子是不会跑错的,因为 TSO。在 ARM 下你的例子应该就是合适的。
是我考虑的不仔细,随手写的确实没考虑 x86 STORE 保序的问题。
你不说我还真记不起来 TSO 这个事,得好好谢谢你。
其实也不是记着 tso,因为 x86 的 tso 只允许 R-A-W 这一种 reorder,所以这种 sequential consistency violation 的例子是比较唯一的,就是各种 mutex 嘛。反倒是理解 store buffer 和 speculation execution 比较重要。
多谢回复,我再去看看相关资料。
是的, 我在 https://stackoverflow.com 也问了这个问题, 就被他们这么教育了, 我对未定义行为的认识不够, 不过了解一下到底为什么这种未定义之后, 到底发生了什么, 为什么会这样, 还是挺好玩的, 要不然难受的慌.
为什么都在关心 happens before…? happens before 发生在 i =0; x= i * 4; 值有依赖的情况,CRVV 发的 github 才是正解啊,修 bug 前这个 routine 直接没有被调度到
不是一码事啊老哥, 用 atomic 还打印 0 说明是程序 bug. 我没有用, 会出现竞态, 编译器直接把 Add 这个操作优化没了. 不是没有调度的问题<br>package main<br><br>import (<br> "fmt"<br> "os"<br> "runtime"<br> "time"<br>)<br><br>var a uint64 = 0<br><br>func main() {<br> runtime.GOMAXPROCS(runtime.NumCPU())<br> fmt.Println(runtime.NumCPU(), runtime.GOMAXPROCS(0))<br><br> go func() {<br> for {<br> a += 1<br> // just do something<br> _ = make(chan os.Signal)<br> }<br> }()<br><br> for {<br> fmt.Println(a)<br> time.Sleep(time.Second)<br> }<br>}<br><br>
加一句 make, 就可以不是 0 了, 难道加了 make 就会解决调度问题?
Looking at the assembly, the increment (and in fact the whole for loop) has been (over-)optimized away.
for 循环当作 dead code 优化掉了
能贴个 stackoverflow 的链接吗?
在Golang(Go语言)中,如果一个线程写数据而另一个线程读数据,且未使用适当的同步机制,确实无法保证最终一致性。这主要归因于以下几个原因:
-
内存可见性问题:在没有同步的情况下,一个线程对共享内存的写操作可能不会立即对其他线程可见。Go语言的运行时和编译器可能会进行指令重排序以优化性能,这可能导致读线程读取到过时或不一致的数据。
-
竞态条件:竞态条件发生在两个或多个线程同时访问共享资源,且至少有一个线程在写入该资源时。这可能导致数据损坏或不一致的状态,因为读线程可能在写线程完成更新之前读取了数据。
-
缓存一致性:现代处理器使用缓存来加速内存访问。然而,这可能导致缓存中的数据与主内存中的数据不一致。在没有适当的同步机制(如锁或原子操作)的情况下,这种不一致性可能无法被及时发现和解决。
为了保证最终一致性,Go语言提供了多种同步机制,如互斥锁(sync.Mutex
)、读写锁(sync.RWMutex
)和通道(channel
)等。这些机制可以确保在写操作完成之前,读操作不会开始,从而避免竞态条件和内存可见性问题。
因此,在Go语言中实现线程安全的读写操作,必须仔细考虑并使用适当的同步机制来确保数据的一致性和完整性。