Go:Memory Model

Go的内存模型

看完这篇文章你会明白

  • 一个Go程序在启动时的执行顺序
  • 并发的执行顺序
  • 并发环境下如何保证数据的同步性
  • 同步性的错误示范

介绍

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

建议

在一个程序中,多个goroutine同时修改一个都要访问的数据必须将这种访问进行序列化(也就是说需要有一个谁先谁后的规则)。为了序列化的访问,可以使用通道操作或其他同步机制(如 syncsync/atomic包中的方法)保护数据。
如果你想详细了解一个程序的行为,请继续往下读。

Happens Before原则

在单个goroutine中,读取和写入必须按照程序指定的顺序执行。 也就是说,编译器和处理器可以重新排序在单个goroutine中执行的读取和写入。 如果有两个goroutine时,一个goroutine观察到的执行顺序可能不同于另一个goroutine定义的顺序。 例如,如果一个goroutine执行a= 1; b = 2;另一个可能会在观察到a值更新之前先观察b的更新值。
为了指定读和写的顺序,我们定义了Happens Before原则,它表示一个Go程序中执行内存操作的部分顺序。

  • 如果事件e1发生在e2之前,那么我们就可以说事件e2发生在e1之后
  • 如果e1既不发生在e2之前,也不发生在e2之后,那么我们就说e1和e2是同时发生的

在单goroutine的程序中,Happens-Before的顺序就是程序中表达的顺序。
*注:这里的单goroutine的程序是指程序中没有使用go关键字声明一个goroutine的操作。

注:任何一个Go程序中都不会只有一个goroutine的存在,即使你没有显示声明过(go关键字声明),程序在启动时除了有一个main的goroutine存在之外,至少还会隐式的创建一个goroutine用于gc,使用runtime.NumGoroutine()可以得到程序中的goroutine的数量

