web-dev-qa-db-ja.com

スレッド間で共有変数を変更するコードが競合状態の影響を受けないのはなぜですか?

Cygwin GCCを使用して、次のコードを実行します。

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

unsigned u = 0;

void foo()
{
    u++;
}

int main()
{
    vector<thread> threads;
    for(int i = 0; i < 1000; i++) {
        threads.Push_back (thread (foo));
    }
    for (auto& t : threads) t.join();

    cout << u << endl;
    return 0;
}

次の行でコンパイルされます:g++ -Wall -fexceptions -g -std=c++14 -c main.cpp -o main.o

1000を出力しますが、これは正しいです。ただし、スレッドは以前にインクリメントされた値を上書きするため、より少ない数を期待していました。このコードが相互アクセスの影響を受けないのはなぜですか?

私のテストマシンには4つのコアがあり、知っているプログラムに制限はありません。

共有fooのコンテンツをより複雑なもの、たとえば.

if (u % 3 == 0) {
    u += 4;
} else {
    u -= 1;
}
107
mafu

foo()は非常に短いため、各スレッドはおそらく次のスレッドが生成される前に終了します。 u++の前のfoo()にランダムな時間のスリープを追加すると、期待したものが表示される可能性があります。

268
Rob K

競合状態はコードが正しく実行されることを保証するものではなく、単に未定義の動作であるため、何でもできるということを理解することが重要です。期待どおりに実行を含む。

特にX86およびAMD64マシンでは、多くの命令がアトミックであり、一貫性の保証が非常に高いため、競合状態によって問題が発生することはまれです。これらの保証は、多くの命令がアトミックであるためにロックプレフィックスが必要なマルチプロセッサシステムでは多少低下します。

マシン上でインクリメントがアトミックopである場合、言語標準に従って未定義の動作であっても、これはおそらく正しく実行されます。

具体的には、この場合、コードはアトミック フェッチおよび追加 命令(X86アセンブリではADDまたはXADD)にコンパイルされると予想されますが、これはシングルプロセッサシステムでは実際にアトミックですが、マルチプロセッサシステムではそうではありませんアトミックであることが保証され、そのためにはロックが必要になります。マルチプロセッサシステムで実行している場合、スレッドが干渉して誤った結果を生成する可能性のあるウィンドウがあります。

具体的には、 https://godbolt.org/ を使用してアセンブリにコードをコンパイルし、foo()を次のようにコンパイルします。

foo():
        add     DWORD PTR u[rip], 1
        ret

これは、シングルプロセッサではアトミックになるadd命令のみを実行していることを意味します(上記で述べたように、マルチプロセッサシステムではそうではありません)。

59
Vality

u++の前後に睡眠をとるのはそれほど重要ではないと思います。むしろ、操作u++は、fooを呼び出す生成スレッドのオーバーヘッドと比較して、インターセプトされる可能性が非常に低いコードに変換されます。ただし、操作u++を「延長」すると、競合状態が発生する可能性が高くなります。

void foo()
{
    unsigned i = u;
    for (int s=0;s<10000;s++);
    u = i+1;
}

結果:694


ところで:私も試しました

if (u % 2) {
    u += 2;
} else {
    u -= 1;
}

ほとんどの場合1997でしたが、時々1995でした。

20
Stephan Lechner

競合状態になります。 foou++;の前にusleep(1000);を配置すると、毎回異なる出力(<1000)が表示されます。

7
juf
  1. doesが存在するにもかかわらず、競合状態があなたに現れなかった理由に対するおそらく答えは、スレッドを開始するのにかかる時間と比較して、foo()は非常に速いということです。各スレッドは、次のスレッドが開始する前に終了します。しかし...

  2. 元のバージョンでも、結果はシステムによって異なります。(クアッドコア)Macbookで試してみましたが、10回実行すると、1000回3回、999回6回、998回を獲得しました。そのため、レースはややまれですが、明らかに存在します。

  3. '-g'でコンパイルしましたが、これにはバグをなくす方法があります。コードを再コンパイルしましたが、'-g'を使用せずに変更せずに、レースをより顕著にしました。1000回、999回を3回、998回を2回、997回を997回、996回を996回、992回を取得しました。

  4. Re。スリープを追加することの提案-これは役立ちますが、(a)固定のスリープ時間はスレッドを開始時間(スレッド解決の対象)によって歪めたままにし、(b)希望するときにランダムなスリープがそれらを広げますそれらを近づけます。代わりに、開始信号を待つようにコーディングするので、すべてを作成してから作業を開始できます。このバージョン('-g'の有無にかかわらず)を使用すると、最低でも974、最低でも998の結果が得られます。

    #include <iostream>
    #include <thread>
    #include <vector>
    using namespace std;
    
    unsigned u = 0;
    bool start = false;
    
    void foo()
    {
        while (!start) {
            std::this_thread::yield();
        }
        u++;
    }
    
    int main()
    {
        vector<thread> threads;
        for(int i = 0; i < 1000; i++) {
            threads.Push_back (thread (foo));
        }
        start = true;
        for (auto& t : threads) t.join();
    
        cout << u << endl;
        return 0;
    }
    
6
dgould