web-dev-qa-db-ja.com

C ++ 11でのstd :: atomic :: compare_exchange_weak()の理解

_bool compare_exchange_weak (T& expected, T val, ..);
_

compare_exchange_weak()は、C++ 11で提供される比較交換プリミティブの1つです。 weakは、オブジェクトの値がexpectedと等しい場合でもfalseを返すという意味です。 。これは、spurious failureによるものです。一部のプラットフォームでは、命令シーケンス(x86のように1つではなく)が使用されます。それを実装します。このようなプラットフォームでは、コンテキストの切り替え、別のスレッドによる同じアドレス(またはキャッシュライン)の再読み込みなどにより、プリミティブが失敗する可能性があります。操作に失敗するのはオブジェクトの値(spuriousと等しくない)ではないため、expectedです。代わりに、それは一種のタイミングの問題です。

しかし、私を困惑させるのは、C++ 11標準(ISO/IEC 14882)で言われていることです。

29.6.5 ..スプリアス障害の結果は、ほとんどすべての弱い比較と交換の使用がループになることです。

ほぼすべての用途でループする必要があるのはなぜですか?それは、偽の障害のために失敗したときにループすることを意味しますか?その場合、なぜcompare_exchange_weak()をわざわざ使用して自分でループを記述するのですか?私たちはcompare_exchange_strong()を使用することができますが、これは私たちにとって偽の失敗を取り除くべきだと思います。 compare_exchange_weak()の一般的な使用例は何ですか?

関連する別の質問。 Anthony氏の著書「C++ Concurrency In Action」では、

_//Because compare_exchange_weak() can fail spuriously, it must typically
//be used in a loop:

bool expected=false;
extern atomic<bool> b; // set somewhere else
while(!b.compare_exchange_weak(expected,true) && !expected);

//In this case, you keep looping as long as expected is still false,
//indicating that the compare_exchange_weak() call failed spuriously.
_

ループ状態に_!expected_があるのはなぜですか?しばらくの間、すべてのスレッドが枯渇して進行しないことを防ぐためにありますか?

編集:(最後の1つの質問)

単一のハードウェアCAS命令が存在しないプラットフォームでは、弱いバージョンと強いバージョンの両方がLL/SC(ARM、PowerPCなど)を使用して実装されます。それでは、次の2つのループに違いはありますか?なぜですか? (私にとっては、同様のパフォーマンスが必要です。)

_// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_weak(..))
{ .. }

// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_strong(..)) 
{ .. }
_

ループ内でパフォーマンスの違いがあるかもしれないと皆さんが言及しているこの最後の質問で私は出てきます。また、C++ 11標準(ISO/IEC 14882)でも言及されています。

比較と交換がループ内にある場合、一部のプラットフォームではウィークバージョンの方がパフォーマンスが向上します。

しかし、上記で分析したように、ループ内の2つのバージョンは同じ/同様のパフォーマンスを提供するはずです。私が恋しいものは何ですか?

76
Eric Z

さまざまなオンラインリソース(たとえば、 this one および this one )、C++ 11 Standard、およびここに答えがあります。

関連する質問はマージされます(例:「why!expected?」は "compare_exchange_weak()をループに入れる理由?")とそれに応じた答えが得られます。


ほぼすべての用途でcompare_exchange_weak()をループにする必要があるのはなぜですか?

典型的なパターンA

アトミック変数の値に基づいてアトミック更新を実現する必要があります。失敗は、変数が目的の値で更新されておらず、再試行することを示します。 同時書き込みまたはスプリアス障害が原因で失敗するかどうかは実際には気にしないことに注意してください。ただし、それはusこの変更を行います。

_expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));
_

実世界の例では、複数のスレッドが要素を単一リンクリストに同時に追加します。各スレッドは最初にヘッドポインターをロードし、新しいノードを割り当てて、この新しいノードにヘッドを追加します。最後に、新しいノードとヘッドを交換しようとします。

別の例は、_std::atomic<bool>_を使用してミューテックスを実装することです。最初にcurrenttrueに設定し、ループを終了するスレッドに応じて、一度に最大1つのスレッドがクリティカルセクションに入ることができます。

典型的なパターンB

