Go语言Goroutine与Channel内存模型

Go语言内存模型规定了在一个goroutine中一个变量的读取的情况下,确保能够观察到在其他另外goroutine中写入同样变量的值。也就是说,如果在多个goroutine操作修改同一个变量状态情况下,Go内存模型能够保证一个goroutine对变量写入的数据能够被其他goroutine正常读取,类似多线程编程中两个线程对同一个变量读写保证一样。

建议

  多个goroutine同时访问修改同一数据的编程必须是序列化访问。

  为了实现序列化访问,使用channel操作或其他同步语法如sync和sync/atomic进行数据保护。

  你必须阅读本文档所有部分才能理解你的程序运作机制,这样你就很聪明。

  但是不要聪明。

Happens Before

  在单个goroutine中,读取和写入行为需要像它们在代码中编写的顺序一样,因为,当单个goroutine中这种读写操作重新排序不会改变其最终结果时,编译器和处理器也许会重排读取和写入的顺序。正是因为这种顺序重排,被一个goroutine观察到执行顺序也许就不同于同样代码在另外一个goroutine中执行顺序。举例:如果一个goroutine执行 a=1;b=2;,另外一个goroutine也许看到在a值更新之前b已经更新到2这个值了。

为了规定读取和写入,我们定义了happens before,这是Go语言中内存操作执行的偏序(partial order),如果事件e1发生在事件e2之前,那么我们说事件e2发生在事件e1之后,也可以这么说,如果e1没有发生在e2之前,同时也不会发生在e2之后,那么我们说e1和e1是同时并发发生的。

在单个goroutine中,happens-before顺序是程序代码想要表达的顺序。

如果下面两个条件都满足,对于变量v的一个读操作r被允许观察到(之前)对v的写操作w。

  1. r 没有发生在w之前(not happen before,r发生在w之后或同时发生)
  2. 并没有其他对v的写操作 w'发生在w之后与r之前(也就是说:在r和w之间再也没有除这两个操作以外的其他任何写操作)

为了保证变量v的一个读操作r能够获得对这个变量v的写操作,必须确保w是r能够看到的唯一的写操作,只有下面两个条件满足,r就能够确保观察到w:

  • w 发生在r之前(happens before).
  • 任何对共享变量v的其他写操作或者发生在w之前,或者发生在r之后。(也就是:r和w之间没有任何其他写操作)

这一对条件强于先前第一对,它指定没有任何与w或r同时发生其他写操作。

在单个goroutine中,是不存在并发的。举例,下面两个语句前后是等价的:当多个goroutine访问同一共享变量v时,一个读操作r观察到被最近操作w对变量v的写入结果值,,它们必须使用同步事件来创建happen-before条件来确保读取到那个写操作的值。

对go中任意一个类型的零值初始化操作在内存模型中也看作是一个写入操作。

对大于一个机器字节的读写操作可看作多个顺序不定的机器字节(machine-word-sized)操作。

同步

初始化

  程序初始化是运行在单个goroutine中,但是这个goroutine会创建同时其他goroutine运行。

  如果一个包p导入了包q, 那么q的init函数的完成是发生在任何p的其他任何开始之前。

  函数main.main的开始是发生在所有init函数完成了之后的。

Goroutine创建

  注意,语句“go”会启动一个新的gorountine,这是发生在这个gorountine执行开始之前。举例如下:

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

  调用hello函数不保证立即打印出"hello,world",可能会在将来某个时刻(可能会在hello函数已经返回以后)打印出“hello, world”。

Goroutine解构

gorountine的退出并不保证其发生在程序中任何操作事件之前,比如:

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

a分配的值没有使用任何同步事件,这样它就不保证能被其他gorountine看到,实际上,攻击性强的编译器也许已经删除了整个“go func()....”语句

如果你希望一个gorountine必须被其他gorountine看到,那么使用同步机制,比如锁或channel通讯机制创建一个相对顺序。

