关于 golang 官网一段代码的疑惑

67 天前
 rockyliang

Golang 官网一段关于同步错误的代码示例:

var a string
var done bool

func setup() {
	a = "hello, world"
	done = true
}

func main() {
	go setup()
	for !done {
	}
	print(a)
}

官网对这段代码的解析原文:As before, there is no guarantee that, in main, observing the write to done implies observing the write to a, so this program could print an empty string too. Worse, there is no guarantee that the write to done will ever be observed by main, since there are no synchronization events between the two threads. The loop in main is not guaranteed to finish.


print(a)可能会输出一个空字符串这个比较好理解,因为 CPU 指令重排的原因,done = true有可能先于a = "hello world"语句执行,所以出现了这种情况。

但是 for 语句有可能会陷入死循环的原因我理解不了,原因是什么?

我猜测的原因是(有可能不正确): done = true的修改 CPU 只写入到了本地核心的 store buffer ,没有同步给其它 CPU 核心,导致不同核心的缓存数据不一致,接而导致运行在其它核心的 main 协程看不到这个修改,所以陷入了死循环

希望有大神能解析下原因,谢谢~

3937 次点击
所在节点    Go 编程语言
29 条回复
dhb233
66 天前
如果是循环不能结束的情况,只可能是编译器做优化了。C 语言也有类似的优化,C 可以给变量加 volatile 关键字解决
kuanat
66 天前
@rockyliang #20

这部分我也不太清楚,猜测影响的因素会有很多,编译器实现方式影响可能大一些。

如果仅从语言层面上说,Golang 的内存模型里面随机结果的种类会少一点,官方编译器倾向于减少这些不确定性。C 的话编译器很多,基本很难预期 UB 的结果。
xfriday
66 天前
如果看汇编结果,编译器把 a = "hello, world" 直接干没了
treetalk
66 天前
> First, if x and done are ordinary variables, then thread 2's loop may never stop. A common compiler optimization is to load a variable into a register at its first use and then reuse that register for future accesses to the variable, for as long as possible. If thread 2 copies done into a register before thread 1 executes, it may keep using that register for the entire loop, never noticing that thread 1 later modifies done.

rsc 的博客里有提到这种情况
tuoov
65 天前
@kuanat 可以请你推荐一些资料吗,我很想了解这些理论知识
fkdtz
65 天前
官网这篇文章确实有些晦涩,甚至文档一开始就表示如果想要搞协程,建议干脆直接用 channel 或 sync 包就完了,不用看文章内容。
可能官方文档出于严谨考虑,写的东西非常理论化,实际效果又有点反直觉,确实不好理解。
我读过几次心得主要是,文章说明了 go 在协程并发环境下可以提供的保证有哪些,也就是所谓 happens before 、synchronized before 这类保证,这种保证可以让开发者放心的写代码,因为只要符合这些保证生效的场景,就必然可以得到可预期的结果。
反之除了文章中提到的场景外,其他并发场景就不提供任何保证了,换句话说啥结果都可能冒出来。

至于为什么可能出现死循环,我想应该跟底层硬件有关了,例如 sync 包很多同步操作底层都涉及到屏障操作,就是让不同 CPU 寄存器中的数据可以在一致的前提下被读写,那么反之如果不使用任何同步原语,那也就不涉及到屏障操作,不同 CPU 可能持有的还是各自不同的副本...
kuanat
65 天前
@tuoov #25

楼上提到的 https://research.swtch.com/mm 就可以。

另外我印象里内存模型这个概念出现得比较晚,Java 应该是第一个把内存模型理论化的,在这之前的比如 C 都是先有语言后建模,之后的新语言比如 Golang 才是先设计内存模型,后规范语言。

Java 后来发现原本设计的内存模型有问题,所以有了 JSR 133 来做修正。可以通过 http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html 向外发散阅读。
luoashadow
38 天前
可否提供下官方文档链接
noyidoit
38 天前
感谢,从这个帖子中学到了很多。另外[原文见🔗]( https://go.dev/ref/mem)

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

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

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

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

© 2021 V2EX