IBMサンプルコード、非再入可能関数がシステムで機能しない


11

プログラミングの再入学を勉強していました。IBMのこのサイト(本当に良いサイト)。以下にコピーしたコードを見つけました。これは、ウェブサイトに登場する最初のコードです。

コードは、「危険なコンテキスト」で絶えず変化する2つの値を出力することにより、テキストプログラムの非線形開発(非同期)で変数への共有アクセスに関する問題を示します。

#include <signal.h>
#include <stdio.h>

struct two_int { int a, b; } data;

void signal_handler(int signum){
   printf ("%d, %d\n", data.a, data.b);
   alarm (1);
}

int main (void){
   static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };

   signal (SIGALRM, signal_handler); 
   data = zeros;
   alarm (1);
   while (1){
       data = zeros;
       data = ones;
   }
}

コードを実行しようとしたときに問題が発生しました(または、それ以上は表示されませんでした)。デフォルト設定でgccバージョン6.3.0 20170516(Debian 6.3.0-18 + deb9u1)を使用していました。誤った出力は発生しません。「間違った」ペア値を取得する頻度は0です。

結局何が起こっているのですか?静的グローバル変数を使用した再入に問題がないのはなぜですか?


1
すべてのコンパイラの最適化が無効になっていることを確認して、再試行してください
roaima

