首頁 >後端開發 >Golang >Go語言的記憶體模型介紹

Go語言的記憶體模型介紹

尚
轉載
2020-01-10 17:01:202270瀏覽

Go語言的記憶體模型介紹

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的初始化之后的结果。

更多go语言知识请关注PHP中文网go语言教程栏目。

以上是Go語言的記憶體模型介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除