博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
内存模型
阅读量:7082 次
发布时间:2019-06-28

本文共 4727 字,大约阅读时间需要 15 分钟。

 

译者

    原文:http://golang.org/go_mem.html     翻译:柴树杉(chaishushan@gmail.com)     翻译:BianJiang(borderj@gmail.com)     校正:ROTC (ring.of.the.c@gmail.com)

简介

Go的内存模型详述了"在一个groutine中对变量进行读操作能够侦测到在其他goroutine中对该变量的写操作"的条件.

Happens Before

对于一个goroutine来说,它其中变量的读, 写操作执行表现必须和从所写的代码得出的预期是一致的。也就是说,在不改变程序表现的情况下,编译器和处理器为了优化代码可能会改变变量的操作顺序即: 指令乱序重排。但是在两个不同的goroutine对相同变量操作时, 会因为指令重排导致不同的goroutine对变量的操作顺序的认识变得不一致。例如,一个goroutine执行a = 1; b = 2;,在另一个goroutine中可能会现感知到变量b先于变量a被改变。

为了解决这种二义性问题,Go语言中引进一个happens before的概念,它用于描述对内存操作的先后顺序问题。如果事件e1 happens before 事件 e2,我们说事件e2 happens after e1。如果,事件e1 does not happen before 事件 e2,并且 does not happen after e2,我们说事件e1和e2同时发生。

对于一个单一的goroutine,happens before 的顺序和代码的顺序是一致的。

如果能满足以下的条件,一个对变量v的读事件r可以感知到另一个对变量v的写事件w:

  1. 写事件w happens before 读事件r。
  2. 没有既满足 happens after w 同时满主 happens before r 的对变量v的写事件w。

为了保证读事件r可以感知对变量v的写事件,我们首先要确保w是变量v的唯一的写事件。同时还要满足以下条件:

  1. 写事件w happens before 读事件r。
  2. 其他对变量v的访问必须 happens before 写事件w 或者 happens after 读事件r。

第二组条件比第一组条件更加严格。因为,它要求在w和 r并行执行的程序中不能再有其他的读操作。

对于在单一的goroutine中两组条件是等价的,读事件可以确保感知到对变量的写事件。但是,对于在 两个goroutines共享变量v,我们必须通过同步事件来保证 happens-before 条件 (这是读事件感知写事件的必要条件)。

将变量v自动初始化为零也是属于这个内存操作模型。

读写超过一个机器字长度的数据,顺序也是不能保证的。

同步(Synchronization)

初始化

程序的初始化在一个独立的goroutine中执行。在初始化过程中创建的goroutine将在 第一个用于初始化goroutine执行完成后启动。

如果包p导入了包q,包q的init 初始化函数将在包p的初始化之前执行。

程序的入口函数 main.main 则是在所有的 init 函数执行完成 之后启动。

在任意init函数中新创建的goroutines,将在所有的init 函数完成后执行。

Goroutine的创建

用于启动goroutine的go语句在goroutine之前运行。

例如,下面的程序:

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

调用hello函数,会在某个时刻打印“hello, world”(有可能是在hello函数返回之后)。

Channel communication 管道通信

用管道通信是两个goroutines之间同步的主要方法。通常的用法是不同的goroutines对同一个管道进行读写操作,一个goroutines写入到管道中,另一个goroutines从管道中读数据。

管道上的发送操作发生在管道的接收完成之前(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的赋值发生在向管道 c发送数据之前,而管道的发送操作在管道接收完成之前发生。因此,在print 的时候,a已经被赋值。

从一个unbuffered管道接收数据在向管道发送数据完成之前发送。

下面的是示例程序:

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的赋值在从管道接收数据 前发生,而从管道接收数据操作在向unbuffered 管道发送完成之前发生。所以,在print 的时候,a已经被赋值。

如果用的是缓冲管道(如 c = make(chan int, 1) ),将不能保证输出 “hello, world”结果(可能会是空字符串,但肯定不会是他未知的字符串, 或导致程序崩溃)。

包sync实现了两种类型的锁: sync.Mutex 和 sync.RWMutex

对于任意 sync.Mutex 或 sync.RWMutex 变量l。 如果 n < m ,那么第n次 l.Unlock() 调用在第 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”结果。因为,第一次 l.Unlock() 调用(在f函数中)在第二次 l.Lock() 调用(在main 函数中)返回之前发生,也就是在 print 函数调用之前发生。

For any call to l.RLock on a sync.RWMutex variable l, there is an n such that the l.RLock happens (returns) after the n'th call to l.Unlock and the matching l.RUnlock happens before the n+1'th call to l.Lock.

Once

包once提供了一个在多个goroutines中进行初始化的方法。多个goroutines可以 通过 once.Do(f) 方式调用f函数。但是,f函数 只会被执行一次,其他的调用将被阻塞直到唯一执行的f()返回。once.Do(f) 中唯一执行的f()发生在所有的 once.Do(f) 返回之前。

有代码:

var a string func setup(){
        a ="hello, world"; } func doprint(){
        once.Do(setup);         print(a); } func twoprint(){
        go doprint();         go doprint(); }

调用twoprint会输出“hello, world”两次。第一次twoprint 函数会运行setup唯一一次。

错误的同步方式

注意:变量读操作虽然可以侦测到变量的写操作,但是并不能保证对变量的读操作就一定发生在写操作之后。

例如:

var a, b int func f(){
        a =1;         b =2; } func g(){
        print(b);         print(a); } func main(){
        go f();         g(); }

函数g可能输出2,也可能输出0。

这种情形使得我们必须回避一些看似合理的用法。

这里用Double-checked locking的方法来代替同步。在例子中,twoprint函数可能得到错误的值:

var a string vardonebool 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赋值了,但是没有办法给出保证这一点,所以函数可能输出空的值。

另一个错误陷阱是忙等待:

var a string vardonebool func setup(){
        a ="hello, world";         done=true; } func main(){
        go setup();         for!done{
        }         print(a); }

我们没有办法保证在main中看到了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 条件并且退出了循环,但是任何然 不能保证它看到了g.msg的初始化之后的结果。

在这些例子中,只有一种解决方法:用显示的同步。

转载地址:http://troml.baihongyu.com/

你可能感兴趣的文章
ORA-10997:another startup/shutdown operation of this instance in progress解决方法
查看>>
Velocity工作原理解析和优化
查看>>
zabbix 监控 Tomcat
查看>>
如何用Exchange Server 2003 构建多域名邮件系统
查看>>
Delphi内嵌汇编语言BASM精要(转帖)
查看>>
ASP.NET MVC 在控制器中接收视图表单POST过来的数据方法
查看>>
云计算这么火,但市场发展依然存在着7大障碍
查看>>
Oracle 11g AMM与ASMM切换
查看>>
bootstrap-wysiwyg中JS控件富文本中的图片由本地上传到服务器(阿里云、七牛、自己的数据库)...
查看>>
H3 BPM SharePoint解决方案
查看>>
[原]linux 修改 hostname 立即生效
查看>>
图片抖动效果(兼容)
查看>>
windows查看端口占用情况
查看>>
ASA NAT Priority
查看>>
ping -R和traceroute的测试
查看>>
10.python网络编程(解决粘包问题 part 2)
查看>>
自动移动域中计算机到目标OU的脚本
查看>>
grep/sed/awk实战
查看>>
nagios图像(pnp4nagios)
查看>>
如何清除Xcode8打印的系统日志
查看>>