golang底层 内存模型
var x, y int
go func() {
x = 1
fmt.Print("y:", y, " ")
}()
go func() {
y = 1
fmt.Print("x:", x, " ")
}()
结果可能为 y:0 x:0 ,
编译器看到打印和赋值的变量不同,认为交换两条语句不会影响结果,可能会交换两条语句
两个goroutine如果在不同的cpu上执行,在主存同步之前,是看不到另一个goroutine的写入的
竞争条件检测:-race,只能检测到运行时的竞争条件
竞争条件 (race condition),也有的叫竞态,指计算的正确性依赖于相对时间顺序(relative Timing)或线程的交错(Interleaving)。
多个goroutine访问共享变量,且至少一个goroutine执行写入操作时,会发生数据竞争。
竞争条件往往伴随着读脏数据(Dirty Read)问题,即读取到过时的数据;丢失更新(Lost Update)问题,即一个线程的更新没有体现在其他线程的读取上。
竞争条件有两种模式:读-改-写 (read-modify-write),检测而后行动 (check-then-act)
原子性
对共享变量的操作,要么已经执行结束,要么还未执行,其他线程不会"看到"中间状态。
要实现原子性,要么用锁,要么用CAS,而CAS可看成是硬件层面的锁
例子:在32位机器上写int64时,其他线程可能会读到中间状态
可见性
一个线程更新了共享变量后,其他线程可以看到更新的结果。要保障可见性,需要让更新共享变量的cpu冲刷其缓存,并让读取共享变量的cpu刷新其缓存。
例子1:for b {...},另一个goroutine将b改为false,for循环仍将继续,
因为编译器以为b只在当前goroutine内被访问,为避免重复读取变量b,
编译器将代码优化成: if b { for {...} } 。例子是java改的,不能代表go编译器,
在java中解决这个问题的方法是用volatile修饰变量b,作用是防止编译器优化,
且写volatile变量会冲刷处理器缓存,读volatile变量会刷新处理器缓存
例子2:变量存在寄存器里,而一个cpu无法读取另一个cpu寄存器里的数据
例子3:即使变量被写回内存,该变量可能只被更新到cpu的写缓冲器中,
而一个cpu是无法读取另一个cpu的写缓冲器的
例子4:cpu把变量写到高速缓存中,并通知其他cpu,
其他cpu可能只是将通知内容存入无效化队列,并没有更新自己的告诉缓存,
后续读到的内容仍然是过时的
例子5:单线程情形下,一个线程可能将寄存器保存到上下文里,另一个线程看不到该线程的修改。
有序性
一个线程所执行的内存访问操作,在另一个线程看来是有序的。
编译器指令重排会导致源码顺序与指令顺序不一致
处理器乱序执行会导致指令顺序与执行顺序不一致,指令按顺序被处理器读取(顺序读取),然后哪条指令就绪就先执行哪条(乱序执行),将结果存入重排序缓冲器,按读取顺序提交给寄存器或内存(顺序提交),由于结果是按顺序提交的,所以不会对单线程程序产生影响。猜测执行会导致 if 语句的语句体先于条件语句被执行,例如a=0,一个线程执行:a=1; b=true 。另一个线程执行:if b { c=a }。c可能为0
存储子系统重排序,或者叫做内存重排序(Memory Ordering),会导致执行顺序和感知顺序不一致,
重排序:不影响单线程程序的正确性,但可能会影响多线程程序的正确性。为了保证貌似串行语义,存在数据依赖关系的语句不会被重排序
冲刷处理器缓存:将数据写入高速缓存或主存中(而不是写缓冲区中)
刷新处理器缓存:从其他cpu的高速缓存或主存中进行缓存同步
锁 能够保护共享数据,实现线程安全
原子性:锁是互斥的,一个线程执行临界区代码时,其他线程不能访问共享数据,
只能等待临界区执行结束
可见性:java里,获取锁会刷新处理器缓存,释放锁会冲刷处理器缓存
有序性:临界区的操作是原子性的,在其他线程看来,操作结果是同一时刻被更新的,
所以其他线程无法也没有必要区分执行顺序,可以认为代码是按序执行的
临界区内的代码可能会被重排序,但不会重排到临界区之外
也可能被cpu乱序执行,但不会和临界区之外的代码乱序
读取操作也是要加锁的
内存屏障(Memory Barrier 或 Fence)
内存屏障插入到两条指令之间,就像插入一堵墙,防止重排序,且往往会冲刷和刷新处理器缓存
按可见性划分,内存屏障可分为加载屏障和存储屏障,加载屏障会刷新处理器缓存,存储屏障会冲刷处理器缓存,java虚拟机会在释放锁的指令后插入一个存储屏障,在申请锁的指令后插入加载屏障
按有序性划分,内存屏障可分为获取屏障和释放屏障。在读操作之后插入获取屏障,可防止读操作与之后的操作的重排序。在写操作之前插入释放屏障,可防止写操作与之前的操作的重排序。这两种屏障可将临界区围起来,防止临界区的代码被重排到临界区之外。
MESI ( Modified-Exclusive-Shared-Invalid ) 是一种广为使用的缓存一致性协议,类似于读写锁。
4种状态
I (invalid,无效),是缓存条目的初始状态,不包含有效数据
S (shared,共享),包含有效的、和主内存一致的数据,且其他处理器也包含该数据
E (exclusive,独占),包含有效的、和主内存一致的数据,且其他处理器不包含该数据
M (modified,修改),包含修改过的、和主内存不一致的数据,
由于MESI协议一个时刻只能有一个cpu对某一内存地址的数据进行修改,
所以对于该数据而言,一个时刻只会有一个处理器上的条目处于M状态
6种消息
MESI定义了一组消息,处理器在某些情况下会往总线发送请求消息,处理器还会嗅探总线上的消息并发送响应消息
Read(请求),通知其他cpu和主存,当前cpu要读取某个地址的数据。
Read Response(响应),包含被请求的数据,由主存或其他cpu提供
Invalidate(请求),通知其他cpu,将某地址的缓存条目置为I,即删除该数据副本
Invalidate Acknowledge(响应),表示已删除该数据副本(已做标记)
Read Invalidate(请求),Read+Invalidate,读然后更新,
其他cpu要回复Read Response和Invalidate Acknowledge
Writeback(请求),将数据写入内存
。。。待看。。。
单例模式 (改自Java,不能代表Go编译器)
1,以下实现会导致多次执行 new( A )
if instance == nil { instance = new( A ) }
return instance
2,以下实现会导致每次获取A对象,都会加锁
l.Lock()
if instance == nil { instance = new( A ) }
l.Unlock()
return instance
3,两次加锁,会返回未初始化的A对象,instance = new(A)被分成了三步,
a)分配内存 b)初始化 c)赋值给instance,编译器可能会执行 a->c->b,
执行完a->c后,instance就不是nil了,其他线程会得到一个未初始化的instance
if instance == nil {
l.Lock()
if instance == nil { instance = new( A ) }
l.Unlock()
}
return instance
4,java的解决方法是在3的基础上,用volatile修饰instance,这样就不会重排序