Channel通讯

  Channel通讯是gorountine之间主要的同步方法,发送到channel每个事件消息将被另外一个gorountine收到。

  在一个channel中A发送将发生在从channel完全接受之前。案例:

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的写操作发生在对channel通道c的写操作之前,而通道c写操作会发生通道c的完成接受之前,通道c的接受完成是发生在print之前。 channel通道的关闭是发生在一个返回代表channel已经关闭的0值的接受操作之前。 在之前这个案例,可以使用close(c)代表 c <- 0,这会yield(线程用语)一个程序,同样保证操作行为顺序。 从一个无缓冲的channel中接受操作会发生该对channel发送操作完成之前。下面代码类似前面,但是使用了

unbuffered无缓冲的channel:
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的接受操作发生在相应的对c发送操作完成之前,但是对c的发生操作完成肯定在print之前发生。 如果channel被缓冲了比如使用c = make(chan int, 1)那么程序将不会保证能打印出"hello,world",它也许打印出空字符串或崩溃或其他什么事情会发生。 一个有容量C的通道第k大小接受操作发生在发送第第k+C个发送操作之前。 这个规则对于缓冲channel涵括之前的规则,这样能够允许一个计数semaphore 能被缓冲channel建模,channel中条目的数量对应于channel活动用户的数量,channel的容量对应于并发用户的最大数量,这些用户会在发送一个条目时获得semaphore,并且在接受到这个条目后释放semaphore,这是通常限制性并发的通用原理。 下面代码是在工作列表中每项启动一个goroutine,goroutine之间使用limit这个channel来确保一次最多运行三个函数。

var limit = make(chan int, 3)

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

本文分享自微信公众号 - Golang语言社区(Golangweb)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2016-09-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏DannyHoo的专栏

APNS推送原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/de...

36230
来自专栏吴伟祥

Linux下的shell简介(三) 原

        shell的本意是“壳”的意思,其实已经很形象地说明了shell在Linux系统中的作用。shell就是围绕在Linux内核之外的一个“壳”程序...

13330
来自专栏未闻Code

Tenacity——Exception Retry 从此无比简单

Python 装饰器装饰类中的方法这篇文章,使用了装饰器来捕获代码异常。这种方式可以让代码变得更加简洁和Pythonic。

20010
来自专栏Kevin-ZhangCG

[ Java面试题 ]多线程篇

28470
来自专栏用户2442861的专栏

Linux中的pushd和popd

其实,很早就知道pushd和popd在Linux中可以用来方便地在多个目录之间切换。那时比较浮躁,感觉切换目录没必要这么复杂。在实际中,发现通过使用pushd...

18020
来自专栏JAVA同学会

JAVA9模块化详解(二)——模块的使用

各自的模块可以在模块工件中定义,要么就是在编译期或者运行期嵌入的环境中。为了提供可靠的配置和强健的封装性,在分块的模块系统中利用他们,必须确定它们的位置,然后决...

9220
来自专栏Java大联盟

Java面试手册:线程专题 ④

11910
来自专栏直播开发

SRS开源直播服务 - StateThreads微线程框架学习

SRS是一个开源流媒体服务器,在目前大火的直播行业中较多的被使用。笔者作为直播行业的后台开发,对SRS的学习必不可少,本文主要讲解SRS底层使用的微线程开源...

1.4K50
来自专栏积累沉淀

干货--Redis 30分钟快速入门

一、 redis环境搭建 1.简介        redis是一个开源的key-value数据库。它又经常被认为是一个数据结构服务器。因为它的value不仅...

361100
来自专栏FreeBuf

Flask Jinja2开发中遇到的的服务端注入问题研究

0×00. 前言 作为一个安全工程师,我们有义务去了解漏洞产生的影响,这样才能更好地帮助我们去评估风险值。本篇文章我们将继续研究Flask/Jinja2 开...

24650

扫码关注云+社区

领取腾讯云代金券