Goのメモリモデルメモ
そもそもデータ競合って?
データ競合(data race)は、マルチスレッド・プログラム実装上の問題である。
競合状態(race condition)は、並行処理システム設計上の問題である。
定義: データ競合(data race)とは、(1)複数スレッド間で共有する変数に対して、(2)同時に、(3)読み/書きアクセスが行われる事象を指す
マルチスレッドで動作するプログラムにおいて、変数に対して同時にアクセスが発生する場合は何かしらで起こりうる、この時スレッド間で排他制御する、mutexとかatomicとか。
Goでは、sync/atomic
とかsync
とか使う。
どんなプログラムでも、完全に同時に1つの変数の値を変更することはできないので、スレッド間でどこかで同期してその変数を更新する必要がある
ちなみに「アトミック」とは、
にあるように、
システムの他の部分から見て、操作の組合せが一度に成功したか失敗したように見える。途中の状態にアクセスすることはできない。
とのこと
そもそも言語によっては未定義動作というものがあり、ある状態になるとそもそもシステム全体においてどういう結果が出るかがわからないという状況が発生する。
例えばC++とかだと、
未定義動作は何が起こるかわからないので実装者としてはそういったバグが入らないようにするべき、だが、そもそもなんで未定義動作が存在するのか?
一般的な最適化の話ですが、コンパイラはソースコードの内容をすべて書かれたとおりに正しく実行するプログラムを出力しなければならないわけではありません。外から見た振る舞い(observable behavior)さえ合っていればいいという発想です。これにより、値が定数だったらコンパイル時に先に計算してしまったり、出力に影響しない処理があったら削ってしまったりしても構わないわけです。
大まかに書くと、volatile変数へのアクセス(7.1)、プログラム終了時のファイルへの書き込み(7.2)、対話型デバイスの入力と出力の順序(7.3)の3種類がコンパイラが満たさなければならないobservable behaviorとして定義されています。(標準出力などもファイルの一種です。)
ここで、(7.2)ではプログラム終了時のと書かれていることがポイントです。つまり終了しないプログラムは対象から外れているのです。
なぜ未定義動作があるかに関しては、最適化とのトレードオフということかぁ。
つまり、C++では未定義動作を許容?そこの部分は起きないように実装者に委ねる上で、最適化する方をとったということかな
この中でC, C++は未定義動作があるが、一方でJavaなどは未定義動作などはない。
これは、
Goのコア開発者である、Russ CoxのMemory Modelsで、happens-before
がよく出てくる。
happens-before
ってなんだろ?
リオーダとはプログラムのステートメントや命令を入れ替えることです。
なるほど、プログラム実行に影響がない範囲でリオーダが起こるのか。
問題はデータ競合があるときにこのリオーダが起こるとよくわからん動作をする可能性があるってことか。
リオーダは最適化のために重要でもある。
リオーダとデータ競合の関係において重要なのは「同期されておらず」という部分です。データ競合がある→共有変数のアクセスが同期されていない→リオーダ可能,ということになるからです。
なるほど〜、ロックやアトミックはリオーダーを防いで同期的な機構が保たれるようにするってことか。
happens-before 関係は 2 つの操作のどちらが 先に起きたか を表します。英語の "A happens before B" を日本語にすると「A は B より前に起きる」となります。先ほど説明した acquire/release は,1 つの共有変数に関する読み書き操作に happens-before 関係を付けるためのものです。
happens-before 関係は,先に起きた操作の影響が後の操作から見える,可視である,ということを保証するものです。注意したいのは,A happens before B は「A は B の前に必ず起きる」と言っているわけではないということです。
一見1つの値を更新しているように見えてそうではないものもある。
マルチワードの値とかがその例。
にあるように、interface
は内部的に値の方へのポインタと実際の値へのポインタの2つを持っている。
つまり、このような値に対してデータ競合が発生すると、可能性としては1つのポインタは更新されたが、もう1つが古いままということが起こりうり、おかしな状態が発生する可能性がある。
これは、Russ CoxのMemory Modelsでは、以下のように書かれている。
Note that this means that races on multiword data structures can lead to inconsistent values not corresponding to a single write. When the values depend on the consistency of internal (pointer, length) or (pointer, type) pairs, as is the case for interface values, maps, slices, and strings in most Go implementations, such races can in turn lead to arbitrary memory corruption.
Sliceなども内部的には、arryへのポインタとlen, capの3つのフィールドを持っているため、データ競合によってある一部のフィールドだけ書き換えが成功し、一部は失敗した、というケースがありうるということかな。
↑の内容で間違いなどありましたら、ぜひ訂正などお願いします m(_ _)m
Stringでもそういったことが起こりうるらしい
なので、チャネルやロックを使わずに複数のgoroutineから共有変数を読み書きするのは、プリミティブな値ぽいやつでも危険