C ++ 11のstd :: atomic :: compare_exchange_weak()を理解する


88
bool compare_exchange_weak (T& expected, T val, ..);

compare_exchange_weak()C ++ 11で提供される比較交換プリミティブの1つです。それはだ、弱いオブジェクトの値が等しい場合、それは偽さえを返すという意味でexpected。これは、(x86のように1つではなく)一連の命令を使用して実装する一部のプラットフォームでの誤った障害が原因です。このようなプラットフォームでは、コンテキストスイッチ、別のスレッドによる同じアドレス(またはキャッシュライン)のリロードなどにより、プリミティブが失敗する可能性があります。それはだspurious、それが(等しくないオブジェクトの値ではありませんようexpected、操作を失敗しました)。代わりに、それは一種のタイミングの問題です。

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

29.6.5 ..疑似障害の結果、弱いコンペアアンドスワップのほぼすべての使用がループになります。

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

関連する別の質問。彼の著書「C ++ Concurrency In Action」の中で、Anthonyは次のように述べています。

//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ループ状態にあるのですか?すべてのスレッドが飢えてしばらく進行しないのを防ぐためにありますか?

編集:(最後の質問)

単一のハードウェア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つのバージョンは、同じ/同様のパフォーマンスを提供するはずです。私が恋しいことは何ですか?


4
最初の質問ではありませんが、多くの場合、(強いバージョンと弱いバージョンのどちらを使用するかにかかわらず)ループする必要があり、弱いバージョンの方が強いバージョンよりもパフォーマンスが優れている可能性があります。
TC

2
バブルソートとクイックソートの両方が「スワップを使用して」実装されるのと同じ方法で、弱いCASと強いCASの両方が「LL / SCを使用して」実装されます。つまり、それがタスクを実行するために使用される基本的な操作であるという意味で。彼らはラップの周りのLL / SCは非常に異なっています。弱いCASはLL / SCです。強力なCASはLL / SCであり、他にもたくさんのものがあります。
sneftel 2014


@TuXiaomiがそのリンクに答えを持っているので、標準で述べられているように「弱いバージョンが一部のプラットフォームでより良いパフォーマンスをもたらす」理由がわかりません。
2016

@Deqing他の場合、compare_exchange_weakは、他のプロセッサまたはスレッドの割り込みまたはアクションが原因で、誤って失敗する可能性があります。これらのプラットフォームでは、compare_exchange_strongは実質的にcompare_exchange_weakのループです。誤って失敗した場合は、再びループします。それは役に立ちますか?多分私は間違っています
Tu Xiaomi 2016

回答:


75

なぜループで交換を行うのですか?

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

またcompare_exchange_strong、ループでよく使用されることに注意してください。誤った失敗が原因で失敗することはありませんが、同時書き込みが原因で失敗します。

なぜ使用するweakのではなくstrong

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

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

いつweak、いつ使用する必要がありますstrongか?

参照が使用する際のヒントを述べているweakとするときに使用しますstrong

コンペアアンドスワップがループしている場合、一部のプラットフォームでは、弱いバージョンの方がパフォーマンスが向上します。弱いコンペアアンドスワップがループを必要とし、強いループが必要ない場合は、強いループが望ましいです。

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

なぜ!expected例にあるのか

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

別のスレッドが書き込みを行うときのファーストトラックtrueです。その後、true再度書き込みを試みる代わりに中止します。

最後の質問について

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

ウィキペディアから:

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

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

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

また、あなたの議論(LL / SC)は、これを実装するための1つの可能性にすぎないことに注意してください。命令セットがさらに異なるプラットフォームは他にもあります。さらに(そしてもっと重要なことに)、すべての可能なデータ型のstd::atomicすべての操作をサポートする必要があることに注意してください。したがって、1,000万バイトの構造体を宣言した場合でも、これで使用できます。CASを備えたCPUの場合でも、1,000万バイトをCASすることはできないため、コンパイラは他の命令を生成します(おそらく、ロック取得、非アトミックコンペアアンドスワップ、ロック解放の順)。ここで、1,000万バイトをスワップしている間に何が起こり得るかを考えてみてください。したがって、スプリアスエラーは8バイトの交換では非常にまれですが、この場合はより一般的である可能性があります。compare_exchange

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


「偽の障害に耐えられない場合にのみ、強力に使用してください。」-同時書き込みによる障害と誤った障害を区別するアルゴリズムは本当にありますか?私が考えることができるすべてのものは、更新を見逃すことがあるか、そうでない場合は、とにかくループが必要です。
Voo

4
@Voo:回答を更新しました。これで、リファレンスからのヒントが含まれます。区別するアルゴリズムがあるかもしれません。たとえば、「更新する必要がある」というセマンティクスについて考えてみます。何かの更新は1回だけ実行する必要があるため、同時書き込みが原因で失敗すると、他の誰かがそれを実行したことがわかり、中止できます。誤った失敗が原因で失敗した場合は、誰も更新していないため、再試行する必要があります。
gexicide 2014

8
この例で!が期待されるのはなぜですか?正確さのために必要ではありません。それを省略すると、同じセマンティクスが生成されます。」-ではないので...それが見つかったので、最初の交換が失敗したと言った場合b、既にあるtrue、その後、 -とexpectedtrue-せず&& !expected、それがループし、他の(愚かな)交換をしようとtrueしてtrueいるが、うまく自明から壊す「成功」もwhileループ、しかし発揮できbその間にに戻っfalseた場合、意味のある異なる動作になります。その場合、ループは継続し、最終的には中断する前に再び設定される可能性があります。b true
トニーデルロイ2014

