4

私は C++11 アトミック プリミティブを試して、ある種のアトミックな「スレッド カウンター」を実装しています。基本的に、コードのクリティカル セクションは 1 つだけです。このコード ブロック内では、任意のスレッドがメモリから自由に読み取ることができます。ただし、すべての共有メモリをデフォルトの初期値にリセットするリセットまたはクリア操作を実行したい場合があります。

これは、読み取り/書き込みロックを使用する絶好の機会のようです。C++11 には、すぐに使用できる読み取り/書き込みミューテックスは含まれていませんが、もっと単純なものがあればよいでしょう。この問題は、C++11 のアトミック プリミティブに慣れる絶好の機会になると思いました。

それで、私はこの問題をしばらく考えました。私がしなければならないことは、

  1. スレッドがクリティカル セクションに入るたびに、アトミック カウンター変数をインクリメントします

  2. スレッドがクリティカル セクションを離れるたびに、アトミック カウンター変数をデクリメントします。

  3. スレッドがすべての変数をデフォルト値にリセットしたい場合、カウンターが 0 になるのをアトミックに待機し、アトミックに特別な「クリア フラグ」値に設定し、クリアを実行してから、カウンターを 0 にリセットする必要があります。

  4. もちろん、カウンターをインクリメントおよびデクリメントしたいスレッドは、クリアフラグもチェックする必要があります。

したがって、今説明したアルゴリズムは 3 つの関数で実装できます。最初の関数はincrement_thread_counter()、クリティカル セクションに入る前に必ず呼び出す必要があります。2 番目の関数 はdecrement_thread_counter()、必ずクリティカル セクションを出る直前に呼び出す必要があります。最後に、この関数は、スレッド カウンター == 0 の場合にのみclear()、クリティカル セクションの外から呼び出すことができます。

これは私が思いついたものです:

与えられた:

  1. スレッドカウンター変数、std::atomic<std::size_t> thread_counter
  2. clearing_flagに設定された定数std::numeric_limits<std::size_t>::max()

...

void increment_thread_counter()
{
    std::size_t expected = 0;
    while (!std::atomic_compare_exchange_strong(&thread_counter, &expected, 1))
    {
        if (expected != clearing_flag)
        {
            thread_counter.fetch_add(1);
            break;
        }
        expected = 0;
    }
}

void decrement_thread_counter()
{
    thread_counter.fetch_sub(1);
}

void clear()
{
    std::size_t expected = 0;
    while (!thread_counter.compare_exchange_strong(expected, clearing_flag)) expected = 0;

    /* PERFORM WRITES WHICH WRITE TO ALL SHARED VARIABLES */

    thread_counter.store(0);
}

私が推論できる限り、これはスレッドセーフであるべきです。この関数は、常に前に呼び出されるdecrement_thread_counterものであるため、同期ロジックを必要としないことに注意してください。したがって、 に到達すると、thread_counter が 0 または になることはありません。increment()decrement()decrement()clearing_flag

いずれにせよ、THREADING IS HARD™ はロックレス アルゴリズムの専門家ではないため、このアルゴリズムが競合状態にないかどうかは完全にはわかりません。

質問: このコードはスレッドセーフですか? ここで競合状態が発生する可能性はありますか?

4

1 に答える 1

6

競合状態があります。increment_thread_counter()別のスレッドがの test forclearing_flagとの間でカウンターを変更すると、悪いことが起こりますfetch_add

この古典的な CAS ループの方がうまくいくと思います。

void increment_thread_counter()
{
    std::size_t expected = 0;
    std::size_t updated;
    do {
        if (expected == clearing_flag) {     // don't want to succeed while clearing, 
             expected = 0;      //take a chance that clearing completes before CMPEXC
        }

        updated = expected + 1;
        // if (updated == clearing_flag) TOO MANY READERS!
    } while (!std::atomic_compare_exchange_weak(&thread_counter, &expected, updated));
}
于 2014-08-07T17:53:32.607 に答える