The Go Memory Model

Version of May 31, 2014

Introduction

Go内存模型指定了一种条件,在这种条件下,可以保证在一个goroutine中读取变量可以观察到在不同goroutine中写入同一变量所产生的值.

Advice

修改由多个goroutine同时访问的数据的程序必须将这种访问序列化.

要序列化访问,请使用通道操作或其他同步原语(例如syncsync/atomic软件包中的原语)保护数据.

如果您必须阅读本文档的其余部分以了解程序的行为,那么您就太聪明了.

别聪明

Happens Before

在单个goroutine中,读取和写入的行为必须像它们按照程序指定的顺序执行一样. 也就是说,仅当重新排序不会改变语言规范所定义的该goroutine中的行为时,编译器和处理器才可以对单个goroutine中执行的读取和写入进行重新排序. 由于此重新排序,一个goroutine观察到的执行顺序可能与另一个goroutine察觉到的执行顺序不同. 例如,如果一个goroutine执行a = 1; b = 2; a = 1; b = 2; 另一个可以观察的更新值b的更新值前a .

, a partial order on the execution of memory operations in a Go program. 为了指定读取和写入的要求,我们在Go程序中定义 ,这是执行内存操作的部分顺序. 如果事件e 1发生在事件e 2之前,那么我们说e 2发生在e 1之后 . 另外,如果E 1e 2前发生和E 2后不会发生,那么我们说,E 1E 2同时发生.

在单个goroutine中,事前发生顺序是程序表示的顺序.

如果同时满足以下两个条件,则对变量vr观察对v的写w

  1. r does not happen before w.
  2. 有没有其他的写w"命令vw,R之前,这种情况发生.

要保证一个变量的读R v观察一个特定的写wv ,确保w是唯一写入R能够观看到. to observe w if both of the following hold: 也就是说,如果以下两个条件成立,则 r遵守w

  1. wr之前发生.
  2. 对共享变量v任何其他写操作都发生在w之前或r之后.

这对条件要强于第一对条件. 它要求没有其他写入与wr同时发生.

在单个goroutine中,没有并发性,因此这两个定义是等效的:read r观察对v最新写入w写入的值. 当多个goroutine访问共享变量v ,它们必须使用同步事件来建立事件发生-在确保读取观察到所需写入的条件之前.

v的类型的零值初始化变量v的行为就像在内存模型中进行写操作一样.

大于单个机器字的值的读取和写入将以未指定的顺序充当多个机器字大小的操作.

Synchronization

Initialization

程序初始化在单个goroutine中运行,但是该goroutine可能会创建其他同时运行的goroutine.

如果包p导入了包q ,则qinit函数的完成发生在任何p的开始之前.

函数main.main的启动发生在所有init函数完成之后.

Goroutine creation

启动新goroutine的go语句在goroutine的执行开始之前发生.

例如,在此程序中:

var a string

func f() {
	print(a)
}

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

呼叫hello会在将来的某个时候(也许在hello返回之后)打印"hello, world" .

Goroutine destruction

不能保证goroutine的退出会在程序中发生任何事件之前发生. 例如,在此程序中:

var a string

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

a的分配不会跟随任何同步事件,因此不能保证任何其他goroutine都会遵守该同步事件. 实际上,积极的编译器可能会删除整个go语句.

如果必须通过另一个goroutine来观察goroutine的影响,请使用同步机制(例如锁定或通道通信)来建立相对顺序.

Channel communication

通道通信是goroutine之间同步的主要方法. 通常在不同的goroutine中,将特定通道上的每个发送与该通道上的相应接收进行匹配.

通道上的发送发生在该通道上的相应接收完成之前.

该程序:

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

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

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

保证打印"hello, world" . 对a的写操作发生在发送c之前,发生在相应的c接收完成之前,发生在print之前.

通道关闭发生在由于通道关闭而返回零值的接收之前.

在前面的示例中,用close(c)替换c <- 0将产生具有相同保证行为的程序.

来自未缓冲通道的接收发生在该通道上的发送完成之前.

该程序(如上所述,但是使用send和receive语句进行了交换,并使用了未缓冲的通道):

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的写入发生在c的接收之前,发生在相应的c发送完成之前,发生在print之前.

如果通道已缓冲(例如c = make(chan int, 1) ),则不能保证程序会打印"hello, world" . (它可能会打印空字符串,崩溃或执行其他操作.)

在容量为的信道上的第个接收发生在从该信道发送的第 + 个发送完成之前.

该规则将前一个规则推广到缓冲通道. 它允许通过缓冲的通道对计数信号量进行建模:通道中的项目数量对应于活动使用的数量,通道的容量对应于同时使用的最大数量,发送一个项目获取信号量,以及接收项目会释放信号量. 这是限制并发的常见习惯用法.

该程序为工作列表中的每个条目启动一个goroutine,但是goroutine使用limit通道进行协调,以确保一次最多运行三个功能.

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.Mutexsync.RWMutex .

对于任何sync.Mutexsync.RWMutex可变l和 呼叫的 l.Unlock()的调用之前发生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() (在f )发生在第二次调用l.Lock() (在main )返回之前,这发生在print之前.

对于任何呼叫到l.RLock上的sync.RWMutex可变l ,存在的使得l.RLock呼叫至发生后(返回) l.Unlock和匹配l.RUnlock呼叫 + 1 至之前发生l.Lock . l.Lock

Once

sync包为使用Once类型的多个goroutines进行初始化提供了一种安全的机制. 多个线程可以执行一次.对于特定的f执行once.Do(f) ,但是只有一个将运行f() ,而其他调用将阻塞直到f()返回.

的单个呼叫f()once.Do(f)中的任何呼叫之前发生(返回) 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写入的值. 即使发生这种情况,也不意味着在r之后发生的读取将观察到在w之前发生的写入.

在此程序中:

var a, b int

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

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

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

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的写入,因此该程序也可以打印一个空字符串. 更糟糕的是,由于两个线程之间没有同步事件,因此无法保证main始终会观察到done写入. 不能保证main的循环完成.

There are subtler variants on this theme, such as this program.

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的初始化值.

在所有这些示例中,解决方案都是相同的:使用显式同步.

by  ICOPY.SITE