@TonyD:そうですね、はっきりさせておきます。
gexicide 2014

申し訳ありませんが、最後の質問をもう1つ追加しました;)
Eric Z

18

さまざまなオンラインリソース(たとえば、これこれ)、C ++ 11標準、およびここに記載されている回答を確認した後、私は自分でこれに答えようとしています。

関連する質問がマージされます(たとえば、「なぜ!期待されるのですか?」が「なぜcompare_exchange_weak()をループに入れるの」とマージ)、それに応じて回答が与えられます。


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

典型的なパターンA

アトミック変数の値に基づいてアトミック更新を実行する必要があります。失敗は、変数が目的の値で更新されておらず、再試行することを示しています。同時書き込みが原因で失敗するのか、誤った失敗が原因で失敗するのかは、実際には気にしない こと注意してください。しかし、私たち はこの変化を起こすのは私たちあることに気を配ってます。

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と同様のスキームに頼る可能性があります)、弱いバージョンと強いバージョンはどちらも単一の命令に要約されるため、基本的に同じcmpxchgです。アトミックにcompare_exchange_XXX()実装されていない他のいくつかのプラットフォーム(ここでは単一のハードウェアプリミティブが存在しないことを意味します)では、ループ内の弱いバージョンが戦いに勝つ可能性があります。

だが、

まれに、ループ内であっても優先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()ループに入れられる可能性が高く、前のケースに戻ります。


17

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

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

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

はい。

もしそうなら、なぜcompare_exchange_weak()私たちは自分たちでループを使用して書くのですか?compare_exchange_strong()を使用するだけで、誤った失敗を取り除くことができると思います。compare_exchange_weak()の一般的な使用例は何ですか?

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

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

なぜですか !expectedループ状態にあるのですか?

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

編集:

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

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

弱い形式は、誤った失敗で戻るだけで、再試行しません。


2
+1すべての点で事実上正確です(Qが必死に必要とします)。
トニーデルロイ2014

you don't know what its current value isスプリアス障害が起きる第一の点、で、現在の値はその時点で期待値と等しくないでしょうか?そうでなければ、それは本当の失敗になるでしょう。
Eric Z

IMO、ウィークバージョンとストロングバージョンの両方が、単一のCASハードウェアプリミティブが存在しないプラットフォームでLL / SCを使用して実装されます。だから、私になぜ間のパフォーマンスの差があるwhile(!compare_exchange_weak(..))とはwhile(!compare_exchange_strong(..))
Eric Z

申し訳ありませんが、最後の質問をもう1つ追加しました。
Eric Z

1
@ジョナサン:ちょっとしたことですが、誤って失敗した場合は現在の値を知っています(もちろん、変数を読み取るまでに現在の値であるかどうかはまったく別の問題ですが、それは弱い/強いかどうかは関係ありません)。たとえば、これを使用して、値がnullであると想定して変数を設定しようとしました。失敗した場合は(誤ってかどうかにかかわらず)、実際の値に応じて試行を続けます。
キャメロン

13

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

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が誤って失敗しました。

私は正直どちらを気にしません。左シフトは十分に高速なので、失敗が誤っていたとしても、もう一度やり直したほうがよいでしょう。

何が少ないものの、速い、強いCASが強くなるために周りの弱いCASをラップする必要があると余分なコードがあります。そのコードは、弱いCASが成功した場合はあまり機能しません...しかし、失敗した場合、強いCASは、それがケース1かケース2かを判断するために、何らかの探偵作業を行う必要があります。その探偵作業は2番目のループの形をとります。効果的に私自身のループの中に。2つのネストされたループ。あなたのアルゴリズムの先生が今あなたを睨みつけていると想像してみてください。

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

つまり、弱いCASを使用してアトミック更新操作を実装します。CASの結果を気にする場合は、強力なCASを使用します。


0

上記の回答のほとんどは、「疑似障害」をある種の問題、パフォーマンスと正確さのトレードオフとして扱っていると思います。

ほとんどの場合、弱いバージョンの方が速いように見えますが、誤った障害が発生した場合は遅くなります。また、強力なバージョンは、誤った障害の可能性がないバージョンですが、ほとんどの場合、低速です。

私にとっての主な違いは、これら2つのバージョンがABA問題を処理する方法です。

弱いバージョンは、ロードとストアの間のキャッシュラインに誰も触れていない場合にのみ成功するため、ABA問題を100%検出します。

強力なバージョンは、比較が失敗した場合にのみ失敗するため、追加の対策なしではABA問題を検出しません。

したがって、理論的には、弱順序アーキテクチャで弱バージョンを使用する場合、ABA検出メカニズムは必要なく、実装ははるかに簡単になり、パフォーマンスが向上します。

ただし、x86(厳密な順序のアーキテクチャ)では、弱いバージョンと強いバージョンは同じであり、どちらもABA問題に悩まされています。

したがって、完全にクロスプラットフォームのアルゴリズムを作成する場合は、とにかくABA問題に対処する必要があるため、弱いバージョンを使用してもパフォーマンス上のメリットはありませんが、誤った障害を処理するとパフォーマンスが低下します。

結論として、移植性とパフォーマンスの理由から、強力なバージョンは常により良いまたは同等のオプションです。

弱いバージョンは、ABA対策を完全にスキップできる場合、またはアルゴリズムがABAを気にしない場合にのみ、より良いオプションになります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.