白话Go内存模型&Happen-Before

1. Go内存模型基础

Go内存模型是指规定goroutine如何与共享变量进行交互的一组规则。对于Go来说,内存模型是在语言层面做的,而非编译层面。Go1.7版本之后采用了新的内存模型Happens-Before,下面我们先来了解下Go内存模型的基础知识。

在Go内存模型中,内存被抽象成由一系列的字节(byte)组成的,每个字节都有对应的内存地址。与此同时,Go内存模型还定义了共享变量与普通变量之间的区别。共享变量指的是会被若干个goroutine同时访问的变量,比如我们常用的全局变量或者是在程序中通过channel传递的变量等。普通变量则指只会被单个goroutine访问的变量。

在Go内存模型中,goroutine使用的是基于cache的本地视图(Local View)来访问内存的。这个本地视图又被称为goroutine的工作内存(Working Memory),它保存了共享变量和普通变量的副本。这些副本只会存放在该goroutine自己的工作内存中,而不会被其他goroutine所访问。

当多个goroutine之间需要对同一个共享变量进行操作时,它们会通过通信或者同步来实现对内存的访问。另外,Go内存模型还规定了对于同一个goroutine来说,在程序中的指令执行顺序必须要符合其在源代码中的顺序。

1.1 内存访问同步

由于goroutine之间的工作内存是独立的,因此一个goroutine修改了工作内存中的变量的值后,并不能保证这个变量的新值会立即被其他goroutine感知到。因此,Go提供了一些同步机制来协调多个goroutine之间对共享变量的访问。

1. 互斥锁

互斥锁(Mutex)是Go最基本的一种同步原语,它提供了两个方法Lock和Unlock来锁定和解锁某个共享资源,以保证同一时刻只有一个goroutine能够访问该资源。

var mu sync.Mutex //定义一个全局的互斥锁

func f() {

mu.Lock()

defer mu.Unlock()

//对共享变量进行读写操作

}

上面的代码中,我们首先声明了一个全局的互斥锁var mu sync.Mutex,然后在f()函数中使用mu.Lock()将该共享资源进行了锁定,这里一定要使用defer语句来保证在f()函数退出的时候解锁共享资源,以免出现死锁的情况。

2. 读写锁

读写锁(RWMutex)则是互斥锁的一种变种,它具有更好的性能。与互斥锁不同的是,读写锁允许在没有任何等待者的情况下,多个goroutine同时对共享资源进行读操作,而在进行写操作时,则只允许一个goroutine进行操作。

var mu sync.RWMutex //定义一个全局的读写锁

func f() {

mu.RLock()

defer mu.RUnlock()

//对共享变量进行读操作

}

func g() {

mu.Lock()

defer mu.Unlock()

//对共享变量进行写操作

}

上面的代码中,我们同样定义了一个全局的读写锁var mu sync.RWMutex,然后在f()函数中使用mu.RLock()对共享资源进行了锁定,这里同样要使用defer来解锁共享资源,在g()函数中使用mu.Lock()进行写操作。

3. 原子操作

除了锁机制以外,Go还提供了一些原子操作(Atomic Operation)来支持对共享变量的安全访问。原子操作指的是不能被中断的操作,从而保证了操作的完整性和正确性。在Go中,原子操作主要涉及到两种类型:原子读写操作和原子计数器操作。

原子读写操作包括原子加载(Atomic Load)和原子存储(Atomic Store)。原子计数器指的则是一种特殊的原子操作,它可以支持并发地对一个计数器进行加或减的操作。

//原子读写操作

var v int32 //定义一个共享变量

atomic.StoreInt32(&v, 123) //原子存储

val := atomic.LoadInt32(&v) //原子加载

//原子计数器操作

var counter int32 //定义一个共享计数器

atomic.AddInt32(&counter, 1) //加1操作

上面的代码中,我们首先定义了一个共享变量v和一个共享计数器counter,然后使用atomic.StoreInt32函数进行原子存储,使用atomic.LoadInt32函数进行原子加载。另外我们还使用了atomic.AddInt32函数对counter进行了加1操作。

2. Happens-Before内存模型