これは、実際にはアンソニーの本で言及されているパターンです。パターンAとは反対に、アトミック変数を1回更新する必要がありますが、誰がそれを行うかは気にしません。更新されていない限り、再度試行します。これは通常、ブール変数で使用されます。たとえば、ステートマシンが先に進むためのトリガーを実装する必要があります。どのスレッドがトリガーをプルするかは関係ありません。

_expected = false;
// !expected: if expected is set to true by another thread, it's done!
// Otherwise, it fails spuriously and we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);
_

通常、このパターンを使用してミューテックスを実装することはできません。そうしないと、複数のスレッドがクリティカルセクション内に同時に存在する可能性があります。

ただし、ループの外でcompare_exchange_weak()を使用することはまれです。それどころか、強力なバージョンが使用されている場合があります。例えば。、

_bool criticalSection_tryEnter(lock)
{
  bool flag = false;
  return lock.compare_exchange_strong(flag, true);
}
_

_compare_exchange_weak_はここでは適切ではありません。スプリアス障害のために戻ったときに、クリティカルセクションを占有している人がまだいない可能性が高いためです。

飢えているスレッド?

言及する価値のある点の1つは、スプリアス障害が引き続き発生し、スレッドが不足するとどうなるかということです。理論的には、compare_exchange_XXX()が一連の命令(LL/SCなど)として実装されている場合、プラットフォームで発生する可能性があります。 LLとSCの間で同じキャッシュラインに頻繁にアクセスすると、連続的なスプリアス障害が発生します。より現実的な例は、すべての同時スレッドが次の方法でインターリーブされるダムスケジューリングによるものです。

_Time
 |  thread 1 (LL)
 |  thread 2 (LL)
 |  thread 1 (compare, SC), fails spuriously due to thread 2's LL
 |  thread 1 (LL)
 |  thread 2 (compare, SC), fails spuriously due to thread 1's LL
 |  thread 2 (LL)
 v  ..
_

それは起こりますか?

幸いなことに、C++ 11が必要とするもののおかげで、それは永遠に起こりません。

実装では、アトミックオブジェクトの値が期待値と異なるか、アトミックオブジェクトへの同時変更がない限り、弱い比較および交換操作が一貫してfalseを返さないようにする必要があります。

なぜcompare_exchange_weak()をわざわざ使用して、ループを自分で記述するのですか? compare_exchange_strong()を使用するだけです。

場合によります。

ケース1:両方をループ内で使用する必要がある場合C++ 11によると:

比較と交換がループ内にある場合、一部のプラットフォームではウィークバージョンの方がパフォーマンスが向上します。

X86では(少なくとも現時点では、より多くのコアが導入されたときにパフォーマンスのためにLL/SCのような類似のスキームに頼ることになるかもしれません)、弱いバージョンと強いバージョンはどちらも単一の命令cmpxchgcompare_exchange_XXX()が実装されていないatomically(ここでは単一のハードウェアプリミティブが存在しないことを意味します)強いものはスプリアス障害を処理し、それに応じて再試行する必要があります。

しかし、

まれに、ループ内であってもcompare_exchange_strong()よりもcompare_exchange_weak()を好む場合があります。たとえば、アトミック変数がロードされ、計算された新しい値が交換される間に行うことがたくさんある場合(上記のfunction()を参照)。アトミック変数自体が頻繁に変更されない場合、すべてのスプリアス障害に対してコストのかかる計算を繰り返す必要はありません。代わりに、compare_exchange_strong()がそのような失敗を「吸収」し、実際の値の変更により失敗した場合にのみ計算を繰り返すことを期待できます。

ケース2:のみcompare_exchange_weak()ループ内で使用する必要がある場合C++ 11のコメント:

弱い比較と交換がループを必要とし、強い比較がループを必要としない場合、強いループが望ましいです。

これは通常、弱いバージョンから偽の障害を排除するためだけにループする場合です。同時書き込みが原因で交換が成功または失敗するまで再試行します。

_expected = false;
// !expected: if it fails spuriously, we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);
_

せいぜい、それは車輪を再発明し、compare_exchange_strong()と同じことを実行するだけです。悪いですか? このアプローチでは、ハードウェアで非スプリアスの比較と交換を提供するマシンを最大限に活用できません

