ホームページ  >  記事  >  バックエンド開発  >  固有の Go メモリ モデルと Happen-Before

固有の Go メモリ モデルと Happen-Before

Go语言进阶学习
Go语言进阶学习転載
2023-07-24 14:50:411178ブラウズ
#Go メモリ モデルは、Goroutine が同じ変数への他の Goroutine の書き込みをどのように観察できるかを明確に示しています。

#複数のゴルーチンが同じデータに同時にアクセスする場合、同時アクセス操作をシリアル化する必要があります。 Go での読み取りと書き込みのシリアル化は、チャネル通信またはその他の同期プリミティブ (ミューテックス ロック、同期パッケージの読み取り/書き込みロック、sync/atomic のアトミック操作など) を通じて保証できます。

Happens Before

単一の goroutine では、読み取りと書き込みの動作が、プログラムで指定された実行順序と一致している必要があります。言い換えれば、コンパイラとプロセッサは、言語仕様で定義された動作を変更することなく、単一のゴルーチン内の命令を並べ替えることができます。

a := 1
b := 2

命令の並べ替えにより、b := 2a := 1 より前に実行される可能性があります。単一のゴルーチンでは、実行順序を調整しても最終結果には影響しません。ただし、複数の goroutine シナリオでは問題が発生する可能性があります。

var a, b int
// goroutine A
go func() {
    a := 5
    b := 1
}()
// goroutine B
go func() {
    for b == 1 {}
    fmt.Println(a)
}()

上記のコードを実行すると、ゴルーチン B は通常 5 を出力するはずですが、命令の並べ替えにより、b := 1a := 5# より前に実行される可能性があります。 ## 、最終的には goroutine B が 0 を出力する可能性があります。

: 上記の例は間違った例であり、説明のみを目的としています。

読み取りおよび書き込み操作の要件を明確にするために、Go では、メモリ操作を実行するための部分的な順序関係を表す happens before を導入しました。

happens-before の役割

複数のゴルーチンが共有変数にアクセスする場合、同期イベントを確立して、happens-before 条件が確実に実行されるようにする必要があります。これにより、読み取りで予期された書き込みが確実に行われるようになります。

前に何が起こるか##​​

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

単一の goroutine では、happens-before の順序がプログラムの実行順序になります。では、happens-before の順序は何でしょうか?以下に条件を見てみましょう。

変数 v に対する読み取り操作 r と書き込み操作 w が次の 2 つの条件を満たす場合、r は

に w の観察を許可します:

# r は w の前には現れません。
  1. w の後および r の前には、他の書き込み操作は発生しません。
  2. 変数 v の読み取り操作 r が特定の書き込み操作 w を確実に監視できるようにするには、r によって監視できる書き込み操作が w だけであることを確認する必要があります。 。次に、r と w の両方が次の条件を満たす場合、r は

