Goのメモリモデル
要約
Goのメモリモデルはある書き込み(write)がある読み出し(read)から観察されるための条件を記述する。
その条件は、イベント間の"happens before"という半順序関係によって記述される。
この関係をこの記事では簡単に<と書く。
さらに、2つのイベント間に"happens before"関係が結ばれるのはどのようなときか、を表すいくつかのルールがある。これらのルールの組により、Goのメモリモデルが構成される。
問題意識
the execution order observed by one goroutine may differ from the order perceived by another.
あるgoroutineから観測される実行順序は、他のgoroutineから観測される実行順序とは異なることがある。
これがなぜなのかを知るにはコンパイラやハードウェアの知識が必要だが、メモリモデルそのものを理解するにはそれらの知識は不要
問題意識を示す典型的な例
code: example.go
package main
var a, b int
func main() {
go func() {
a = 1
b = 1
}()
for b != 1 {
}
print(a) // 必ずしも1にならない
}
事前に発生する(happens before)という関係 事前発生(happens before)というイベントの間の半順序関係を定義する 別な言い方をすると、happens before関係は数学用語としての半順序関係の性質を満たしているということをGoのメモリモデルは主張している この記事ではこの関係を不等号<で表すことにする
ここでは、「2つのイベントを受け取り、true/falseのどちらかを返す関数」が「イベント間の関係」
ここではe1 happens before e2を e1 < e2 と表すことにする
e1 < e2 かつ e2 < e3ならば e1 < e3も成り立つ(半順序の推移律)
e1 < e2 == false かつ e2 < e1 == false である場合がありうる(「半」順序であり全順序ではないため) Goのメモリモデルにおいてはこのような場合e1, e2がconcurrentであるという
結局、2つのイベントe1とe2をとると、次の3つの場合がある
e1 < e2
e2 < e1
e1とe2はconcurrentである
あるreadイベントがあるwriteイベントを「観察する」条件
「観察することがある(allowed)」ための条件と、「観察することが保証される(guaranteed)」ための条件の2種類がある。
後者の方が前者よりも強い条件となっている。
readイベントrがwriteイベントwを観察することがある(allowed)のは、次の2条件が成り立つときである。
$ r < wではないこと(条件A-1)
$ w < w^{\prime} < rとなるような他のwriteイベントwが存在しないこと(条件A-2)
変数vに対するreadイベントrが、vに対するwriteイベントwを観察することが保証される(guaranteed)のは、次の2条件が成り立つときである。
$ w < rであること(条件G-1)
変数vに対する他の全てのwriteイベントw'について、$ w^{\prime} < wか$ r < w^{\prime}のいずれかが成り立つこと(条件G-2)
条件A-1とG-1を比べると、G-1は rとwがconcurrentである場合を排除している点が異なる
条件A-2とG-2を比べると、G-2はw'がrとwのいずれともconcurrentである場合を排除している点が異なる
コードを読むときにはG-1とG-2が成り立つかどうかをチェックすれば良い
happens before関係を規定するルール(サマリー)
単一のgoroutine内のイベント同士では、プログラムで表現されている順序の通りにhappends-before関係が成り立つ
package pがqをimportするとき、qのinitの完了 < pのinitの開始が成り立つ
全てのinitの完了 < main.main関数の開始が成り立つ
goroutineの終了はどのイベントとも<関係を持たない
あるchannelへの送信 < そのchannelへの対応する受信の完了
あるchannelのclose < そのchannelからのゼロ値の受信(closeされたことによるもの)
あるバッファなしchannelからの受信 < そのchannelへの送信の完了
大きさCのバッファつきchannelをとったとき、k番目の受信 < k+1番目の送信の完了
任意のsync.Mutexまたはsycn.RWMutex変数lとn < mなる任意の自然数n, mについて、
n回目のl.Unlock()呼び出し < m回目のl.Lock()呼び出し
sync.RWMutex変数lに対する任意のl.RLock()呼び出しについて、
n回目のl.Unlock()が戻ってくる < l.RLock() かつ、
対応するl.RUnlock() < n+1回目のl.Lock()
であるような自然数$ nが存在する
once.Do(f)によって引き起こされる唯一のf()が戻ってくる < 任意のonce.Do(f)が戻ってくる
2つのイベントを<で順序づける方法はこれしかない。
↑本当にこれしかないのかな
元記事にはいくつかの「間違ったコーディング」の例が載っている。
それがなぜ間違っているのかは、上記のルールを当てはめた上で、注目するreadとwriteが順序づけられない=concurrentであることからわかる。
これらの誤ったコードを修正するには、明示的にsynchronizeすれば良い。
「イベントが同期されている」ということをメモリモデルの言葉で言い換えれば、「2つのイベントが<関係で結ばれていること」=「2つのイベントがconcurrentな関係にないこと」であると言える。
happens before関係を規定するルール(具体例付き) →TBW
疑問
syncパッケージがsync/atomicパッケージに依存する