Golang内存模型

简介

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

官方文档建议

  1. 修改由多个goroutine同时访问的数据的程序必须序列化这种访问。
  2. 要序列化访问,请使用channel操作或其他同步原语(syncsync/atomic)保护数据。
  3. 如果您必须阅读本文档的其余部分才能理解程序的行为,那么您太聪明了。
  4. 别自作聪明。

happens before

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

为了确定读写的需求,Golang定义了程序中执行内存操作的偏序:如果事件e1发生在事件e2之前,那么我们说e2发生在事件e1之后,同样,如果e1不发生在e2之前,也不发生在e2之后,那么我们说e1e2同时发生。

在一个goroutine内,happens-before顺序就是程序编写的顺序。

r does not happen before w.

There is no other write w to v that happens after w but before r.

w happens before r.

Any other write to the shared variable v either happens before w or after r.

这一对条件比第一对条件强;它要求没有其他写操作与 wr 同时发生。

在单个goroutine中,没有并发性,因此这两个定义是等效的:read r 观察由最近的write w 写入 v 的值。

当多个goroutine访问共享变量 v 时,它们必须使用同步事件来建立在确保读取观察所需写入之前发生的条件。

变量的初始化在内存模型中被认为是write。

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

同步

初始化

程序的初始化在一个特定的goroutine中完成,但是该协程可以创建其他协程并发运行。

如果包p引入了包q,则包qinit的函数执行完成happens beforepinit函数开始执行。

main.main函数的执行happens after所有init函数的执行完成。

协程创建

开始新goroutinego语句发生在goroutine执行开始之前。

var a string

func f() {
    print(a)
}

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

上面的代码输出hello world. 因为a的写(第8行)happen before 协程创建(第9行),所以a的写happen before协程的执行。

协程销毁

goroutine的退出不能保证发生在程序中的任何事件之前。

var a string

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

上面的代码并不能保证一定输出hello。因为分配给a之后没有任何同步事件,因此不能保证任何其他goroutine都能观察到它。一个激进的编译器可能会删除整个go语句。

如果一个goroutine的效果必须由另一个goroutine观察,则应该使用同步机制(如锁或通道通信)来建立相对顺序。

通道(channel)通信

通道通信是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的写操作发生在通道写之前,通道写发生在读之前。

通道关闭发生在因为通道已关闭返回零值的接收之前。

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

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

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

这个代码和上面代码有相同的效果。

从无缓冲通道的接收发生在该通道上的发送完成之前。

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上相应的发送完成之前,发生在打印之前。

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

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

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

上述代码不能保证输出hello world,可能会造成程序崩溃或者打印空字符串。

容量为C的通道上的第k次接收发生在该通道的第k+C次发送完成之前。

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

var limit = make(chan int, 3)

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

上面代码用有buffer的管道限制同时工作的协程的最大数量。

sync包实现了sync.Mutexsync.RWMutex两种锁

任给sync.Mutexsync.RWMutex的实例ln, 调用第nl.Unlock()发生在调用第ml.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,因为第一次unlock调用发生在第二次lock返回之前,因此发生在print之前。

对于sync.RWMutex的实例l,存在n,若对l.RLock()的返回发生第nl.Unlock()返回之后,则其对应的l.RUnlock()返回发生在第n+1l.Lock()返回之前

Once

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

一个通过once.Do(f)调用的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()
}

上述代码只会执行一次setup函数,同时打印两次hello world

错误的同步代码

r可能观察到与r同时发生的写w所写的值。即使发生这种情况,也并不意味着在r之后发生的读将观察到在w之前发生的写

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的写入,导致死循环不会中断。

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)
}

同上,即使观测到g!=nil也无法保证g.msg一定不为空

所有上述的解决办法就只有一个:使用显式的同步方式

展开阅读全文

页面更新:2024-03-13

标签:信号量   变量   函数   顺序   模型   通道   内存   发生   事件   操作   代码   程序

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top