-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
由Golang happens before 带来的学习与思考 #25
Comments
首先聊聊CPU cache。在现如今的cpu参数上,我们都会看到有叫L1 L2 L3 cache的东西。没错,它就是除了寄存器cpu能访问到的最快的存储器,为了避免cpu每次从内存去拿数据而浪费的算力。随着多核时代的到来,每个核都有自己的cache,便会出现同一个内存地址上的数据对象有多个副本分布在不同cpu核的cache上,且数据内容不一致。也就是数据一致性问题。 (这里顺带提一下Cache Line这个概念,它是CPU Cache的最小读取单元,一般为cpu字长,比如64bit。这也是为什么在编程语言里可以看到许多结构体会内存对齐,特意padding,避免false sharing问题。) 为了解决CPU cache的数据一致性问题(也可以说是内存可见性问题),比较著名的有在Intel系列cpu广泛使用的MESI协议。它实际代表了四种数据状态:Modified、Exclusive、 Share 、Invalid。这篇文章很好地描述了MESI协议的原理:聊聊缓存一致性协议。 虽然MESI协议确实能解决缓存一致性问题,但是消息在总线上的发送接收和相关的处理都是有成本的,会导致一些性能和稳定性问题。为了优化性能,cpu又引入了store buffer这个模块。详细的工作原理不太清楚,我的抽象理解就是延迟执行,间接引入了内存可见性问题 |
总结来说,有这么几个层次会导致指令重排(或者结果看起来是那样):
|
为了解决由上述几个点带来的可见性问题,同时为了屏蔽不同的系统平台底层硬件的差异,编程语言抽象出了memory model这个概念。只要开发者正确使用语言提供的同步原语,就能保证程序行为与预期一致。 内存模型: |
上面有说到CPU为了提高性能可能会乱序执行,进而多线程程序导致内存可见性问题。针对这点,处理器提供了相关的内存屏障(Memory Barrier)指令,来保证内存可见性:
推荐这篇文章给大家,写得很好: 聊聊内存屏障
|
接下来我们通过一个实际的例子(sync.Once的实现 ),来感受下上面所说的东西,在我们实际编码中应该如何应用使得程序行为符合预期。 |
首先明确sync.Once语义。它接收一个参数,类型为 f func()。且保证在任何情况下,once.Do(f)在f函数执行完后返回,f执行且仅执行一次(exactly once)。 明确语义之后,我们来看几个错误的实现sample,来了解官方源码实现为什么要这么写 |
1
这个现在是直接放在源码文档里的,我印象里之前是没有这段说明的(貌似是go官方被问烦了为什么不这么实现) 这个错误就是当某个goroutine A成功抢到cas,置o.done为1;同时另一个goroutine B检测到o.done为1,直接返回了。而此时,A中的f可能还没有执行完,而B已经返回,违背了once的语义 |
2
这个例子有以下问题:
Ref: ----- Updated 2021.3.5 ------- |
PS: rsc 在13年就提出建议需要一个描述sync.Atomic与memeroy model关系的说明。golang/go#5045 |
https://github.com/cch123/golang-notes/blob/master/memory_barrier.md (更完整的一篇讲memory barrier) |
首先想说明一下诞生背景,这篇文章缘起于在某个Go讨论群里看到有人贴了 https://golang.org/ref/mem 里的一段sample代码,说是a不一定保证打印"hello, world",也可能打印空字符串。代码如下
当时我是挺惊讶的(有点颠覆我的编码认知),于是读完了 go memory model这篇Go Blog。但说实话,收获也只是限于了解 Happens Before 语义,以及如何避免上面这种无法保证预期输出打印的代码产生(通过Go提供的同步原语,Once,Mutex,Channel)。
但是我是一个比较喜欢刨根究底的人,我仍旧不清楚为什么我们需要Mutex来保证 Happens Before 语义,进而保证写出的代码符合预期。
于是我根据已了解到的相关keyword,Google了一圈。然后发现涉及到的面非常多,也很底层,关乎到CPU执行优化,编译器指令重排,CPU cache ...
所以这篇blog不太可能面面俱到,更多的还是我自己学习整合其它文章的心得与体会,也难免会有错误。希望未来随着自己的精进,也能不断更新完善这篇blog
The text was updated successfully, but these errors were encountered: