Go内存模型

go 内存模型

介绍

go 内存模型是在特定的情况下可以保证一个goroutine观察到的数据和另一个不同的goroutine写入的数据一致。

happens before

在一个goroutine中,写和读必须按照程序定义的顺序去执行。也就是说,只有在指令重排序不会改变语言规范定义的goroutine中的行为时,编译器和处理器才可以重新排序在单个goroutine中执行的读和写操作。 由于指令重排序,一个goroutine 观察到的是顺序可能和另外一个goroutine 观察到的顺序不同。例如 a=1;b=2;其他goroutine可能观察到ba之前更新数据。

为了明确读和写的顺序,引入了happens before概念,来表示Go程序中内存操作执行的部分顺序。如果e1 happens before e2 ,我们也可以说e2 happens after e1; 如果e1 不happens before e2 也不happens after e2,那么我们可以说 e1和e2 是并发的。

在一个goroutinehappens before 的顺序就是程序的顺序

一个读操作r 读取数据v 如果能观察到v的写操作w 就得满足以下条件

  1. r 不happens before w
  2. 没有其他写操作在w 之后和r 之前

为了保证变量v的读操作r读取到v的写操作w,若w是唯一的写操作,假如符合以下两个条件 r 能够观察到w的操作后的数据

  1. w happens before r
  2. 对共享变量v的任何其他写入要么happens-before w,要么happens-after r。

这两个条件比之前的更强,它要求没有其他写操作于w或者r 同时发生。

在一个goroutine 中是没有并发的,r能够观察到最近w写入v的值。当多个goroutine 同时访问共享变量v时,他们必须
使用同步事件去建立happens-before 条件 确保读操作能够观察到预期的写操作。

变量v的类型值为0的初始化行为就像在内存模型中写入一样。

对大于单个机器字的值的读写行为与多个机器字大小的操作一样,顺序未指定。

同步

初始化

程序初始化在一个goroutine 中,但是这个goroutine可能创建其他的goroutine它们是并发执行的。

如果包p 导入了包q, qinit 函数happens-before 任何p的函数。main 方法happens-after 所有init方法.

goroutine 创建

go关键字声明启动一个goroutine happens-beforegoroutine启动。
例如以下程序:


var a string

func f() {
	print(a)
}

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

调用hello 方法将会打印出hello world.

goroutine 销毁

goroutine的退出无法保证 happens-before 任何事件,例如


var a string

func hello() {
	go func() { a = "hello" }()
	print(a)
}

a 的赋值没有任何的同步操作,因此无法保证它能被其他的goroutine 观察到,实际上偏激的编译器可能会删除该语句。

如果一个goroutine的影响必须被另一个goroutine观察到,可以使用一个同步机制,比如锁或者通道通信来建立一个相对的顺序。

channel 通讯

在两个goroutine 之间通信主要是通过channel(管道),通常有一个goroutine 在往一个特定的channel 发送消息,对应的还会有一个不同的goroutine在这个channel 接收消息。
一个发送消息到channel happens-before 从对应的channel接收消息完成


var c = make(chan int, 10)
var a string

func f() {
	a = "hello, world" //1    1 happens before 2
	c <- 0    //2
}

func main() {
	go f()
	<-c //3  // 3 happens  before 4
	print(a) //4
}
 

以上程序能保证main 函数打印出hello world。因为看上面序号 1 happens before 2 3 happens before 4 然后2 happens before 3 , 所以a 的赋值一定在打印之前。

关闭一个channel happens before 从该channel 中接收一个值(仅无缓冲channel
如下面的例子 调换了 c<- 0<- c 的位置 依旧能够保证程序打印出争取的值

var c = make(chan int)
var a string

func f() {
	a = "hello, world" //1
	<-c // 2
}

func main() {
	go f()
	c <- 0 //3
	print(a) //4 
}

以上例子依旧能够保证输出是hello world 因为 1 happens before 2 , 3 happens before 4, 又 2 happens before 3 (无缓冲channel 消息发送者会一直阻塞到接收者接收消息) 所以 1 happens before 4。

假如channel 是一个缓冲的通道(e.g. c=make(chan int,1)) 那么程序将无法保证打印出hello world (程序可能打印空字符串,崩溃 ,或者其他行为)

在有缓冲大小为C的通道中第k个接收者 heppens before 第 k+C 个发送者


sync 包实现了两种锁sync.Mutexsync.RWMutex
对于任何锁任何变量l 和n<m, 调用n 的 l.Unlock() happens before m的 l.Lock()返回

var l sync.Mutex
var a string

func f() {
	a = "hello, world"
	l.Unlock()
}

func main() {
	l.Lock()
	go f()
	l.Lock()
	print(a)
}

这也是能保证打印出hello world

ref: https://golang.org/ref/mem

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值