golang内存模型

  • 介绍
go内存模型是指在特定的条件下,向goroutine中的变量写入值,在另一个goroutine中能够读取到该变量的值
  • 建议

多个goroute同时修改一个数据必须是有序的

使用channel或sync、sync/atomic包中提供的同步原语,可保证对数据顺序访问

  • happens before

单个goroutine读写必需按一定顺序执行,编译器和处理器只有在不改变程序执行最终结果的情况下会对单个goroutine的读写重新排序。重新排序会导致一个goroutine看到的行为与另一个goroutine不一致。比如在一个goroutine中执行a=1;b=2,另外的goroutine看到的b值的更新发生在a值更新之前。

golang的内存操作读写请求的happens before:事件e1 happens before 事件e2,则e2在e1后执行。事件e1不是happen before 事件e2且不是在事件e2之后执行,则说事件e1和事件e2是同时执行的。

单个goroutine的happens-before是由程序顺序表达。

以下两条件都满足时一个写操作w向变量v写入数据,读操作r可以观察到变量v的值

1、读操作r没有 happen before 写操作w

2、在写w操作之后与读r操作之前,没有其他的写操作w’’对变量v写入数据

为了保证读操作r读到变量v,是写操作w向变量v写入的值,必须符合以下两个条件

1、写操作w happens before 读操作r

2、其他的写变量V的操作要么发生在写操作w之前,要么发生在读操作r之后

这部分限制条件比第一种限制条件严格,要求没有其他写操作同时与写操作w或读操作r一起执行。

单个goroutine没有并发问题,写操作w的变量v可以被读操作r读取到。当多个goroutines访问共享变量v,必须使用同步事件建立执行顺序,确保写操作的变量值被读操作正确读取。

在内存模型中将初始化变量v类型零值的行为作为一次写操作。

读写值大于32位(4 bytes)或64位操作系统(8 bytes)时的操作行为,跟在多个32位或64位操作系统操作的行为顺序一致是不明确的。

  • 同步

Initialization

程序初始化运行单个goroutine,但这个goroutine可能创建其他同时运行的goroutines。

package p导入package q,q的init方法比p的先执行。

main方法在所有init方法完成初始化后执行

goroutine creation

go声明启动一个goroutine happens before 执行goroutine,示例如下:

var a string

func f() {
        print(a)
}

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

未来可能执行hello方法可能会打印出”hello, world”,目前不会

goroutine destruction

一个goroutine的退出不保证任何事件的执行顺序,示例

var a string

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

给变量a赋值不是一个同步事件,所以不能保证变量a可以被其他goroutine获取到。这段a的赋值操作代码可能在程序编译阶段直接被丢弃掉了。

假如一个goroutine变量值对另一个goroutine可获取到,需要使用锁或信道(channel)通信同步机制保证执行顺序。

Channel communication

goroutines之间的消息同步的主要是通过信道通信(channel)方式实现。在不同的groutine中,每个发送的信道,有个对应的接收信道与其对应。

发送信道happens before接收信道。示例:

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 happens before发送信息至信道c,发送信道happens before从接收信道c中获取信息,接收信息happens before打印动作print.

一个关闭的信道happens before 接收信道,未向信道中发送信息,会返回一个零值,因为信道已经关闭了。可通过替换代码中c<- 0为close(c)代码输出结果是一致的。

下面这段代码类似,使用无缓存的信道通信且变换下发送与接收语句

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获取数据,从接收信道获取数据happens before从信道c中发送数据,从信道c中发送数据happens before 打印动作Print

假如使用的是缓存信道(如c = make(chan int, 1)),这段代码不能保证输出“hello, world”。代码可能输出空的字符串,崩溃,或其他。

向一个容量为C的信道发送数据,从该信道接收数据时第k个数据接收happens before 第k+c个数据接收

总结缓存信道通信规则。缓存信道通信方式允许统计信号量:信号量数为活跃使用数,信号量容量为最大活跃使用数,发送数据请求信号量,接收数据释放信号量。这是用于限制并发的常用方法。

下列代码,为work启动一个goroutine,使用了信道限制确保同一时刻最多有三个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两种数据类型的锁。

变量l为sync.Mutex or sync.RWMutex锁,假如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”。在f方法中第一次调用l.Unlock()返回happens before在main方法中第二次调用l.Lock()返回,可正常打印输出变量a的值

变量l为 sync.RWMutex调用 l.RLock时,n调用l.RLock 返回happens after n调用l.Unlock,同时l.RUnlock happens before n+1调用l.Lock

Once

在多个groroutine中sync包使用Once类型安全初始化,多个线程同时执行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可以观察到写happened before写操作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)
}

跟前面的例子一样,这段代码也有可能输出空字符串,观察写数据至变量done,意味着观察写数据至变量a。在两个线程中,没有同步事件用于保证main方法可以观察到写入done变量的值,不能保证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然后退出循环,还是不能保证main方法可以观察到g.msg的初始化值。

所有的这些错误的例子,表明要从一个gorotine观察到另一个goroutine赋值问题必须使用显式的同步机制。

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

关联的部分面试题目

该程序片段输出内容是什么?这种问法是否有误,是否换应该换种思路问:如要输出正确的i值应该怎么处理?

func main() {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i)
            wg.Done()
        }()
    }
    wg.Wait()
}

该程序片断使用是否有问题?如有如何纠正?

func goRoutineA(a <- chan int) {
    val := <- a
    fmt.Println("goRoutineA received the data", val)
}

func goRoutineB(b <- chan int) {
    val := <- b
    fmt.Println("goRoutineB received the data", val)
}

func main() {
    ch := make(chan int)
    go goRoutineA(ch)
    go goRoutineB(ch)
    ch <- 3
    time.Sleep(time.Second * 1)

}
我来评几句
登录后评论

已发表评论数()

相关站点

热门文章