go内存模型

go内存模型

原文链接:https://golang.org/ref/mem

介绍

go内存模型指定了在一个goroutine中对一个变量的读取,可以保证观察到被另一个不同的goroutine写到同一个变量的值。

建议

程序修改数据被多个goroutine同时访问必须保证必须序列化这样的访问。
为了序列化访问,用管道操作或者其他同步原语来保护数据,比如那些在syncsync/atomic包下的。
如果你必须阅读这篇文档剩余部分来理解你程序的行为,你已经太聪明了。
不要太聪明。

Happens Before

在一个单goroutine中,读取和写入必须表现得好像它们由程序指定按顺序执行。也就是,编译器和解析器只有在重排序不会改变语言声明定义的goroutine的行为时,才可能在一个单goroutine中对读写的执行进行重排序。
因为这种重排序,一个goroutine观察执行的顺序可能和另一个goroutine不同。举个例子,如果一个goroutine执行a=1;b=2;,另一个可能观察到b的值在a的值更新之前。

为了指定读写的需求,我们定义了happens before,在一个go程序中的内存操作的局部执行顺序。如果事件e1 happens before 事件e2,我们就说e2 happens after e1。并且,如果e1没有happens before e2,也不happens after e2,我们就说e1和e2并发发生。

在一个单goroutine中,happens-before 顺序就是程序表达的顺序。

变量v的读r允许观察到v的写w,如果满足以下两个条件:

  1. r没有happen before w。
  2. 没有其他对v的写w’ happens after w但是happen before r。

为了保证变量v的读r观察到对v特别的写w,确认w是唯一的写能让r被允许观察到。那就是,r被保证观察到w如果以下两个条件都持有:

  1. w happens before r
  2. 任何其他对变量v的写都happens before w,或者happens after r。

这对条件比第一对更强大;它要求没有其他写操作同时发生在w或者r上。

在一个单独的goroutine,没有并发,所以两个定义是等效的:一个读r观察到最近对v的写入w操作。当多个goroutine访问一个共享的变量v时,它们必须使用同步时间来简历happen-before条件来确保读观察到期望的写。

为变量v的类型初始化为零值在内存模型中,表现得就像一个写操作。

读和写的值比单个机器码更大,表现得就像在一个未指定顺序的多个机器码长度的操作。

Synchronization

Initialization

程序初始化运行在单goroutine中,但是这个goroutine可能会创建其他并发的goroutines。

如果一个package p imports package q,那么q的init函数的完成 happens before于任何p的init函数的开始。

main.main函数的开始,happens after于所有的init函数的完成。

Goroutine creation

go关键字声明的是开启一个新的goroutine happens before于这个goroutine的执行开始。
举个例子,在下面这个程序中:

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

调用hello将会打印"hello,world"在将来的某些瞬间(也许在hello已经返回以后)

Goroutine destruction

一个goroutine的退出不保证happen before于程序中运行的任何事件。例如,在这个程序中:

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

对a的声明没有跟随任何的同步事件,所以这无法保证被任何其他goroutine观察到。实际上,一个有进取心的边以及可能会删除这整个go的声明。

如果一个goroutine的影响必须被其他goroutine观察到,使用一个同步机制比如说一个lock锁或者channel通信来建立一个关键的顺序。

Channel communication(管道通信)

管道通信是goroutines之间同步的主要方法。每一个发送到一个特别的管道会被匹配给一个相应的不同的goroutine从管道中接收。

一个管道的发送 happens before管道完成接收。

下面这个程序:保证会运行"hello, world"。

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

func f() {
	a = "hello,world"
	c <- 0
}

func main() {
	go f()
	<-c
	print(a)
}

对a的写 happens before 于c的发送,c的发送又happens before于c完成接收,c的接收也happens before于print函数。

一个channel的关闭happens before于接收返回0值,因为channel被关闭了。

在前面的例子中,把c<-0替换成 close©产出的程序拥有相同的保证行为。

从一个非缓冲channel的接收 happens before于channel完成时的发送。

这一个程序(跟上面一样,但是发送和接收交换,并且使用了一个非缓冲的channel):

