web-dev-qa-db-ja.com

別のスレッドが(最大1回)設定する可能性がある場合、ロックせずに共有ブールフラグを読み取っても大丈夫ですか?

スレッドをより適切にシャットダウンしたいので、単純な信号メカニズムを実装しようとしています。完全にイベント駆動型のスレッドが必要だとは思わないので、クリティカルセクションMonitor(C#lockと同等)を使用してそれを正常に停止するメソッドを持つワーカーがいます。

DrawingThread.h

class DrawingThread {
    bool stopRequested;
    Runtime::Monitor CSMonitor;
    CPInfo *pPInfo;
    //More..
}

DrawingThread.cpp

void DrawingThread::Run() {
    if (!stopRequested)
        //Time consuming call#1
    if (!stopRequested) {
        CSMonitor.Enter();
        pPInfo = new CPInfo(/**/);
        //Not time consuming but pPInfo must either be null or constructed. 
        CSMonitor.Exit();
    }
    if (!stopRequested) {
        pPInfo->foobar(/**/);//Time consuming and can be signalled
    }
    if (!stopRequested) {
        //One more optional but time consuming call.
    }
}


void DrawingThread::RequestStop() {
    CSMonitor.Enter();
    stopRequested = true;
    if (pPInfo) pPInfo->RequestStop();
    CSMonitor.Exit();
}

私は(少なくともWindowsでは)Monitor/locksが最も安価なスレッド同期プリミティブであることを理解していますが、過度の使用を避けたいと思っています。このブール値フラグの各読み取りをラップする必要がありますか?これはfalseに初期化され、停止が要求されたときに(タスクが完了する前に要求された場合)1回だけtrueに設定されます。

読み書きはアトミックではない可能性があるため、私の家庭教師はboolさえも保護することを勧めました。このワンショットフラグは、ルールを証明する例外だと思いますか?

42
John

同期せずに異なるスレッドで変更された可能性のあるものを読み取ることは決して大丈夫です。どのレベルの同期が必要かは、実際に読んでいるものによって異なります。プリミティブ型については、アトミック読み取りを見てください。 std::atomic<bool>の形式。

同期が常に必要な理由は、プロセッサがキャッシュラインでデータを共有する可能性があるためです。同期がない場合、この値を別のスレッドで変更された可能性のある値に更新する理由はありません。さらに悪いことに、同期がない場合、値の近くに保存されている何かが変更されて同期されると、間違った値を書き込む可能性があります。

44
Dietmar Kühl

ブール割り当てはアトミックです。それは問題ではありません。

問題は、コンパイラーまたはCPU命令の並べ替えまたはデータキャッシングのいずれかが原因で、スレッドが別のスレッドによって行われた変数の変更を認識できないことです(つまり、ブールフラグを読み取るスレッドは、実際の値)。

解決策はメモリフェンスであり、これは実際にロックステートメントによって暗黙的に追加されますが、単一の変数に対しては過剰です。 std::atomic<bool>として宣言するだけです。

12
Tudor

答えは、「それは依存する」だと思います。 C++ 03を使用している場合、標準ではスレッド化が定義されていないため、コンパイラとスレッドライブラリの内容を読む必要がありますが、この種のことは通常「良性競合」と呼ばれます。 そして通常はOK です。

C++ 11を使用している場合、良性の競合は未定義の動作です。未定義の動作が基礎となるデータ型に対して意味をなさない場合でも。問題は、コンパイラがプログラムに未定義の動作がないと仮定できることです およびそれに基づいて最適化を行います (そこからリンクされているパート1とパート2も参照)。たとえば、コンパイラは、フラグを1回読み取って値をキャッシュすることを決定できます。これは、何らかのミューテックスやメモリバリアなしで別のスレッドの変数に書き込むのが未定義の動作だからです。

もちろん、yourコンパイラーはその最適化を行わないと約束している可能性があります。あなたが見る必要があります。

最も簡単な解決策は、std::atomic<bool>はC++ 11で、または Hans Boehmのatomic_ops のようなものです。

6
Max Lybbert

いいえ、最新のコンパイラとcpusはマルチスレッドタスクを考慮せずにコードを並べ替えるため、すべてのアクセスを保護する必要があります。異なるスレッドからの読み取りアクセスは機能する可能性がありますが、機能する必要はありません。

1
Jörg Beyer