ホームページ >バックエンド開発 >Golang >Go言語のメモリモデルの紹介

Go言語のメモリモデルの紹介

尚
転載
2020-01-10 17:01:202287ブラウズ

Go言語のメモリモデルの紹介

Go のメモリ モデルでは、「あるゴルーチン内の変数に対する読み取り操作が別のゴルーチン内の変数に対する書き込み操作を検出できる」条件が詳しく説明されています。

Happens Before

ゴルーチンの場合、その変数の読み取りおよび書き込み操作は、記述されたコードから期待どおりに実行される必要があり、一貫性があります。言い換えれば、プログラムのパフォーマンスを変えることなく、コンパイラーとプロセッサーはコードを最適化するために変数の演算順序を変更する可能性があります。つまり、命令が順序どおりに並べ替えられません。

しかし、2 つの異なるゴルーチンが同じ変数を操作する場合、命令の再配置により、異なるゴルーチン間で変数操作の順序の理解に一貫性がなくなる可能性があります。たとえば、あるゴルーチンが a = 1; b = 2; を実行すると、別のゴルーチンは変数 a の前に変数 b が変更されたことに気付く可能性があります。

このあいまいさの問題を解決するために、Go 言語では、メモリ操作の順序を記述するために使用される前に起こるという概念が導入されています。イベント e1 がイベント e2 より前に発生する場合、イベント e2 は e1 の後に発生すると言います。

イベント e1 がイベント e2 の前に発生せず、e2 の後にも発生しない場合、イベント e1 と e2 は同時に発生すると言います。

単一のゴルーチンの場合、前に発生する順序はコードの順序と一致します。

次の条件が満たされる場合、変数 v の「読み取りイベント r」は、変数 v の別の「書き込みイベント w」を認識できます:

1. 「書き込みイベント w」 」が発生する前に「イベントrを読む」。

2. w の後に発生し、同時に r の前に発生するという条件を満たす変数 v への書き込みイベントはありません。

読み取りイベント r が変数 v への書き込みイベントを確実に感知できるようにするには、まず、w が変数 v の唯一の書き込みイベントであることを確認する必要があります。同時に、次の条件が満たされる必要があります:

1. 「書き込みイベント w」は「読み取りイベント r」の前に発生します。

2. 変数 v へのその他のアクセスは、「書き込みイベント w」の前、または「読み取りイベント r」の後に発生する必要があります。

2 番目の条件セットは、最初の条件セットよりも厳しいです。これは、プログラム内で w と r が並行して実行される他の読み取り操作が存在できないことが必要なためです。

単一の goroutine 内で 2 つの条件セットが等価になるように、読み取りイベントによって変数への書き込みイベントが確実に検知されるようにすることができます。ただし、2 つのゴルーチン間の共有変数 v については、同期イベントを通じて前発生条件を保証する必要があります (これは、読み取りイベントが書き込みイベントを認識するために必要な条件です)。

変数 v をゼロに自動的に初期化することも、このメモリ操作モデルに属します。

1 マシンワードの長さを超えるデータの読み書きの順序は保証されません。

#同期

初期化

プログラムは別の goroutine 実装で初期化されます。初期化中に作成されたゴルーチンは、初期化に使用された最初のゴルーチンの実行が完了した後に開始されます。

パッケージ p がパッケージ q をインポートする場合、パッケージ q の init 初期化関数はパッケージ p の初期化前に実行されます。

プログラムのエントリ関数 main.main は、すべての init 関数が実行された後に開始されます。

init 関数内で新しく作成されたゴルーチンは、すべての init 関数が完了した後に実行されます。

Goroutine の作成

Goroutine の開始に使用される go ステートメントは、Goroutine の前に実行されます。

たとえば、次のプログラム:

var a string;

func f() {
        print(a);
}

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

hello 関数を呼び出すと、特定の瞬間 (おそらく hello 関数が戻った後) に「hello, world」が出力されます。

チャネル通信パイプライン通信

パイプ通信の使用は、2 つのゴルーチン間の同期の主な方法です。一般的な使用法は、異なるゴルーチンが同じパイプ上で読み取りおよび書き込み操作を実行し、1 つのゴルーチンがパイプに書き込み、他のゴルーチンがパイプからデータを読み取ることです。

パイプ上の送信操作は、パイプの受信が完了する前に発生します (前に発生します)。

たとえば、このプログラム:

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」が出力されることを保証します。これは、パイプ c にデータを送信する前に a の割り当てが発生し、パイプの受信が完了する前にパイプの送信操作が発生するためです。したがって、印刷時に、 a には値が割り当てられます。

バッファリングされていないパイプからのデータの受信は、パイプへのデータの送信が完了する前に送信されます。

以下はサンプルプログラムです:

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 の割り当てはパイプからデータを受信する前に行われ、パイプからデータを受信する操作はバッファリングされていないパイプへの送信が完了する前に行われるためです。したがって、印刷時には が割り当てられます。

バッファ付きパイプ (c = make(chan int, 1) など) を使用する場合、「hello, world」の結果が出力されるという保証はありません (空の文字列である可能性もありますが、未知の文字列であることは間違いなく、プログラムがクラッシュすることもありません。

Lock

Package sync は、sync.Mutex と sync.RWMutex という 2 種類のロックを実装します。

对于任意 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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcnblogs.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。