私がレビューしたコードベースで、次のイディオムを見つけました。
void notify(struct actor_t act) {
write(act.pipe, "M", 1);
}
// thread A sending data to thread B
void send(byte *data) {
global.data = data;
notify(threadB);
}
// in thread B event loop
read(this.sock, &cmd, 1);
switch (cmd) {
case 'M': use_data(global.data);break;
...
}
「それを持ってください」と私は私のチームのシニアメンバーである著者に言った、「ここにはメモリバリアはありません!あなたはそれを保証しませんglobal.data
はキャッシュからメインメモリにフラッシュされます。スレッドAとスレッドBが2つの異なるプロセッサで実行される場合、このスキームは失敗する可能性があります。
上級プログラマーはニヤリと笑い、靴ひもを結ぶ方法を説明するかのようにゆっくりと説明しました。「若い男の子を聞いてください。ここでは、高負荷テストや実際のクライアントで、スレッド関連のバグがたくさん見られます」彼の長いあごひげを引っ掻くために一時停止しました、「しかし、私たちはこのイディオムにバグがあったことはありません」。
「しかし、それは本の中で述べています...」
「静かに!」と彼はすぐに私を黙らせました。「理論的には保証されないかもしれませんが、実際には、関数呼び出しを使用したという事実は事実上メモリバリアです。コンパイラは命令を並べ替えませんglobal.data = data
、関数呼び出しで誰かがそれを使用しているかどうかを知ることができないため、x86アーキテクチャは、スレッドBがパイプからコマンドを読み取るまでに、他のCPUがこのグローバルデータを確実に認識できるようにします。心配すべき現実世界の問題はたくさんありますので、ご安心ください。偽の理論上の問題に余分な労力を費やす必要はありません。
「私の少年は安心してください。やがて、博士号を取得する必要のある非問題から実際の問題を分離することが理解できるでしょう。」
彼は正しいですか?それは実際には実際には問題ではありませんか(x86、x64、ARMなど)?
それは私が学んだすべてに反します、しかし彼は長いあごひげと本当にスマートな外見を持っています!
彼が間違っていることを証明するコードを見せてくれれば、追加のポイントがあります!
メモリバリアは、命令の並べ替えを防ぐためだけのものではありません。命令の順序を変更しなくても、キャッシュコヒーレンスに問題が発生する可能性があります。並べ替えについては、コンパイラと設定によって異なります。 ICCは、特に並べ替えに積極的です。プログラム全体の最適化を伴うMSVCも可能です。
共有データ変数がvolatile
として宣言されている場合、仕様に含まれていなくてもほとんどのコンパイラは、読み取りと読み取りの前後にメモリ変数を生成します。変数から書き込み、並べ替えを防ぎます。 これは、volatile
の正しい使用方法でも、その意味でもありません。
(投票が残っている場合は、ナレーションの質問を+1します。)
実際には、関数呼び出しはcompilerバリアです。これは、コンパイラがグローバルメモリアクセスを呼び出しを超えて移動しないことを意味します。これに対する警告は、コンパイラが何かを知っている関数です。組み込み関数、インライン関数(IPOに注意してください!)など。
したがって、これを機能させるには、理論的にはプロセッサのメモリバリア(コンパイラのバリアに加えて)が必要です。ただし、グローバル状態を変更するシステムコールである読み取りと書き込みを呼び出しているので、カーネルはそれらの実装のどこかでメモリバリアを発行すると確信しています。ただし、そのような保証はないため、理論的には障壁が必要です。
基本的なルールは次のとおりです。コンパイラは、グローバル状態appearをコーディングしたとおりにする必要がありますが、特定の関数がグローバル変数を使用していないことを証明できる場合次に、選択した方法でアルゴリズムを実装できます。
結果として、従来のコンパイラは、関数の内部を見ることができなかったため、常に関数別のコンパイルユニットをメモリバリアとして扱いました。最近のコンパイラは、これらの障壁を打ち破る「プログラム全体」または「リンク時間」最適化戦略をますます成長させており、willは、何年も正常に機能しているにもかかわらず、不十分に記述されたコードを失敗させます。
問題の関数が共有ライブラリにある場合、その内部を見ることができません。but関数がC標準で定義されているものである場合、その必要はありません- -関数が何をするかはすでにわかっているので、それらにも注意する必要があります。コンパイラはnotカーネル呼び出しを認識しますが、コンパイラが認識できないもの(インラインアセンブラ、または関数)を挿入する行為そのものであることに注意してください。アセンブラファイルの呼び出し)は、それ自体にメモリバリアを作成します。
あなたの場合、notify
はコンパイラが内部を見ることができないブラックボックス(ライブラリ関数)であるか、認識可能なメモリバリアを含んでいるので、おそらく安全です。
実際には、これに失敗するにはvery悪いコードを書く必要があります。
実際には、彼は正しく、この特定のケースではメモリバリアが暗示されています。
しかし、要点は、その存在が「議論の余地がある」場合、コードはすでに複雑すぎて不明確であるということです。
本当にみんな、ミューテックスまたは他の適切な構造を使用してください。これは、スレッドを処理し、保守可能なコードを作成するための唯一の安全な方法です。
また、send()が複数回呼び出された場合、コードが予測できないなど、他のエラーが発生する可能性があります。