古典的な問題の変形を解決する移植可能なコード(Intel、ARM、PowerPC ...)を記述したいと思います。
Initially: X=Y=0
Thread A:
X=1
if(!Y){ do something }
Thread B:
Y=1
if(!X){ do something }
ここでの目標は、両方のスレッドがやっているような状況を避けるためですsomething。(どちらも実行しなくても問題ありません。これは1回だけ実行するメカニズムではありません。)以下の私の推論に欠陥がある場合は、修正してください。
私は次のようにmemory_order_seq_cstアトミックstoresおよびloads を使用して目標を達成できることを認識しています。
std::atomic<int> x{0},y{0};
void thread_a(){
x.store(1);
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!x.load()) bar();
}
{x.store(1), y.store(1), y.load(), x.load()}イベントにはいくつかの単一の合計順序が必要であり、プログラムの順序「エッジ」に同意する必要があるため、これは目標を達成します。
x.store(1)「TOは前に」y.load()y.store(1)「TOは前に」x.load()
foo()呼び出された場合、追加のエッジがあります:
y.load()「前に値を読み取る」y.store(1)
bar()呼び出された場合、追加のエッジがあります:
x.load()「前に値を読み取る」x.store(1)
そして、これらすべてのエッジを結合すると、サイクルが形成されます。
x.store(1)「TOが前にある」y.load()「前に値を読み取る」y.store(1)「TOが前にある」x.load()「前に値を読み取る」x.store(true)
これは、注文に循環がないという事実に違反しています。
happens-beforeこれらのエッジが実際にhappens-before関係を暗示するという私の仮定の正確さについてフィードバックを求めたいので、私はのような標準的な用語とは対照的に、非標準的な用語「TOが前にある」と「前に値を読み取る」を意図的に使用しています。グラフ、およびそのような結合されたグラフのサイクルは禁止されています。それについてはよくわかりません。私が知っているのは、このコードがIntel gccとclangおよびARM gccで正しいバリアを生成することです
ここで、「X」を制御できないため、実際の問題はもう少し複雑です。マクロ、テンプレートなどの背後に隠れており、 seq_cst
"X"が単一の変数なのか、それとも他の概念(たとえば、軽量のセマフォやミューテックス)なのかさえわかりません。私が知っているのは、2つのマクロがset()あり、別のスレッドがを呼び出した後に「戻る」check()というマクロをcheck()返すことだけです。(それはされてもいることを知らおよびスレッドセーフであり、データ・レースUBを作成することはできません。)trueset()setcheck
したがって、概念的にset()は「X = 1」やcheck()「X」に似ていますが、アトミックに直接アクセスすることはできません。
void thread_a(){
set();
if(!y.load()) foo();
}
void thread_b(){
y.store(1);
if(!check()) bar();
}
私は、心配しているset()内部として実装されるかもしれないx.store(1,std::memory_order_release)および/またはcheck()かもしれませんx.load(std::memory_order_acquire)。または、仮にstd::mutex1つのスレッドがロックを解除し、別のスレッドがtry_lockingしているということです。ISO標準でstd::mutexは、seq_cstではなく、取得と解放の順序が保証されています。
これが当てはまる場合は、check()前にボディを「並べ替え」できるかどうかですy.store(true)(PowerPCでこれが発生することを示すAlexの回答を参照してください)。
これでこの一連のイベントが可能になるため、これは本当に悪いことです。
thread_b()最初にx(0)の古い値をロードしますthread_a()を含むすべてを実行しますfoo()thread_b()を含むすべてを実行しますbar()
それで、両方foo()とbar()も呼ばれました、私はそれを避けなければなりませんでした。それを防ぐための私の選択肢は何ですか?
オプションA
Store-Loadバリアを強制してみてください。これは、実際には次の方法で実現できます。Alexが別の回答でstd::atomic_thread_fence(std::memory_order_seq_cst);説明したように、テストされたすべてのコンパイラは完全なフェンスを放出しました。
- x86_64:MFENCE
- PowerPC:hwsync
- イタヌイム:mf
- ARMv7 / ARMv8:dmb ish
- MIPS64:同期
このアプローチの問題は、C ++ルールで保証を見つけることができずstd::atomic_thread_fence(std::memory_order_seq_cst)、完全なメモリバリアに変換する必要があることです。実際、atomic_thread_fenceC ++のsの概念は、メモリバリアのアセンブリの概念とは異なる抽象化レベルにあり、「アトミック操作が何と同期するか」のようなものを扱っています。以下の実装が目標を達成したという理論的な証拠はありますか?
void thread_a(){
set();
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!y.load()) foo();
}
void thread_b(){
y.store(true);
std::atomic_thread_fence(std::memory_order_seq_cst)
if(!check()) bar();
}
オプションB
Y上で読み取り-変更-書き込みのmemory_order_acq_rel操作を使用して、同期を達成するためにYを介して持っている制御を使用します。
void thread_a(){
set();
if(!y.fetch_add(0,std::memory_order_acq_rel)) foo();
}
void thread_b(){
y.exchange(1,std::memory_order_acq_rel);
if(!check()) bar();
}
ここでの考え方は、単一のアトミック(y)へのアクセスは、すべてのオブザーバーが同意する単一の順序を形成する必要があるということです。つまり、fetch_add前exchangeか逆のどちらかです。
のfetch_add前にあるexchange場合、の「解放」部分はfetch_addの「取得」部分と同期します。exchangeしたがって、のすべての副作用がset()コード実行から見えるようにするcheck()必要bar()があるため、呼び出されません。
それ以外の場合は、exchange前であるfetch_add、その後、fetch_add表示されます1と呼んでいませんfoo()。だから、両方を呼び出すことは不可能であるfoo()とbar()。この推論は正しいですか?
オプションC
ダミーアトミックを使用して、災害を防止する「エッジ」を導入します。次のアプローチを検討してください。
void thread_a(){
std::atomic<int> dummy1{};
set();
dummy1.store(13);
if(!y.load()) foo();
}
void thread_b(){
std::atomic<int> dummy2{};
y.store(1);
dummy2.load();
if(!check()) bar();
}
ここでatomicの問題がローカルであると思われる場合は、それらをグローバルスコープに移動することを想像してください。次の理由から、それは私には重要ではないように思われます。ダミー1がいかに面白いかを明らかにするような方法でコードを意図的に記述しました。とdummy2は完全に独立しています。
なぜこれがうまくいくのでしょうか?さて、{dummy1.store(13), y.load(), y.store(1), dummy2.load()}プログラムの順序「エッジ」と一致する必要があるいくつかの単一の合計順序が存在する必要があります。
dummy1.store(13)「TOは前に」y.load()y.store(1)「TOは前に」dummy2.load()
(seq_cstストア+ロードは、個別のバリア命令が必要ないAArch64を含む実際のISAでasmで実行されるように、StoreLoadを含む完全なメモリバリアと同等のC ++を形成することが期待されます。)
ここで、考慮すべき2つのケースがあります。それは、全体の順序のy.store(1)前y.load()または後です。
場合y.store(1)の前でy.load()、その後foo()と呼ばれ、私たちは安全であることはありません。
もし y.load()がbeforeのy.store(1)、それをプログラムの順序ですでに持っている2つのエッジと組み合わせて、それを推定します。
dummy1.store(13)「TOは前に」dummy2.load()
さて、dummy1.store(13)の効果を解除する解除操作は、ありset()、そしてdummy2.load()ので、取得操作でcheck()の効果が表示されるはずですset()ため、bar()呼び出されませんし、私たちは安全です。
check()の結果が表示されると思うのは、ここで正しいset()ですか。さまざまな種類の「エッジ」(「プログラム順序」または「シーケンス前」、「合計順序」、「リリース前」、「取得後」)をそのように組み合わせることができますか?私はこれについて深刻な疑問を抱いています:C ++ルールは、同じ場所でのストアとロードの間の「同期」の関係について話しているようです-ここにはそのような状況はありません。
seq_cst全体の順序で前にあることdumm1.storeがわかっている場合(他の理由により)についてのみ心配していることに注意してくださいdummy2.load。したがって、それらが同じ変数にアクセスしていた場合、ロードは格納された値を確認し、それと同期していました。
(アトミックなロードとストアが少なくとも1方向のメモリバリアにコンパイルされる実装のメモリバリア/順序変更の推論(およびseq_cst操作は順序変更できない:たとえばseq_cstストアはseq_cstロードを渡すことができない)は、任意のロード/店は後にdummy2.load間違いなく他のスレッドに見えるようになり後に y.store。そして、同様に他のスレッドのため、...の前にy.load。)
https://godbolt.org/z/u3dTa8でオプションA、B、Cの実装を試すことができます。
foo()とbar()、呼び出されないようにすること。
compare_exchange_*して、その値を変更せずにアトミックブールに対してRMW操作を実行できます(単純に期待値と新規値を同じ値に設定します)。
atomic<bool>ありexchangeとcompare_exchange_weak。後者は、CAS(true、true)またはfalse、falseによって(試行する)ダミーのRMWを実行するために使用できます。失敗するか、または値を自動的にそれ自体で置き換えます。(x86-64 asmでは、そのトリックlock cmpxchg16bは保証されたアトミックな16バイトのロードを行う方法です。非効率的ですが、個別のロックを取得するよりも悪くはありません。)
foo()もbar()呼び出されないことも起こり得ます。「問題Xがあると思うが、問題Yがある」という種類の応答を回避するために、コードの多くの「現実の世界」の要素を取り入れたくありませんでした。しかし、人は本当に背景階建てであるかを知る必要がある場合:set()本当にされsome_mutex_exit()、check()されtry_enter_some_mutex()、y「いくつかのウェイターがあります」と、foo()「誰にも目覚めないで終了」で、bar()「wakup待ち」である...しかし、私がすることを拒否このデザインについてここで議論してください-私はそれを本当に変えることはできません。
std::atomic_thread_fence(std::memory_order_seq_cst)、完全なバリアにコンパイルされますが、全体の概念は実装の詳細なので、見つけられません標準でのそれの言及。(CPU・メモリ・モデルは、通常れる reoreringsシーケンシャル一貫に対して許可されるものに関して定義された、例えばx86のある配列-CST +ストアバッファW /転送)