对一个变量v的写操作(w)会影响到对v的读操作(r),那么:

  • 这个读操作(r)发生在写操作(w)之后
  • 没有其他的写操作(w')发生在写操作(w)和读操作(r)之间

为了保证对变量v的一个特定读操作(r)读取到一个特定写操作(w)写入的特定值,确保w是唯一的一个写操作,那么:

  • w发生在r之前
  • 对共享变量v的任何其他写入都发生在w之前或之后

这个条件相对于第一个更加苛刻,它需要保证没有其他的写操作发生在w和r之间

在一个goroutine中,这两个定义是等价的,因为它没并发性可言;但是当多个goroutine同时访问变量v时,我们必须使用同步事件(synchronization events)来满足Happends-Before条件以确保读操作(r)观察到期望的写操作(w)的值。

同步(Synchronization)

初始化(Initialization)

程序初始化在单个goroutine中运行,但是goroutine可能会创建其他同时运行的goroutine
如:在package p中import package q,那么q的init()函数先于p的init()函数,起始函数main.main()发生在所有包的init()函数之后

Goroutine creation

go关键词声明一个goroutine的动作发生在goroutine(调用go的那个goroutine)执行之前

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

结果

  • 打印hello,world,说明f()所在的goroutine执行了
  • 什么都不会打印,说明f()所在的goroutine没执行,并不代表f()这个goroutine没被加入到goroutine的执行队列中去,只是f()没来得及执行,而程序已经退出了,(Go会将所有的goroutine加入到一个待执行的队列中),之后它会被gc回收处理。
Goroutine destruction

一个goroutine退出时,并不能保证它一定发生在程序的某个事件之前

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

在这个匿名goroutine退出时,并不能确保它发生在事件print(a)之前,因为没有同步事件(synchronization events)跟随变量a分配,所以并不能保证a的修改能被其他goroutine观察到。事实上,约束性强一点的编译器还可能会在你保存时删除go声明。
一个goroutine对a的修改想要其他的goroutine能观察到,可以使用同步机制(synchronization mechanism)来做相对排序,如lockchannel communication

Channel communication

Channel是引用类型,它的底层数据结构是一个循环队列

Channel communication是多个goroutine之间保持同步的主要方法。每个发送的特定Channel与该Channel的相应接收匹配,发送操作和接收操作通常在不同的goroutine中。

缓冲通道(buffered channel)Happens Before原则
  • 发送操作会使通道复制被发送的元素。若因通道的缓冲空间已满而无法立即复制,则阻塞进行发送操作的goroutine。复制的目的地址有两种。当通道已空且有接收方在等待元素值时,它会是最早等待的那个接收方持有的内存地址,否则会是通道持有的缓冲中的内存地址。
  • 接收操作会使通道给出一个已发给它的元素值的副本,若因通道的缓冲空间已空而无法立即给出,则阻塞进行接收操作的goroutine。一般情况下,接收方会从通道持有的缓冲中得到元素值。
  • 对于同一个元素值来说,把它发送给某个通道的操作,一定会在从该通道接收它的操作完成之前完成。换言之,在通道完全复制一个元素值之前,任何goroutine都不可能从它哪里接收到这个元素值的副本。

一个容量为C的channel的第k个接收操作先于第(k+C)个发送操作之前完成

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的发送操作之前,它们作为一个f()整体又发生在c的接收操作完成之前(<-c),而<-c操作发生在print(a)之前

对Channel的Close操作发生在返回零值的接收之前,因为通道已经关闭
注:所以对Channel的Close操作一般发生在发送结束的地方,如果在接收的地方进行Close操作,并不能保证发送操作不会继续send数据,而对于一个Closed的Channel进行send操作会返回一个panic: send on closed channel

所以上例中,将c<-0的操作换成close(c)也能正确输出"hello,world"。

假如一个channel中的每个元素值都启动一个goroutine来处理业务,那么缓冲通道还可以有效的限制启动的goroutine数量,它总是小于等于channel的capacity的值。

var limit = make(chan int, 3)
func main() {
    for _, w := range work {
        go func(w func()) {
            limit <- 1
            w()
            <-limit
        }(w)
    }
    select{}
}
非缓冲通道(unbuffered channel)Happens Before原则
  • 向非缓冲通道发送元素值的操作会被阻塞,直到至少有一个针对该通道的接收操作进行为止。该接收操作会先得到元素值的副本,然后在唤醒发送方所在的goroutine之后返回。也就是说,这时的接收操作会在对应的发送操作完成之前完成。
  • 向非缓冲通道接收元素值的操作会被阻塞,直到至少有一个针对该通道的发送操作进行为止。该发送操作会直接把元素值复制给接收方,然后在唤醒接收方所在的goroutine之后返回。也就是说,这时的发送操作会在对应的接收操作完成之前完成。

下例是将上例的接收和发送操作交换了一下,并将通道设置为非缓冲通道

var c = make(chan int)
var a string
func f() {
    a = "hello, world"
    <-c
}
func main() {
    go f()
    c <- 0
    print(a)
}

同样会打印出"hello,world",如果使用缓冲通道(buffered channel),那么程序不一定会打印出"hello,world"了(可能会打印一个空字符串,崩溃,或者做些其他事)。

Locks

包sync实现了两种锁数据类型:sync.Mutexsync.RWMutex
程序:

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()发生在第二次调用l.Lock()(main函数里面)之前,它们整体发生在print之前。

对于Lock()和Unlock()都是成对出现的,对于一个Unlocked的变量再次进行Unlock()操作,会panic: sync: unlock of unlocked mutex;而对于已经Lock()的变量再次进行Lock()操作是没有任何问题的(在不同的goroutine中),无非就是谁先抢占到l变量的操作权限而已。如果在同一个goroutine中对一个Locked的变量再次进行Lock()操作将会造成deadlock

Once

包sync提供了一个安全机制,通过使用Once类型可以在存在多个goroutine的情况下进行初始化,即使多个goroutine同时并发,Once也只会执行一次。Once.Do(f),对于函数f(),只有一个goroutine能执行f(),其他goroutine对它的调用将会被阻塞直到返回值(只有f()执行完毕返回时Once.Do(f)才会返回,所以在f中调用Do将会造成deadlock)。
程序:

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()的调用结果是"hello,world"的打印两次,但是setup()函数只会执行一次。

Incorrect synchronization

以下都是“同步”用法的不正确示范

  1. 同步发生的读操作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。

  1. 双重锁定是试图避免同步的开销。 例如,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值的写入,但是操作setup()中a和done的写入并没有同步性。

  1. 另一个错误的用法就是循环等待一个值
var a string
var done bool
func setup() {
    a = "hello, world"
    done = true
}
func main() {
    go setup()
    for !done {
    }
    print(a)
}

在main()中,通过观察done的写入来实现观察a的写入,所以a最终仍可能是空。更糟糕的是,没什么同步机制确保go setup()中done的写入值会被main()中观察到,所以可能main()永远不会退出循环。

  1. 上例的一个微妙变种:
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的初始化值。

附:官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 本文翻译自Sameer Ajmani的文章《Go Concurrency Patterns: Pipelines ...
    大蟒传奇阅读 3,856评论 0 15
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    JackChen1024阅读 7,533评论 1 143
  • 介绍 如何保证在一个goroutine中看到在另一个goroutine修改的变量的值,这篇文章进行了详细说明。 建...
    51reboot阅读 19,612评论 11 41
  • 1 黑夜点亮了星的光 你的悲伤 点亮了我的黑暗 2 轻舟入了浅浅的港湾 那远方 也被搁浅了 3 与你在一起 忧愁 ...
    王错错阅读 802评论 4 10