var c = make(chan int)
var a string

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

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

也保证会打印"hello, world"。对a的写 happens before于对c的接收,c的接收happens before于相应的c的完成时的发送,c的完成时的发送happens before于print。

如果channel是缓冲的(e.g. c = make(chan int, 1)),这样的话程序将不会保证打印"hello, world"(可能会打印空的字符串或者其他东西。)

一个容量为c的channel的第k个接收,happens before于第k+c个channel完成时的发送。

这个规则概括了缓冲channel的前置规则。它允许一个计数信号量通过一个缓冲channel来建模:channel中的项目数量相当于活跃使用的数量,channel的容量相当于同时使用的最大数量,发送一个项目获得信号量,然后接受一个项目释放这个信号量。这是一个用于限制并发的惯用语。

这个程序为work list中的每一个元素开启一个goroutine,但是这些goroutines使用limit channel来协调,确保最多同时只有三个运行中的work函数。

var limit = make(chan int, 3)

func main() {
	for _, w := range work {
		go func(w func()) {
			limit <- 1
			w()
			<-limit
		}(w)
	}
	select{}
}

Locks

sync包实现了两个锁数据类型,sync.Mutex和sync.RWMutex.

对任何sync.Mutex或者sync.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"。第一个调用l.Unlock() happens before 于第二个l.Lock()(main中)的返回,这个返回happens before 于print函数。

对任何调用l.RLock() 在一个sync.RWMutex变量l,有一个n,使得l.RLock在调用n到l.Unlock之后发生(返回),而匹配的l.RUnlock在调用n+1到l.Lock之前发生。

Once

sync包提供了一个安全机制来初始化,在有多个goroutines时通过使用Once类型。多线程可以为一个指定的f函数执行once.Do(f),但是只要有一个执行f()函数,其他的调用会被阻塞直到f() 已经返回。

从once.Do(f)的一个简单f() 的调用 happens(返回) before 任何调用once.Do(f)的返回。

var a string
var once sync.Once

func setup() {
	a = "hello, world"
}

func doprint() {
	once.Do(setup)
	print(a)
}

func twoprint() {
	go doprint()
	go doprint()
}

在这个程序中,调用twoprint方法只会执行一个setup。setup 函数将会甚至在print的调用之前完成。结果是"hello, world"将会被打印两次。

Incorrect synchronization

注意,一个读r可能会观察到被一个跟r并发发生的写w 写过的值。即便这发生了,也不意味着读 happening after r 会观察到happen before w的写。

var a, b int

func f() {
	a = 1
	b = 2
}

func g() {
	print(b)
	print(a)
}

func main() {
	go f()
	g()
}

这个程序将会打印2和0

这个事实让一些通用的惯用语失效了。

双重校验锁是一种为了避免同步开销的尝试。例如,这个twoprint可能被不正确地写成:

var a string
var done bool

func setup() {
	a = "hello, world"
	done = true
}

func doprint() {
	if !done {
		once.Do(setup)
	}
	print(a)
}

func twoprint() {
	go doprint()
	go doprint()
}

但是没有办法保证的是,在doprint,观察到对done的写意味着观察到对a的写。这个版本会打印一个空的字符串而不是"hello, world".

另一个不正确的惯用是忙等一个值,就像这里面:

var a string
var done bool

func setup() {
	a = "hello, world"
	done = true
}

func main() {
	go setup()
	for !done {
	}
	print(a)
}

就像上面,没有保证的是,在main,观察到对done的写意味着观察到对a的写,因此这个程序也可以打印一个空字符串。更惨的是, 没法保证对done的写会曾被main观察到,因为没有同步时间在两个线程之间。main中的循环无法保证完成。

在这个主题上还有更精妙的变形,例如这个程序。

type T struct {
	msg string
}

var g *T

func setup() {
	t := new(T)
	t.msg = "hello, world"
	g = t
}

func main() {
	go setup()
	for g == nil {
	}
	print(g.msg)
}

即使main观察到g!=nil并且推出勋魂,也没法保证它将会观察到g.msg的初始化。

在这些所有的例子中,解决方案是相同的:使用明确的同步机制。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值