Golang内存模型
Ref: https://golang.org/ref/mem
- 简介
golang内存模型,主要说明了如下问题。在一个goroutine中读取变量,而该变量是由其他goroutine赋值的,这种情况下如何能够安全正确的读取。
- 建议
对于有多个goroutine在使用的变量,修改时需要序列化的读取。
主要方式包括,通过channel的方式、sync/atomic等原子同步操作等。
如果你想读完以下内容,以便理解你的程序内在运行机制,说明你很聪明。
但是不建议你这么聪明~
- 历史经验
只有一个goroutine的时候,读写操作都会如程序定义的顺序执行。这是因为,尽管编译器和中央处理器是可能会改变执行顺序,但并不会影响编程语言定义的goroutine中的行为逻辑。但也是因为可能改变执行顺序,同样的操作在不同的goroutine中观察到的执行顺序并不一致。比如A goroutine执行了a=1;b=2;另一个goroutine观察到的结果可能是b先于a被赋值了。
为具体说明读写操作的要求,我们定义了之前某个版本中Golang程序中内存操作的一部分逻辑如下。如果事件e1发生在事件e2之前,我们说e2发生在e1之后.如果e1并不发生在e2之前,也不发生e2在之后,我们说e1和e2是同步发生的。
在一个单goroutine的程序中,事件发生的顺序就是程序描述的顺序。
满足如下两个条件的情况下,对变量v的读取操作r,是能够观察到对v的写入操作w的:
- r并不发生在w之前;
- r之前,w之后,再没有其他对v的写操作;
为了保证对变量v对读取操作r,能够观察到特定的对v得写操作w,需要保证w是r唯一能够观察到写操作。因此,要保证r能够观察到w需要满足如下两个条件:
- w发生在r之前;
- 其他对v的写操作,要么发生在w之前,要么发生在r之后;
这对条件是比第一个条件更加严格,它要求r和w的同时,没有其它的写操作(即和r或w同步的写操作);
在单一goroutine里面,没有同步操作,所以以上两组条件是等价的。但是对于多goroutine,需要通过同步事件来确定顺序发生,从而保证读操作能够观察到写操作。
在内存模型里面,变量v初始化为零值,也是一种写操作。
读写大于单一机器码的变量的动作,实际操作顺序不定,
- 同步
- 初始化
程序初始化在单一goroutine里面,但是goroutine会创建其他goroutine,然后多个goroutine同步执行。
如果package p引用了package q,q的init函数的执行,会先于所有p的函数执行。
Main.main函数的执行,在所有init函数执行完后。
- Goroutine的创建
go表达式,会创建一个goroutine,然后该goroutine才能开始执行。
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
以上代码示例,调用hello函数,可能在hello已经return到时候,f才回执行print。
- Goroutine的销毁
goroutine的退出时机并没有保证一定会在某个事件之前。
var a string
func hello() {
go func() { a =
"hello"
}()
print(a)
}
比如以上示例,对a的赋值,并不保证与hello本身的任何动作保持同步关系,所以也不能保证被其他任何goroutine的读操作观察到。事实上,任何一个激进的编译器都会把这里整个go表达式直接删掉,不做编译。
如果一个goroutine的影响想被其他的goroutine观察到,必须通过同步机制(比如锁、channel)来确定相对顺序关系。
- Channel通信
channel通信是goroutines之间主要的同步方式。一般来说channel上的每一次send都会相应有另一个goroutine从此channel受到消息。
同一个channel上,send操作总是先于相应的receive操作完成。
var c = make(chan int, )
var a string func f() {
a = "hello, world"
c <-
} func main() {
go f()
<-c
print(a)
}
以上示例,能够保证print出『hello, world』。对a的写,是先于往c中发送0,而从c中接收值,先于print。
channel的关闭,先于接收到该channel关闭而发出来的0.
在上面这个例子中,用close(c)代替 c<-0,其效果是一样的。
对于没有缓存的channel,receive发生在send完成之前。
var c = make(chan int)
var a string func f() {
a = "hello, world"
<-c
} func main() {
go f()
c <-
print(a)
}
以上示例,依旧能够保证print出『hello, world』。对a的写,先于从c接收;从c接收,先于 c <- 0执行完; c <- 0执行完,先于print执行。
但如果channel是缓存的(例如c = make(chan int, 1)),那么以上程序不能保证print出『hello, world』,甚至有可能出现空值、crash等情况;
对于缓存容量为C的channel,第k次接收,先于K+C次发送完成。
这条规则概括了缓存和非缓存channel的规则。因此基于带缓存的channel,可以实现令牌策略:在channel中缓存的数量代表active的数量;channel的缓存容量表示最大可以使用的数量;发送消息表示申请了一个令牌,接收消息表示释放了一块令牌。这是限制并发常用的一种手段。
var limit = make(chan int, )
func main() {
for _, w := range work {
go func(w func()) {
limit <-
w()
<-limit
}(w)
}
select{}
}
以上示例程序,对于work list中的每一条,都创建了一个goroutine,但是用limit这个带缓存的channel来限制了,最多同时只能有3个goroutines来执行work方法。
- 锁机制
sync包中实现了两个锁的数据类型,分别是sync.Mutex和sync.RWMutex
对于任何的sync.Mutex和sync.RWMutex类型变量l,和n<m,对于l.Unlock()的调用n,总是先于对于l.Lock()的调用m。
var l sync.Mutex
var a string func f() {
a = "hello, world"
l.Unlock()
} func main() {
l.Lock()
go f()
l.Lock()
print(a)
}
如上示例能够保证print出『hello, world』。f中第一个l.Unlock()的调用,先于main中第二个l.Lock()的调用;第二个l.Lock()的调用先于print的调用;
任何对于l.Rlock的调用(其中l为sync.RWMutex类型变量),总是有一个n,l.Lock在调用n执行l.Unlock之后才能return;对应的,l.RUnlock的执行在调用n+1执行l.Unlock之前。
- 单例(Once)
Sync包提供了一种安全的多goroutine种初始化机制,那就是Once类型。对于特定的方法f,多个线程都能调用Once.Do(f),但是只有一个线程会执行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()
}
这里print两次『hello, world』,但只有第一次调用doprint会执行setup赋值。
- 不正确的同步
对于同步发生的读操作r和写操作w,r有可能观察到w。但即使发生了这种情况,不代表r之后的读操作,也能观察到w之前的写操作。
var a, b int
func f() {
a =
b =
}
func g() {
print(b)
print(a)
}
func main() {
go f()
g()
}
如上例子,g打印出2,然后是0.这个事实颠覆了我们的一些习惯认知。
对于同步问题加锁一定要double check。
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()
}
如这个例子,不能保证观察到done的写操作时候,也能观察到对a的写操作。其中一个goroutine可能打印出空字符串。
另外一种错误的典型如下:
var a string
var done bool func setup() {
a = "hello, world"
done = true
} func main() {
go setup()
for !done {
}
print(a)
}
同上一个例子一样,这里对done得写观察,不能保证对a的写观察,所以也可能打印出空字符串。
更甚,由于main和setup两个线程间没有同步事件,并不能保证main中一定能观察到done的写操作,因此main中的一直循环下去没有结束。(这里不是很理解,只能说setup的执行时机和main中for循环没有明确的相对先后和相对距离,所以可能导致循环很久setup还没执行,或执行了但是没有更新到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)
}
即使main能够观察到g的赋值而退出循环,但是也不能保证观察到g.msg的初始化值。
对于以上所有例子,解决方案是一样的,定义明确的同步机制。
Golang内存模型的更多相关文章
- golang 内存模型
1,是什么 是一套规范.内存操作指导 解决多线程编程的 程序的 原子性,有序性,可见性(主要)的问题. 多核操作系统,会存在缓存不一致的情况,说到底是一个同步的问题. 2, 内容 内存模型,除了定义了 ...
- golang的内存模型与new()与make()
要彻底理解new()与make()的区别, 最好从内存模型入手. golang属于c family, 而c程序在unix的内在模型: |低地址|text|data|bss|heap-->|unu ...
- golang学习笔记 ---面向并发的内存模型
Go语言是基于消息并发模型的集大成者,它将基于CSP模型的并发编程内置到了语言中,通过一个go关键字就可以轻易地启动一个Goroutine,与Erlang不同的是Go语言的Goroutine之间是共享 ...
- Golang面向并发的内存模型
Import Advanced Go Programming 1.5 面向并发的内存模型 在早期,CPU都是以单核的形式顺序执行机器指令.Go语言的祖先C语言正是这种顺序编程语言的代表.顺序编程语言中 ...
- 从GO内存模型与调用协议理解defer closure的坑
资料参考: 官网defer介绍: https://blog.golang.org/defer-panic-and-recover 深入解析go: 多值返回: https://tiancaiamao.g ...
- 栈 堆 stack heap 堆内存 栈内存 内存分配中的堆和栈 掌握堆内存的权柄就是返回的指针 栈是面向线程的而堆是面向进程的。 new/delete and malloc/ free 指针与内存模型
小结: 1.栈内存 为什么快? Due to this nature, the process of storing and retrieving data from the stack is ver ...
- Java内存模型深度解析:总结--转
原文地址:http://www.codeceo.com/article/java-memory-7.html 处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会 ...
- JVM学习(3)——总结Java内存模型
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 为什么学习Java的内存模式 缓存一致性问题 什么是内存模型 JMM(Java Memory Model)简 ...
- 浅析java内存模型--JMM(Java Memory Model)
在并发编程中,多个线程之间采取什么机制进行通信(信息交换),什么机制进行数据的同步? 在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的. 线程之间通过共享程序公共的状态,通 ...
随机推荐
- 小白学 Python 爬虫(39): JavaScript 渲染服务 scrapy-splash 入门
人生苦短,我用 Python 前文传送门: 小白学 Python 爬虫(1):开篇 小白学 Python 爬虫(2):前置准备(一)基本类库的安装 小白学 Python 爬虫(3):前置准备(二)Li ...
- 全网最详细!Centos7.X 搭建Grafana+Jmeter+Influxdb 性能实时监控平台
背景 日常工作中,经常会用到Jmeter去压测,毕竟LR还要钱(@¥&*...),而最常用的接口压力测试,我们都是通过聚合报告去查看压测结果的,然鹅聚合报告的真的是丑到家了,作为程序猿这当然不 ...
- JsonResponse和HttpResponse
1.联系 JsonResponse继承HttpResponse 2.区别 JsonResponse 数据类型装自动换成json字符串并相应到前端,传到前端的是数据类型而非json字符串 HttpRes ...
- windows下配置tomcat的虚拟路径编译器为IDEA
在tomcat中配置好后发现运行项目还是无效 我们需要打开tomcat的配置,把下方的红色区域勾选上即可生效 具体原因如下 那是由于默认时IDEA的热部署机制,会在以下路径: [C:\Users\系统 ...
- 网络通信-基本概念:网络、IP地址、端口、socket
目录 网络通信 1 网络 1.1 网络定义 1.2 使用网络的目的 1.3 总结 2 IP地址 2.1 ip地址的作用 2.2 ip地址的分类 3 端口 3.1 什么是端口 3.2 端口号 3.3 端 ...
- [bzoj4444] [loj#2007] [洛谷P4155] [Scoi2015] 国旗计划
Description \(A\) 国正在开展一项伟大的计划--国旗计划.这项计划的内容是边防战士手举国旗环绕边境线奔袭一圈.这项计划需要多名边防战士以接力的形式共同完成,为此,国土安全局已经挑选了 ...
- ninject 的 实现 的 理解
mvc 用ninject 好像 有 的. 加上 ClassDiagram .ClassDiagram1.rar Represents a site on a type where a value c ...
- typescript step by step interface class
- Math.Atan2 方法
返回正切值为两个指定数字的商的角度. public static double Atan2 ( double y, double x ) 参数 y 点的 y 坐标. x 点的 x 坐标. 返回值 角 ...
- 【原创】(一)Linux进程调度器-基础
背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...