10

私はC++03にこの形をとるいくつかのコードに出くわしました:

struct Foo {
    int a;
    int b;
    CRITICAL_SECTION cs;
}

// DoFoo::Foo foo_;

void DoFoo::Foolish()
{
    if( foo_.a == 4 )
    {
        PerformSomeTask();

        EnterCriticalSection(&foo_.cs);
        foo_.b = 7;
        LeaveCriticalSection(&foo_.cs);
    }
}

読み取り元をfoo_.a保護する必要がありますか?例えば:

void DoFoo::Foolish()
{
    EnterCriticalSection(&foo_.cs);
    int a = foo_.a;
    LeaveCriticalSection(&foo_.cs);

    if( a == 4 )
    {
        PerformSomeTask();

        EnterCriticalSection(&foo_.cs);
        foo_.b = 7;
        LeaveCriticalSection(&foo_.cs);
    }
}

もしそうなら、なぜですか?

整数は32ビットで整列されていると想定してください。プラットフォームはARMです。

4

5 に答える 5

11

技術的には可能ですが、多くのプラットフォームでは不可能です。まず、これが 32 ビットであると仮定しintます (これはかなり一般的ですが、ほとんど普遍的ではありません)。

32 ビットの 2 つのワード (16 ビット部分) がint別々に読み書きされる可能性があります。一部のシステムでは、が適切に配置されていない場合、それらは別々に読み取られintます。

32 ビットで整列された 32 ビットの読み取りと書き込み (および 16 ビットで整列された 16 ビットの読み取りと書き込み) しか実行できないシステムと、intそのような境界をまたぐシステムを想像してみてください。最初はintはゼロです (つまり、0x00000000)

1 つのスレッドが に書き込み0xBAADF00D、もう 1 つのスレッドがintそれを「同時に」読み取ります。

書き込みスレッドは、最初0xBAADに の上位ワードに書き込みますint。次に、リーダー スレッドは全体int(ハイとローの両方) の取得を読み取ります。これは、意図的に設定され0xBAAD0000た状態ではありません。int

次に、書き込みスレッドが下位ワードを書き込みます0xF00D

前述のように、一部のプラットフォームではすべての 32 ビットの読み取り/書き込みがアトミックであるため、これは問題ではありません。ただし、他にも懸念事項があります。

ほとんどのロック/ロック解除コードには、ロック全体での並べ替えを防ぐためのコンパイラへの命令が含まれています。並べ替えの防止がなければ、コンパイラは、単一のスレッド化されたコンテキストで「あたかも」動作する限り、自由に並べ替えを行うことができます。そのため、コードで読み取る場合abコンパイラは、その間隔で変更されるスレッド内の機会が見られない限り、読み取るb前に読み取ることができます。ab

したがって、おそらく、あなたが読んでいるコードはこれらのロックを使用して、変数の読み取りがコードに書かれた順序で行われるようにしています。

以下のコメントで他の問題が提起されていますが、私はそれらに対処する能力がないと感じています: キャッシュの問題と可視性.

于 2012-11-30T18:19:46.337 に答える
3

これを見ると、arm のメモリ モデルは非常に緩和されているように見えるため、あるスレッドでの書き込みが別のスレッドで期待されるときに確実に表示されるようにするには、メモリ バリアの形式が必要です。したがって、あなたがしていること、または std::atomic を使用していることが、プラットフォームで必要になる可能性があります。これを考慮に入れないと、別のスレッドで順不同の更新が表示され、例が壊れる可能性があります。

于 2012-11-30T18:33:46.850 に答える
2

(たとえば)を使用して、C++ 11を使用して整数読み取りがアトミックであることを確認できると思いますstd::atomic<int>

于 2012-11-30T18:20:20.440 に答える
2

C++ 標準では、1 つのスレッドがその変数から読み取ると同時に別のスレッドが変数に書き込む場合、または 2 つのスレッドが同じ変数に同時に書き込む場合、データ競合が発生すると述べています。さらに、データ競合が未定義の動作を生成すると述べています。したがって、正式には、これらの読み取りと書き込みを同期する必要があります。

あるスレッドが別のスレッドによって書き込まれたデータを読み取る場合、3 つの個別の問題があります。まず、ティアリングがあります。書き込みに複数のバス サイクルが必要な場合、操作の途中でスレッドの切り替えが発生する可能性があり、別のスレッドが半分書き込まれた値を見る可能性があります。読み取りに複数のバスサイクルが必要な場合、同様の問題があります。次に、可視性があります。各プロセッサには、最近処理したデータの独自のローカル コピーがあり、あるプロセッサのキャッシュに書き込んでも、別のプロセッサのキャッシュが更新されるとは限りません。第 3 に、シングル スレッド内では問題ない方法で読み取りと書き込みを並べ替えるコンパイラの最適化がありますが、マルチスレッド コードは壊れます。スレッドセーフ コードは3 つすべてを処理する必要があります問題。これは、同期プリミティブ (ミューテックス、条件変数、およびアトミック) の仕事です。

于 2012-11-30T19:45:32.877 に答える
0

整数の読み取り/書き込み操作は実際にアトミックである可能性が高いですが、適切に実行しないと、コンパイラの最適化とプロセッサキャッシュで問題が発生します。

説明すると、コンパイラは通常、コードがシングルスレッドであると想定し、それに依存する多くの最適化を行います。たとえば、命令の順序を変更する場合があります。または、変数が書き込まれるだけで読み取られないことがわかった場合は、変数を完全に最適化する可能性があります。

CPUもその整数をキャッシュするので、一方のスレッドがそれを書き込んだ場合、もう一方のスレッドはかなり後になるまでそれを見ることができない可能性があります。

できることは2つあります。1つは、元のコードのようにクリティカルセクションにラップインすることです。もう1つは、変数を。としてマークすることですvolatile。これは、この変数が複数のスレッドによってアクセスされ、さまざまな最適化を無効にするだけでなく、変数へのアクセスの周りに特別なキャッシュ同期命令(別名「メモリバリア」)を配置することをコンパイラに通知します(または私は理解しています)。どうやらこれは間違っています。

追加:また、別の回答で指摘されているように、Windowsには、非変数Interlockedのこれらの問題を回避するために使用できるAPIがあります。volatile

于 2012-11-30T18:27:50.287 に答える