zoukankan      html  css  js  c++  java
  • Go语言内存模型

    转自:https://code.google.com/p/golang-china/wiki/go_mem

    简介

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

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

    var a string
    var done bool
    
    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的初始化之后的结果。

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

  • 相关阅读:
    MySql cmd下的学习笔记 —— 引擎和事务(engine,transaction)
    MySql cmd下的学习笔记 —— 有关视图的操作(algorithm)
    MySql cmd下的学习笔记 —— 有关视图的操作(建立表)
    MySql cmd下的学习笔记 —— 有关常用函数的介绍(数学函数,聚合函数等等)
    MySql cmd下的学习笔记 —— 有关多表查询的操作(多表查询练习题及union操作)
    MySql 在cmd下的学习笔记 —— 有关多表查询的操作(内连接,外连接,交叉连接)
    MySql cmd下的学习笔记 —— 有关子查询的操作(where型,from型,exists型子查询)
    MySql cmd下的学习笔记 —— 有关select的操作(order by,limit)
    剑指Offer--第21题 调整数组顺序使奇数位于偶数前面;
    剑指Offer--和为s的连续正数序列
  • 原文地址:https://www.cnblogs.com/sevenyuan/p/3029388.html
Copyright © 2011-2022 走看看