私はそれを想定していました...しかし、どのオプションを変更しますか?何も思いつきません。:-(
ダニエルバンデイラ

5
これはプログラミングの質問のようです(スタックオーバーフロー)。ここにはうまく配置されていないようです。(申し訳ありませんが、私はサブサイトが少なかったので、それは切り取られています。しかし、それはそうです)
ctrl-alt-delor

1
最も単純な再入可能コードは不変です。
ctrl-alt-delor

最初は、問題はgccとLinux環境に関連していると思います。たとえば、OSのスケジューリング(ハンドラールーチンを呼び出す前に、割り込み後のシグナルより多くのプログラムテキストを実行する)の進化など。
ダニエルバンデイラ

回答:


12

それは本当に再入可能ではありません。同じスレッド(または異なるスレッド)で関数を2回実行していません。これは、再帰によって取得することも、現在の関数のアドレスをコールバック関数ポインタの引数として別の関数に渡すことで取得できます。(そしてそれは同期するのでそれは危険ではありません)。

これは、シグナルハンドラーとメインスレッドの間の単純なデータレースUB(Undefined Behaviour)にすぎませsig_atomic_tこれに対して安全であることが保証されているだけです。8バイトのオブジェクトをx86-64の1つの命令でロードまたは保存でき、コンパイラーがたまたまそのasmを選択する場合のように、他のものが動作する場合があります。(@icarusの答えが示すように)。

MCUプログラミング-C ++ O2最適化がwhile whileループを参照してください-シングルコアマイクロコントローラーの割り込みハンドラーは、基本的にシングルスレッドプログラムのシグナルハンドラーと同じです。その場合、UBの結果として、ループから負荷が引き上げられます。

データレースUBが原因で実際​​にティアリングのテストケースが発生したのは、おそらく32ビットモードで開発またはテストされたか、構造体メンバーを個別にロードした古いdumberコンパイラを使用したためです。

あなたの場合、UBフリープログラムがストアを監視できないため、コンパイラはストアを無限ループから最適化できます。 datais _Atomicまたはvolatileでなく、ループ内に他の副作用はありません。したがって、リーダーがこのライターと同期する方法はありません。これは、最適化を有効にしてコンパイルした場合に実際に起こります(Godboltはメインの下部に空のループを表示します)。構造体も2に変更し、long longgccはmovdqaループの前に単一の16バイトストアを使用します。(これはアトミックな保証はありませが、整列されていると仮定すると、ほとんどすべてのCPUで実際に行われます。またはIntelではキャッシュラインの境界を越えないだけです 。x86で自然に整列された変数アトミックに整数が割り当てられるのはなぜですか?

したがって、最適化を有効にしてコンパイルすると、テストが失敗し、毎回同じ値が表示されます。Cは移植可能なアセンブリ言語ではありません。

volatile struct two_intまた、コンパイラーにそれらを最適化しないよう強制しますが、構造体全体をアトミックにロード/ストアするように強制しません。(ただし、そうすることを妨げるものでvolatileはありませ。)データレースUBを回避するわけではありませが、実際には、スレッド間通信には十分であり、(インラインasmと共に)手作業のアトミックを構築する方法でした。 C11 / C ++ 11より前、通常のCPUアーキテクチャー。彼らはしているキャッシュコヒーレントはそうvolatileであるとほとんど同様の練習に_Atomicmemory_order_relaxed純粋な負荷と純粋な店舗用、タイプのために使用されている場合、あなたが裂けを得ることはありませんので、コンパイラは、単一の命令を使用することを十分に絞り込みます。そしてもちろんvolatileISO C規格_Atomicとmo_relaxed を使用して同じasmにコンパイルするコードを作成することの保証はありません。


あなたがした機能があった場合global_var++;int、またはlong longあなたがメインから実行すること非同期シグナルハンドラからの、それは、データ・レースUBを作成するために使用リエントラントへの道だろう。

コンパイル方法に応じて(メモリの宛先incまたはadd、またはload / inc / storeを分離するため)、同じスレッド内のシグナルハンドラーに関してアトミックであるかどうかが決まります。「int num」のnum ++をアトミックにすることできますか?x86およびC ++での原子性についての詳細。(C11のstdatomic.hand _Atomic属性は、C ++ 11のstd::atomic<T>テンプレートと同等の機能を提供します)

命令の途中で割り込みやその他の例外が発生することはないため、メモリ宛先の追加はアトミックです。シングルコアCPUのコンテキストスイッチ。(キャッシュコヒーレントな)DMAライターのみが、シングルコアCPUでのプレフィックスadd [mem], 1なしのからのインクリメントを「踏む」ことができlockます。別のスレッドが実行されている可能性のある他のコアはありません。

したがって、シグナルの場合と同様です。シグナルを処理するスレッドの通常の実行の代わりにシグナルハンドラが実行されるため、1つの命令の途中で処理することはできません。


2
イカルの答えは私にとって十分であるにもかかわらず、私はあなたの答えを最良の答えとして受け入れることを余儀なくされました。あなたが私たちに言った明確な概念は、私にこの日中(そしてさらに)勉強するためのトピックのバケットを与えます。実際、最初の2つの段落で書いた内容は、一見してほとんどわかりません。ありがとうございました!コンピュータとプログラミングに関するインターネット上の記事を公開している場合は、リンクをお知らせください。
ダニエルバンデイラ

17

見るとgodbolt(行方不明に追加した後、コンパイラエクスプローラ#include <unistd.h>)、1は、ほぼすべてのx86_64版コンパイラのコード生成の用途QWORDが移動するロードするためにことを見ているonesと、zeros1つの命令で。

        mov     rax, QWORD PTR main::ones[rip]
        mov     QWORD PTR data[rip], rax

IBMのサイトはOn most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.、2005年の典型的なCPUにはどちらが当てはまったかを述べていますが、コードが示すように現在は当てはまりません。2つのintではなく2つのlongを持つように構造体を変更すると、問題が発生します。

私は以前これが怠惰だった「原子」であると書いた。プログラムは単一のCPUでのみ実行されています。このCPUの観点から、各命令は完了します(dmaなどのメモリを変更するものが他にない場合)。

したがって、このCレベルでは、コンパイラーが構造体を書き込むために単一の命令を選択することは定義されていないため、IBMペーパーで言及されている破損が発生する可能性があります。現在のCPUをターゲットとする最新のコンパイラは、単一の命令を使用します。単一の命令は、単一のスレッド化されたプログラムの破損を回避するのに十分です。


3
データ型をからintに変更してlong long、32ビットにコンパイルしてみてください。教訓は、それが壊れるかどうか、いつ壊れるかは決して分からないということです。
ctrl-alt-delor

2
つまり、私のマシンでは、この2つの値の割り当てはアトミック操作ですか?(x86_64アーキテクチャのコンパイルを検討)
Daniel Bandeira

1
long longそれでもx86-64の1つの命令である16バイトにコンパイルされますmovdqa。Godboltリンクのように、最適化を無効にしない限り。(GCCのデフォルトは-O0デバッグモードで、ストア/リロードノイズが多く、通常は見るのは面白くありません。)
Peter Cordes

すべてのコメントを読んだ後、タイプを「long long」に変更しました。結果は興味深いものでした。待機した結果が達成され、いくつかのカウンターを設定すると、不一致データの割合が残りのコードによってどのように影響を受けるかなど、他の概念を改善することができました。助けてくれてありがとう!
Daniel Bandeira
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.