Go并发编程-内存模型

Go并发编程-内存模型

​ 此处的内存模型并不是指Go对象的内存分配、内存回收、内存整理的规范。指的是在并发环境下goroutine读相同变量的时候,变量的可见性条件。换句话说就是,一个goroutine在读取变量时,在什么条件下,能够看到其他goroutine对这个变量进行赋值的结果。

​ 由于CPU的指令重排序和多级Cache的存在,保证多核条件下,访问同一个变量这件事情变得很复杂。所以编程语言需要一个规范用来明确多线程同时访问一个变量的可见性及顺序。这个规范就叫做内存模型

指令重排序导致的问题

​ 由于指令重排,代码并不一定会按照你写的顺序运行。但是保证结果正确。如以下代码,在多核cpu及重排的情况下导致程序运行和代码的书写顺序不一样的情况。

var a,b int
fun f() {
  a = 1 
  b = 2
}
fun g(){
  print(b)
  print(a)
}
fun main(){
  go f()
  g()
}

​ 这段代码在main函数中开启了一个goroutine调用f函数,随后调用了g函数。如果没有指令重排的情况下,如果g函数打印出来b是2时,按照代码书写的顺序,打印a的值一定是1。但是由于a和b的赋值并没有先后关系,由于指令重排,实际编译后的代码可能是b赋值2在前,a赋值1在后。这种情况下在打印b的值为2时,打印a可能为1,也可能为0。这样就导致了代码的不确定性。

Happens-before

​ 指令重排情况在单个goroutine下并不会有任何问题,但是在多线程情况下你的代码会出现偶尔的问题。这种不是必现的问题,对于程序猿来讲是最难排查的。所以我们必须要搞清楚在Go内存模型中的happens-before。以此来确保你程序在多线程情况下100%不会出现问题。

hanppens-before有一个重要的保证

在一个goroutine内部,程序执行顺序和她们的代码的指定顺序是一样的,即便进行了重排,从行为上来看,也和代码的制定顺序一样

within a single goroutine, the happens-before order is the order expressed by the program

Go语言中保证的happens-before关系

init函数

​ main函数一定在导入的包的init函数之后执行的。
​ 同一个包下可以有多个init函数,但是每个.go文件只能有一个init函数,多个init函数会按照他们的文件名顺序逐个初始化。

goroutine

启动goroutine的go语句的执行,一定happens before此goroutine内的代码执行。根据这个规则,如果go语句传入的是一个函数的执行结果,那么这个函数一定先于goroutine内部的代码执行。

var a string
fun f(){
  print(a)
}

fun hello(){
  a = "hello, world"
  go f()
}

​ 根据goroutine的规定,hello函数中的对a进行赋值一定先于启动goroutine。也就是说他先于print(a)函数的调用,所以打印a一定是打印hello, world。

channel

  1. 往channel中发送的操作,happens before 从该channel接收相应数据的动作完成之前。即第n个send一定happens before 第n个recevie的完成。

    var ch = make(chan struct{}, 10)//有buffered的channel与unbuffered均可以
    var s string
    
    fun f(){
      s = "hello, world"
      ch <- struct{}{}
    }
    
    func main(){
      go f()
      <- ch
      print(s)
    }
    
    1. close一个channel的调用,happens before从该channel中取出一个零值。
    var ch = make(chan struct{}, 10)//有buffered的channel与unbuffered均可以
    var s string
    
    fun f(){
      s = "hello, world"
      close(ch)
    }
    
    func main(){
      go f()
      <- ch
      print(s)
    }
    
  2. 对于unbuffered的channel ,从此channel中读取数据一定happens before往此channel发送数据的调用完成

    var ch = make(chan struct{})//有buffered的channel与unbuffered均可以
    var s string
    
    fun f(){
      s = "hello, world"
      <- ch
    }
    
    func main(){
      go f()
      ch <- struct{}{}
      print(s)
    }
    
  3. 对于buffered的channel,那么第n个receive一定happens before 第n+m个send完成。

Mutex /RWMutex

  1. 第n次的m.Unlock一定happens before 第n+1次m.Lock方法的返回
  2. 对于读写锁I的I.RLock方法的调用,如果存在一个n,这次的I.RLock调用happens after第n次的I.Unlock,那么,和这个RLock相对应的I.RUnlock一定happens before 第n+1次的I.Lock。读写锁的Lock必须等待既有的读锁释放后才能获取到。
var mu sync.Mutex
var s string

func f() {
	s = "hello, world"
	mu.Unlock()
}

func main() {
	mu.Lock()
	go f()
	mu.Lock()
	print(s)
}

WaitGroup

​ wait方法要等待计数值归零之后才返回

Once

​ 对于Once.Do(f)的调用,f函数的单次调用一定happens before任何once.Do(f)调用的返回。也就是说f一定会在Do方法返回之前执行

var s string
var once sync.Once

func f() {
	s = "hello, world"
}
func main() {
	once.Do(f)
	print(s)
}

总结

if you must read the rest of this document to understand the behavior of your program, you are beiing to clever

But don’t be clever

通过这节课来学习你程序的行为是非常聪明的,但是不要自作聪明。就是说不要以为自己完全理解了这些概念和保证,就可以随意的制造所谓的技巧。否则很容易掉进坑里,给代码留下定时炸弹。


原文地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值