複数のゴルーチンが同じデータに同時にアクセスする場合、同時アクセス操作はシリアル化する必要があります。 Go での読み取りと書き込みのシリアル化は、チャネル通信またはその他の同期プリミティブ (ミューテックス ロック、同期パッケージの読み取り/書き込みロック、sync/atomic のアトミック操作など) を通じて保証できます。
単一の goroutine では、読み取りと書き込みの動作が、プログラムで指定された実行順序と一致している必要があります。言い換えれば、コンパイラとプロセッサは、言語仕様で定義された動作を変更することなく、単一のゴルーチン内の命令を並べ替えることができます。
a := 1 b := 2
命令の並べ替えにより、b := 2
が a := 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 := 1
が a := 5# より前に実行される可能性があります。 ## 、最終的には goroutine B が 0 を出力する可能性があります。
”注: 上記の例は間違った例であり、説明のみを目的としています。
”
読み取りおよび書き込み操作の要件を明確にするために、Go では、メモリ操作を実行するための部分的な順序関係を表す happens before
を導入しました。
複数のゴルーチンが共有変数にアクセスする場合、同期イベントを確立して、happens-before 条件が確実に実行されるようにする必要があります。これにより、読み取りで予期された書き込みが確実に行われるようになります。
に w の観察を許可します:
# r は w の前には現れません。w が r の前に出現することを保証します。
迷っているなら、それは正解です。老徐も最初は混乱していましたが、この 2 つの条件は同じでした。このため、老許は特別に原文と繰り返し比較し、上記の理解が正しいことを確認しました。
考え方を変えて、逆推論をしてみましょう。 2 つの条件が同じであれば、元のテキストを 2 回書く必要はありませんが、当然のことながら、問題は単純ではありません。
分析を続ける前に、中国語の先生に感謝したいと思います。先生なしでは、両者の違いを見つけることはできなかったでしょう。
r が w
より前に発生しない場合、r が発生する可能性がある状況は、以下の図に示すように、r が w の後、または w と同時に発生することです (実線は、w
w の後および r
程序初始化运行在单个goroutine中,但是该goroutine可以创建其他并发运行的goroutine。
如果包p导入了包q,则q包init函数执行结束先于p包init函数的执行。main函数的执行发生在所有init函数执行完成之后。
goroutine的创建先于goroutine的执行。老许觉得这基本就是废话,但事情总是没有那么简单,其隐含之意大概是goroutine的创建是阻塞的。
func sleep() bool { time.Sleep(time.Second) return true } go fmt.Println(sleep())
上述代码会阻塞主goroutine一秒,然后才创建子goroutine。
goroutine的退出是无法预测的。如果用一个goroutine观察另一个goroutine,请使用锁或者Channel来保证相对有序。
Channel通信是goroutine之间同步的主要方式。
Channel的发送动作先于相应的接受动作完成之前。
无缓冲Channel的接受先于该Channel上的发送完成之前。
这两点总结起来分别是开始发送
、开始接受
、发送完成
和接受完成
四个动作,其时序关系如下。
开始发送 > 接受完成 开始接受 > 发送完成
“注意:开始发送和开始接受并无明确的先后关系
”
Channel的关闭发生在由于通道关闭而返回零值接受之前。
容量为C的Channel第k个接受先于该Channel上的第k+C个发送完成之前。
这里使用极限法应该更加易于理解,如果C为0,k为1则其含义和无缓冲Channel的一致。
对于任何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.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
。
“老许在本地实验了多次结果都是输出
”00
,20
这个输出估计只活在理论之中了。
错误示范二
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 サイトの他の関連記事を参照してください。