w が r の前に出現することを保証します。

  1. 他の書き込み操作は、w の前と r の後に発生します。
  2. 単一の goroutine には同時実行性はありません。これら 2 つの条件は同等です。 Lao Xu はこれに基づいて拡張し、これら 2 つの条件セットがシングルコアの動作環境では同等であることを発見しました。同時実行の場合、後者の条件セットは最初の条件セットよりも厳格です。

    迷っているなら、それは正解です。老徐も最初は混乱していましたが、この 2 つの条件は同じでした。このため、老許は特別に原文と繰り返し比較し、上記の理解が正しいことを確認しました。

    固有の Go メモリ モデルと Happen-Before

    考え方を変えて、逆推論をしてみましょう。 2 つの条件が同じであれば、元のテキストを 2 回書く必要はありませんが、当然のことながら、問題は単純ではありません。

    固有の Go メモリ モデルと Happen-Before

    分析を続ける前に、中国語の先生に感謝したいと思います。先生なしでは、両者の違いを見つけることはできなかったでしょう。

    r が w より前に発生しない場合、r が発生する可能性がある状況は、以下の図に示すように、r が w の後、または w と同時に発生することです (実線は、w

    が存在することを示します)。同時に発生する可能性があります)。
    固有の Go メモリ モデルと Happen-Before

    w の後および r

    の前に他の書き込み操作は発生しません。その後、他の書き込み w' が w の前または w と同時に発生する可能性があります。以下の図に示すように、r の後に、または r と同時に発生する可能性があります (実線は同時に発生する可能性があることを示します)。
    固有の Go メモリ モデルと Happen-Before

    2 番目の条件セットは非常に明確です。以下に示すように、w は r の前に発生し、他の書き込み操作は w の前か r の後にのみ発生します (空白は、w が r の前に発生することを示します)。同時に実行することはできません)。
    固有の Go メモリ モデルと Happen-Before

    #この時点で、2 番目の条件セットが最初の条件セットよりも厳しい理由を理解できるはずです。最初の条件セットでは w の観測が許可され、2 番目の条件セットでは w の観測が保証されます。

    Go での同期

    以下は、Go で合意されたいくつかの同期イベントです。これにより、プログラムが確実に前発生の原則に従うようになります。したがって、並行ゴルーチンを比較的規則的に作成します。 ###

    Go的初始化

    程序初始化运行在单个goroutine中,但是该goroutine可以创建其他并发运行的goroutine。

    如果包p导入了包q,则q包init函数执行结束先于p包init函数的执行。main函数的执行发生在所有init函数执行完成之后。

    goroutine的创建结束

    goroutine的创建先于goroutine的执行。老许觉得这基本就是废话,但事情总是没有那么简单,其隐含之意大概是goroutine的创建是阻塞的。

    func sleep() bool {
       time.Sleep(time.Second)
       return true
    }
    
    go fmt.Println(sleep())

    上述代码会阻塞主goroutine一秒,然后才创建子goroutine。

    goroutine的退出是无法预测的。如果用一个goroutine观察另一个goroutine,请使用锁或者Channel来保证相对有序。

    Channel的发送和接收

    Channel通信是goroutine之间同步的主要方式。

    • Channel的发送动作先于相应的接受动作完成之前。

    • 无缓冲Channel的接受先于该Channel上的发送完成之前。

    这两点总结起来分别是开始发送开始接受发送完成接受完成四个动作,其时序关系如下。

    开始发送 > 接受完成
    开始接受 > 发送完成

    注意:开始发送和开始接受并无明确的先后关系

    • Channel的关闭发生在由于通道关闭而返回零值接受之前。

    • 容量为C的Channel第k个接受先于该Channel上的第k+C个发送完成之前。

    这里使用极限法应该更加易于理解,如果C为0,k为1则其含义和无缓冲Channel的一致。

    Lock

    对于任何sync.Mutex或sync.RWMutex变量l以及n < m,第n次l.Unlock()的调用先于第m次l.Lock()的调用返回。

    假设n为1,m为2,则第二次调用l.Lock()返回前一定要先调用l.UnLock()。

    对于sync.RWMutex的变量l存在这样一个n,使得l.RLock()的调用返回在第n次l.Unlock()之后发生,而与之匹配的l.RUnlock()发生在第n + 1次l.Lock()之前。

    不得不说,上面这句话简直不是人能理解的。老许将其翻译成人话:

    有写锁时:l.RLock()的调用返回发生在l.Unlock()之后。

    有读锁时:l.RUnlock()的调用发生在l.Lock()之前。

    注意:调用l.RUnlock()前不调用l.RLock()和调用l.Unlock()前不调用l.Lock()会引起panic。

    Once

    once.Do(f)中f的返回先于任意其他once.Do的返回。

    不正确的同步

    错误示范一

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

    这个例子看起来挺简单,但是老许相信大部分人应该会忽略指令重排序引起的异常输出。假如goroutine f指令重排序后,b=2先于a=1发生,此时主goroutine观察到b发生变化而未观察到a变化,因此有可能输出20

    老许在本地实验了多次结果都是输出0020这个输出估计只活在理论之中了。

    错误示范二

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

    这种双重检测本意是为了避免同步的开销,但是依旧有可能打印出空字符串而不是“hello, world”。说实话老许自己都不敢保证以前没有写过这样的代码。现在唯一能想到的场景就是其中一个goroutine doprint执行到done = true(指令重排序导致done=true先于a="hello, world"执行)时,另一个goroutine doprint刚开始执行并观察到done的值为true从而打印空字符串。

以上が固有の Go メモリ モデルと Happen-Beforeの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

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