最後に、他のことをループする場合(たとえば、上記の「典型的なパターンA」を参照)、compare_exchange_strong()もループに入れられる可能性が高く、前のケースに戻ります。 。

14
Eric Z

ループで交換する理由

通常、先に進む前に作業を完了したいので、compare_exchange_weakをループに入れて、成功するまで交換を試みる(つまり、trueを返す)。

compare_exchange_strongは、ループでよく使用されます。スプリアス障害が原因で失敗することはありませんが、同時書き込みが原因で失敗することはありません。

weakの代わりにstrongを使用する理由

非常に簡単:スプリアス障害は頻繁に発生しないため、パフォーマンスに大きな影響はありません。対照的に、このような障害を許容すると、一部のプラットフォームでweakバージョンを(strongと比較して)より効率的に実装できます。strongは常にスプリアス障害をチェックする必要がありますマスクしますこれは高価です。

したがって、一部のプラットフォームではweakよりもはるかに高速であるため、strongが使用されます。

いつweakstrongを使用すべきですか?

参照 は、weakを使用するときとstrongを使用するときのヒントを示します。

比較と交換がループ内にある場合、一部のプラットフォームではウィークバージョンの方がパフォーマンスが向上します。弱い比較と交換がループを必要とし、強い比較がループを必要としない場合、強いループが望ましいです。

そのため、答えは覚えるのが非常に簡単なようです。スプリアス障害のためだけにループを導入する必要がある場合は、それを行わないでください。 strongを使用します。とにかくループがある場合は、weakを使用します。

なぜ!expectedの例

それは状況とその望ましいセマンティクスに依存しますが、通常は正確さのために必要ではありません。省略すると、非常によく似たセマンティクスが得られます。別のスレッドがfalseに値をリセットする可能性がある場合にのみ、セマンティクスがわずかに異なる可能性があります(しかし、それを望む意味のある例を見つけることはできません)。詳細な説明については、Tony D.のコメントを参照してください。

anotherスレッドがtrueを書き込むときの単純な高速トラックです:trueを再度書き込むのではなく、中止します。

最後の質問について

しかし、上記で分析したように、ループ内の2つのバージョンは同じ/同様のパフォーマンスを提供するはずです。私が恋しいものは何ですか?

From Wikipedia

問題のメモリロケーションへの同時更新がない場合、LL/SCの実際の実装は常に成功するとは限りません。コンテキストスイッチ、別のロードリンク、または(多くのプラットフォームで)別のロードまたはストア操作など、2つの操作間の例外的なイベントにより、ストア条件が誤って失敗します。メモリバス経由で更新がブロードキャストされると、古い実装は失敗します。

そのため、たとえば、LL/SCはコンテキストスイッチで誤って失敗します。現在、強力なバージョンはその「独自の小さなループ」をもたらし、その偽の障害を検出し、再試行することでそれをマスクします。この独自のループは通常のCASループよりも複雑であることに注意してください。これは、スプリアス障害(およびマスク)と同時アクセスによる障害(値falseを返す)を区別する必要があるためです。弱いバージョンには、このような独自のループはありません。

両方の例で明示的なループを提供しているため、強力なバージョンでは小さなループを使用する必要はありません。その結果、strongバージョンの例では、失敗のチェックが2回行われます。 compare_exchange_strong(スプリアス障害と同時アクセスを区別する必要があるため、より複雑です)およびループごとに1回。この高価なチェックは不要であり、weakがここで高速になる理由です。

また、引数(LL/SC)は、これを実装する可能性があるoneであることに注意してください。さまざまな命令セットを備えたプラットフォームがさらにあります。さらに(さらに重要なことですが)、std::atomicは、すべての可能なデータ型のすべての操作をサポートする必要があるため、1000万バイトの構造体を宣言した場合でも、compare_exchangeこれについて。 CASを搭載したCPUでも、1000万バイトをCASすることはできません。そのため、コンパイラは他の命令を生成します(おそらくロック取得、その後に非アトミックな比較とスワップ、ロック解除が続きます)。次に、1000万バイトのスワップ中に発生する可能性のある事柄を考えます。したがって、8バイトの交換ではスプリアスエラーは非常にまれですが、この場合はより一般的です。