Happens-Before(HB)是Go语言在1.7版本之后引入的全新内存模型。相对于旧的内存模型,Happens-Before更加完善和严格,可以让我们更加准确和安全地编写并发程序。

2.1 Happens-Before规则

Happens-Before规则主要指的是,在程序执行过程中,一些事件的顺序性规则。Happens-Before规则定义了各种操作之间的执行顺序,从而避免了数据访问冲突和数据竞争的问题。

在Happens-Before模型中,除了程序中存在先后顺序的操作之间会遵从Happens-Before的规则之外,还有四种操作是特殊的:初始化、传递、关闭和接收。

其中,初始化操作指的是对某个变量进行初始化的操作,接收操作指的是从某个channel接收数据的操作,传递操作指的是通过channel发送数据的操作,关闭操作则指的是关闭某个channel的操作。

对于这四种特殊的操作,在Happens-Before模型中有如下规则:

对一个变量的初始化操作Happens-Before该变量的后续操作。

对于发送操作,它的完成Happens-Before从相应的接收操作的返回。

某个channel的关闭操作Happens-Before该channel上的后续接收操作。

在一个channel上的接收操作Happens-Before在该channel上的后续发送操作。

除了上述四个规则以外,还有如下规则:

在一个goroutine中,对于一个时间t1,如果时间t2在程序中跟在t1后面执行,那么t1 Happens-Before t2。

如果a Happens-Before b,b Happens-Before c,那么a Happens-Before c。这个规则被称为传递性规则。

如果a Happens-Before b,那么在该goroutine中的所有操作a的结果对操作b都是可见的。

2.2 Sync/Atomic包的支持

对于Go语言的Sync/Atomic包来说,它通过一系列的内置函数来保证内存访问的顺序性和正确性。在Happens-Before模型中,Sync/Atomic包支持的内置函数都遵循了各种规则,从而保证了对于共享变量的读写操作正确顺序的执行。

在Sync/Atomic包中,包含了如下几个内置函数:

atomic.Store系列:原子存储操作。

atomic.Load系列:原子加载操作。

atomic.Add系列:原子加操作。

atomic.CompareAndSwap系列:如果地址指向的值是old,则将该值和new进行交换,返回原值是否是old。

atomic.Swap系列:将地址指向的值设置为new,返回原来的值。

这些内置函数可以保证内存操作之间的正确顺序性。

2.3 示例分析

下面我们以一个示例来说明Happens-Before模型的工作流程。

假设我们的程序中有如下两个goroutine,它们之间通过channel来进行通信。

var done = make(chan bool)

var msg string

func main() {

go produce()

go consume()

<-done

}

func produce() {

msg = "hello, world!"

close(done)

}

func consume() {

fmt.Println(msg)

}

在上面的代码中,我们首先定义了一个done channel和一个msg变量,然后使用两个goroutine来生产和消费msg变量。produce()函数对msg进行了赋值,然后关闭了done channel,而consume()函数则对msg变量进行了读取操作。最后在main()函数中,我们使用<-done来阻塞程序,直到done channel被关闭。

在Happens-Before模型中,如果我们对上述代码进行HB分析的话,我们可以得到如下结果:

在main()函数执行的过程中,produce()函数的执行Happens-Before<-done操作。

在produce()函数执行的过程中,对msg变量进行的赋值操作和done channel关闭操作Happens-Before<-done操作。

在consume()函数执行的过程中,msg变量的读取操作Happens-Beforefmt.Println(msg)操作。

因此,根据上述分析,我们可以确定程序中所有操作的执行顺序,从而保证了数据的正确性和完整性。

3. Go内存模型小结

Go内存模型是一个非常重要的概念,在Go语言的并发编程中扮演着关键的角色。在Go内存模型中,我们需要了解共享变量和普通变量之间的区别,以及使用锁机制和原子操作等手段来实现对共享变量的安全访问。

同时,Go1.7版本之后引入的Happens-Before模型则更加完善和严格,它定义了各种操作之间的执行顺序,从而避免了数据访问冲突和数据竞争的问题。在实际中,我们可以通过Sync/Atomic包中提供的内置函数来实现对于共享变量的安全操作。

后端开发标签