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,这样就不会重排序

 

posted @ 2020-05-27 22:24  是的哟  阅读(460)  评论(0编辑  收藏  举报