つまり、C++は2つのセマンティクスを提供します。「ベストエフォート」の1つ(weak)と、「間に何回悪いことが起こっても、確実に実行します」1つ(strong)。これらがさまざまなデータ型およびプラットフォームでどのように実装されるかは、まったく異なるトピックです。メンタルモデルを特定のプラットフォームの実装に結び付けないでください。標準ライブラリは、あなたが知っているよりも多くのアーキテクチャで動作するように設計されています。私たちが導き出せる唯一の一般的な結論は、成功を保証することは、失敗の可能性を試すだけでなく、通常、より困難です(したがって、追加の作業が必要になる場合があります)。

63
gexicide

ほとんどすべての用途でループする必要があるのはなぜですか?

ループせず、誤って失敗した場合、プログラムは有用なことを何もしていません-アトミックオブジェクトを更新せず、その現在の値がわからないためです(訂正:以下のコメントを参照してください)。呼び出しが役に立たない場合、それを行う意味は何ですか?

それは、偽の障害のために失敗したときにループすることを意味しますか?

はい。

その場合、なぜcompare_exchange_weak()をわざわざ使用して自分でループを記述するのですか?私たちは単にcompare_exchange_strong()を使用することができます。 compare_exchange_weak()の一般的な使用例は何ですか?

一部のアーキテクチャcompare_exchange_weakはより効率的であり、偽の障害は非常にまれであるため、弱い形式とループを使用してより効率的なアルゴリズムを作成できる可能性があります。

一般に、アルゴリズムがループする必要がない場合は、スプリアス障害を心配する必要がないため、代わりに強力なバージョンを使用することをお勧めします。強力なバージョンでもループする必要がある場合(および多くのアルゴリズムでループする必要がある場合)、一部のプラットフォームでは弱い形式を使用する方が効率的です。

なぜ!expectedループ状態にありますか?

値は別のスレッドによってtrueに設定されている可能性があるため、設定しようとしてループを続けたくありません。

編集:

しかし、上記で分析したように、ループ内の2つのバージョンは同じ/同様のパフォーマンスを提供するはずです。私が恋しいものは何ですか?

スプリアス障害が発生する可能性のあるプラットフォームでは、compare_exchange_strongは、スプリアス障害をチェックして再試行するために、より複雑にする必要があります。

弱い形式は、偽の失敗で単に戻り、再試行しません。

15
Jonathan Wakely

さて、アトミック左シフトを実行する関数が必要です。私のプロセッサにはこのためのネイティブ操作がなく、標準ライブラリにはそのための機能がないため、自分で作成しているように見えます。ここに行く:

void atomicLeftShift(std::atomic<int>* var, int shiftBy)
{
    do {
        int oldVal = std::atomic_load(var);
        int newVal = oldVal << shiftBy;
    } while(!std::compare_exchange_weak(oldVal, newVal));
}

ここで、ループが複数回実行される可能性がある2つの理由があります。

  1. 左シフトをしている間に他の誰かが変数を変更しました。計算の結果をアトミック変数に適用しないでください。他の誰かの書き込みが事実上消去されるためです。
  2. CPUが暴れ、弱いCASが誤って失敗しました。

正直言って、どちらでも構いません。左シフトは十分に速いので、たとえ失敗がスプリアスであったとしても、私はそれをもう一度やり直します。

less fastとは、強力なCASが強力であるために弱いCASをラップする必要がある余分なコードです。弱いCASが成功した場合、そのコードはあまり機能しません...しかし、失敗した場合、強力なCASは、ケース1またはケース2であるかどうかを判断するための検出作業を行う必要があります。効果的に自分のループ内。 2つのネストされたループ。今あなたのアルゴリズムの先生があなたをにらみつけていると想像してください。

そして、前述したように、私はその探偵の仕事の結果を気にしません!いずれにせよ、CASをやり直します。したがって、強力なCASを使用してもまったく何も得られず、わずかではあるが測定可能な効率が失われます。

言い換えると、アトミック更新操作を実装するために弱いCASが使用されます。 CASの結果に関心がある場合は、強力なCASが使用されます。

12
Sneftel