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

    内存模型介绍

    Go语言的内存模型规定了一种规则。这种规则可以保证,在一个goroutine读取某个变量的值,是其他goroutine对同一个变量写入的值。

    建议

    程序中多个goroutine同时修改相同数据,必须使之能够按序访问。为了按序访问以及保护数据,可以使用channel操作或者其他同步原语例如sync及sync/atomic包。

    If you must read the rest of this document to understand the behavior of your program, you are being too clever.

    Don't be clever.

    Happens-Before原则

    在单个goroutine中,读取和写入的操作一定与程序的执行顺序一致。 也就是说,仅当重新排序语句不会改变语言规范所定义的该goroutine中的行为时,编译器和处理器才可以对单个goroutine中执行的读取和写入进行重新排序。 由于此重新排序,一个goroutine观察到的执行顺序可能不同于另一个goroutine所察觉的执行顺序。 例如,如果一个goroutine执行a = 1; b = 2;,另一个goroutine可能会a的值更新前,就已经观察到b的更新后的值。

    在Go程序中,为了限定读取和写入的要求,我们定义happens-before原则,此原则限定了程序在读写内存时的一种偏序关系。 如果事件e1发生在事件e2之前,那么我们说e2发生在e1之后。 同样,如果e1不在e2之前发生并且在e2之后也没有发生,那么我们说e1和e2同时发生。

    Happen-Before是一个术语,不仅仅是go才有的。

    原始的定义是由Leslie Lamport在1978年的文章 Time, Clocks and the Ordering of Events in a Distributed System中提出来的

    另见维基百科 https://en.wikipedia.org/wiki/Happened-before

    在单个goroutine中,happens-before发生顺序即是程序中的顺序。

    如果同时满足下述两个规则,那么对变量v的读操作r能够观察到对变量v的写操作w:

    1. 读操作r没有发生在写操作w之前
    2. 没有另外一个针对变量v的写操作w`发生在,写操作w之后且在读操作r之前

    为了保证对变量v的读操作r能够观察到对变量v的特定的写操作w的值,即确保读操作r可以观察到写操作w是唯一的对变量v的写操作。 那么,如果满足下述两个原则,读操作r将能够观察到写操作w

    1. w发生在r之前
    2. 任何其他的对共享变量v的写操作w`发生在写操作w之前或者读操作r之后

    这对条件的约束强于前面一对条件的约束;它需要没有其他任何的写操作为w`,与写操作w 或 读操作r同时发生。

    在单一的goroutine中,由于没有并发,因此两个定义也是等价的:读操作r可以观察到最近的写操作w写入到v的值。当多个goroutine访问一个共享变量v时,必须使用同步事件建立happens-before条件来确保读操作观察到期望的写操作的变化。

    使用变量v的类型零值初始化变量v的行为与内存模型中写操作相同。

    如果读取和写入的值大于一个机器字,那么多个机器字节的操作顺序不是固定的顺序。

    同步

    初始化

    程序初始化操作是在单个goroutine中,但是该goroutine可能会创建的其他goroutine,这些goroutines是并发运行的。

    如果包p导入了包q,则q包中的的init函数的完成发生在任何p包中的init函数开始之前。

    函数main.main的启动发生在所有init函数完成之后。

    Goroutine创建

    使用go语句启动新的goroutine发生在此goroutine执行前。

    例如下面的程序

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

    调用hello()函数会在某个时刻打印hello, world字符串(也可能打印字符串的时刻发生在hello函数返回之后)

    Goroutine销毁

    不能保证goroutine的退出在程序中发生任何事件之前发生。 例如下面的程序中:

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

    对变量a的赋值没有跟随任何同步事件之后, 所以不能保证会被其他goroutine观察到。 事实上,一个激进的编译器可能会删除整个go声明语句。

    如果想让一个goroutine的执行操作影响必须对另外一个goroutine可见, 可以使用同步机制(例如lock或者channel通信)来确定相对的顺序。

    Channel 通信

    channel通信是goroutine之间同步数据的主要方法。 通常在不同的goroutine中,将特定channel上的每个发送与该channel上的相应接收进行匹配。

    channel上的发送 happens-before 该channel上的相应接收操作完成。

    下面程序

    Copy
    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信息的发送,c的发送一定会先于c接受的完成,因此这些都先于print调用完成

    happens before 具有传递性

    关闭一个channel happens-before channel收到返回零值, 因为channel已经关闭了

    在前面的例子中,如果使用close(c)代替c <- 0,能够保证相同的行为。

    从一个无缓冲的channel接收数据 happens-before 向这个channel完成发送数据之前

    下面这段程序(如前面的代码,但是使用无缓冲的channel)

    Copy
    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的写操作发生在channel变量c的接收之前, c的接收发生在c的发送之前,c的发送发生在print打印之前。

    如果是带缓冲的channel(例如c = make(chan int, 1)),那么程序则不一定能保证会打印“hello,world”。(可能打印空字符串或者crash、或者其他未知情况)

    在缓冲大小为C的chanel上接收第k个数据 happens-before 在第 k + C发送完成之前。

    该规则将前一个规则(无缓冲的channel)推广到缓冲channel。 它允许通过缓冲的channel对计数信号量进行建模:channel中的项目数量对应于活动使用的数量,channel的容量对应于同时使用的最大数量,发送一个项目获取信号量,以及 接收项目会释放信号量。 这是限制并发性的常见用法。

    该程序为工作列表中的每个条目启动一个goroutine,但是goroutine使用限制channel进行协调,以确保一次最多运行三个work。

    Copy
    var limit = make(chan int, 3)
    
    func main() {
        for _, w := range work {
            go func(w func()) {
                limit <- 1
                w()
                <-limit
            }(w)
        }
        select{}
    }
    

    Locks 锁

    sync包实现了两种类型的锁:互斥锁(sync.Mutex)及读写锁(sync.RWMutex)

    对于任意 sync.Mutex 或 sync.RWMutex 类型的变量l。 如果 n < m ,那么第n次 l.Unlock() 调用在第 m次 l.Lock()调用返回前发生。

    下面程序

    Copy
    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函数中),第二次调用l.Lock()发生在print之前。

    上面的例子相当于 n=1, m=2

    在一个sync.RWMutex变量l上任何调用 l.RLock,都有一个n,使得l.RLock在调用n个l.Unlock之后发生(并返回),并且匹配的l.RUnlock发生在调用第n + 1个l.Lock之前。

    Once

    这个同步包为多个goroutines进行初始化只执行一次的操作,提供了一种安全的机制,多个线程可以执行once.Do(f),那么函数会且只会有一个执行f函数,而其他调用将阻塞直到f函数的返回。

    使用once.Do(f)执行f()并返回,发生在任何调用once.Do(f)返回之前。

    下面程序

    Copy
    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将只调用一次setup函数。 setup函数将在两次调用打印之前完成。 结果将是“ hello,world”将被打印两次。

    错误的同步

    注意,读操作r可能会观察到与r同时发生的写操作w写入的值。 即使发生这种情况,也并不意味着在r之后发生的读取将观察到在w之前发生的写入。

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

    有可能打印出2和0

    即b已经赋值为2,但是a还未被赋值

    这个事实使一些常见的习语无效。

    双检锁是为了避免同步的开销,例如,twoprint程序可能会错误地写成

    Copy
    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的赋值变化。 这里可能会输出空字符串,而不是希望的hello, world

    下述为另外一个错误示例(一直忙等)

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

    上述示例所示,在主函数main中,观察到done值为true并不能保证一定能看到对a的赋值变化, 因此本示例程序也有可能打印空值。 更糟糕的是,setup函数对done=true的赋值,并不一定能被main函数观察到,因此这两个线程之间并没有使用任何的同步事件。 因此for循环在主函数中并不能保证一定会循环结束。

    下述为一些更为难以发现的变体

    Copy
    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)
    }
    

    即使主函数观察到g != nil且退出了循环,也不能保证main能够观察到g.msg的赋值内容。

    对于上述的所有错误示例,都可以采用同样的解决办法:使用明确的同步方式。

    每天逼着自己写点东西,终有一天会为自己的变化感动的。这是一个潜移默化的过程,每天坚持编编故事,自己不知不觉就会拥有故事人物的特质的。 Explicit is better than implicit.(清楚优于含糊)
  • 相关阅读:
    hdu 3746 Cyclic Nacklace
    hdu 3336 Count the string
    hdu 1358 Period
    hiho一下 第一周 最长回文子串
    KMP算法详解
    Java 之 static的使用方法(6)
    Jave 之方法-函数(5)
    Java 之数组(4)
    大数据-storm学习资料视频
    大数据-spark-hbase-hive等学习视频资料
  • 原文地址:https://www.cnblogs.com/kylin5201314/p/14945289.html
Copyright © 2011-2022 走看看