写在开头
非原创,知识搬运工/整合工,仅用于自己学习,本文是对曹大内存重拍文章的阅读
往期回顾
- 基础章节
- GMP章节
带着问题去阅读
- 什么情况下会发生内存重排
- 内存重排的概念
- 内存重排的本质
- CPU的缓存策略是什么
- 如何避免内存重排
由问题引出主题
demo1
func TestProblem1(t *testing.T) {
runtime.GOMAXPROCS(1)
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
// 10 10 10 10 10
time.Sleep(2 * time.Second)
}
这里相信在学过之前的函数和GMP后都能分析出输出10的原因(runtime.GOMAXPROCS<1表示设置与核数相同的线程数量):
- 设置最大线程数为1,即一个P处理G
- 循环生成G,但是一个G的生成并不代表立即执行(要等待调度)
- 所有G生成完后,最后生成的G加入到p.runnext最先被执行,其余因为runnext已经被占用按顺序入队(runqput函数规则)
- go1.13版本time会额外生成一个G,但是之后就不会了,这里模拟阻塞
- 执行G时i不是参数,也不是函数局部变量,从括号外层层往上找,此时循环执行完,i=10,因此全部为10
修改一下demo1为demo2
func TestProblem2(t *testing.T) {
runtime.GOMAXPROCS(1)
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Println(i)
}()
}
// 9 0 1 2 3
time.Sleep(2 * time.Second)
}
这里因为i与循环的i不是同一个,因此为这样输出
上面例子是对GMP的一个简单复习,这里因为是模拟单核的缘故没有存在数据竞争,那我们再模拟多核呢?
demo3
func TestProblem3(t *testing.T) {
var x int
threads := runtime.GOMAXPROCS(0)
fmt.Println("threads = ", threads)
for i := 0; i < 8; i++ {
go func() {
for {
x++
}
}()
}
time.Sleep(2 * time.Second)
fmt.Println("x =", x)
}
最终结果输出
threads = 8
x = 0
为什么,不是应该存在数据竞争,x的值应该是一个大于0随机数的呀,不可能G都没执行吧,这个情况就要引入今天的主题内存重排
1.内存重排
内存重排指内存的读/写指令重排
一看到指令就想到CPU的汇编代码了,又要头疼。
我们引用曹大博客中的例子再来详细体验下内存重排
var x, y int
//G1
go func() {
x = 1 // A1
print(y) // A2
}()
//G2
go func() {
y = 1 // A3
print(x) // A4
}()
image.png
多核情况下G1和G2是等价的,那么就会出现下面几种我们认为合理的情况:
- 执行顺序:a1-2-3-4,输出 01
- 执行顺序:a3-4-1-2,输出 01
- 执行顺序:a1-3-2-4,输出 11
- 执行顺序:a1-2-4-2,输出 11
还有几意外的情况就是输出先于赋值执行的: - 执行顺序:a2-4-1-3,输出 00
- 执行顺序:a1-4-3-2,输出 10
那么我们就有如下猜想:众所周知,用户的代码最终会转为CPU指令,CPU的设计者为了榨干CPU无所不用,那为了提高CPU读写内存的效率(写指令更耗时),会不会对读写指令进行了重新排序?
实际上不是指令排序引起的,是CPU的缓存引起的,我们要从CPU的发展架构讲起
1.1CPU架构的变迁
CPU1.0
image.png
每次操作直接操作内存
CPU2.0
image.png
每个核中加入了高速缓存cache,写数据访问流程:
1.先去高速缓存中查看有无,有则直接修改cache数据
2.没有就去内存中读
3.内存读完后写入高速缓存
但是同时也带来了新问题,访问共享数据如上文的x和y,如何保证每个核上的cache数据一致呢,特别是共享数据x和y,那就是引入缓存一致协议,即如果是内存写的操作就需要广播,让其他核也一起更新,等都更新完后才返回。
这种协议是在牺牲性能的代价上换取数据的完整性
CPU3.0天降猛男(三级缓存策略)
image.png
- L1在核内(store buffer),存放指令核数据
- L2在核内(cache),高速缓存,比L1更大
- L3在核外,缓存队列,所有CPU共享一个L3
三级缓存如何保证数据一致性?这里只是简单总结下写协议(实际很复杂):
1.cache中无数据则直接写入cache,无需广播
2.cache有数据写入L3,然后直接返回
-
L3异步同步数据
所以就会出现这种情况,缓存数据还没同步到其他核,该旧数据就被读取了,称为脏读,读取的旧数据为脏数据(读数据是先从cache中读,不在再从内存读,cache中的数据有4种状态)
回到上文的xy代码
image.png
当a1执行后,不需要等待x=1进入L3就可以执行a2代码了,因为两个核是同时执行的G2也这样 image.png
神奇的情况发生了(此时两个核都分别没有y数据和x数据,读取时需要从内存读),a2和a4都从内存中读,但是L3还没更新,读取到了脏数据0
demo3是因为在循环中不断的更新x,不断刷新缓存,让L3来不及异步更新
总结与展望
内存重排实际是多线程情况下,CPU核缓存未更新导致的诡异现象,解决办法就是实验锁。但锁会带来性能问题,为降低影响,需要减小锁的粒度,并且不在互斥区放入耗时长的操作
网友评论