古典的な問題の変形を解決するポータブルコード(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
アトミックstore
sおよびload
sを使用して目標を達成できることを知っています。
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()
が呼び出された場合、追加のEdgeがあります。
y.load()
「前に値を読み取る」y.store(1)
そして、bar()
が呼び出された場合、追加のEdgeがあります。
x.load()
「前に値を読み取る」x.store(1)
そして、これらすべてのエッジを結合すると、サイクルが形成されます。
x.store(1)
「TOの前にある」y.load()
「TOの前に値を読み取る」y.store(1)
「TOの前にある」x.load()
「前に値を読み取る」x.store(true)
これは、注文に循環がないという事実に違反しています。
私はhappens-before
のような標準的な用語ではなく、「TOが前にある」および「値を前に読み取る」という非標準的な用語を意図的に使用しています。これらのエッジは確かにhappens-before
関係を意味し、単一のグラフで一緒に組み合わせることができ、そのような組み合わされたグラフでのサイクルは禁止されています。それについてはよくわかりません。私が知っているのは、このコードがIntel gccとclangとARM gccに正しいバリアを生成することです。
今、「X」を制御できないため、実際の問題はもう少し複雑です。マクロやテンプレートなどの背後に隠れており、seq_cst
"X"が単一の変数なのか、それとも他の概念(たとえば、軽量のセマフォやミューテックス)なのかもわかりません。私が知っているのは、2つのマクロset()
とcheck()
があり、別のスレッドがcheck()
を呼び出した後、set()
がtrue
を返すことです。 (これはですset
とcheck
はスレッドセーフであり、データレースUBを作成できないことも知られています。)
つまり、概念的には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)
である可能性があることを心配しています。または、仮に1つのスレッドがロックを解除しており、もう1つがstd::mutex
ingであるtry_lock
です。 ISO標準のstd::mutex
は、seq_cstではなく、取得と解放の順序付けのみが保証されています。
これが事実である場合、check()
の前にy.store(true)
のif bodyを「並べ替え」ることができます(Alex's answer を参照してください)。 )。
現在、この一連のイベントが可能であるため、これは本当に悪いことです。
thread_b()
は最初にx
(0
)の古い値をロードしますthread_a()
はfoo()
を含むすべてを実行しますthread_b()
はbar()
を含むすべてを実行しますつまり、foo()
とbar()
の両方が呼び出されましたが、これは避けなければなりませんでした。それを防ぐための私の選択肢は何ですか?
オプションA
Store-Loadバリアを強制してみてください。実際には、これはstd::atomic_thread_fence(std::memory_order_seq_cst);
によって実現できます-別の答えで Alexによって説明されています テストされたすべてのコンパイラが完全なフェンスを放出しました:
- x86_64:MFENCE
- PowerPC:hwsync
- イタヌイム:mf
- ARMv7/ARMv8:dmb ish
- MIPS64:同期
このアプローチの問題は、C++ルールで保証が見つからなかったこと、std::atomic_thread_fence(std::memory_order_seq_cst)
が完全なメモリバリアに変換される必要があることです。実際、C++でのatomic_thread_fence
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
sがローカルであると考える場合は、それらをグローバルスコープに移動することを想像してください。次の理由で、それは私には重要ではないと思われ、意図的にコードを記述しましたダミー1とダミー2が完全に分離していることがどれほど面白いかを明らかにするような方法で。
なぜこれがうまくいくのでしょうか?まあ、{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()
がy.store(1)
の前にある場合、それをプログラムの順序ですでに持っている2つのエッジと組み合わせると、次のように推定されます。
dummy1.store(13)
「TOが前にある」dummy2.load()
ここで、dummy1.store(13)
は解放操作であり、set()
の効果を解放します。dummy2.load()
は取得操作です。したがって、check()
はset()
の効果を確認する必要があるため、bar()
は呼び出されず、安全です。
check()
がset()
の結果を表示すると考えるのは、ここで正しいですか? このように、さまざまな種類の「エッジ」(「プログラムの順序」または「シーケンスの前」、「合計の順序」、「リリース前」、「取得後」)を組み合わせることができますか?私はこれについて深刻な疑いを抱いています:C++ルールは、同じ場所でのストアとロード間の「同期」の関係について話しているようです-ここではそのような状況はありません。
dumm1.store
がseq_cst全体の順序でdummy2.load
の前にあるknown(他の理由による)である場合についてのみ心配していることに注意してください。したがって、同じ変数にアクセスしていた場合、ロードは格納された値を確認し、それと同期していました。
(アトミックなロードとストアが少なくとも1方向のメモリバリアにコンパイルされる実装のメモリバリア/並べ替えの推論(およびseq_cst操作は並べ替えできない:たとえば、seq_cstストアはseq_cstロードを渡すことができない)は、任意のロード/ dummy2.load
の後のストアは、他のスレッドから確実に見えるようになりますaftery.store
。そして、他のスレッドについても同様に、y.load
の前に。)
https://godbolt.org/z/u3dTa8 にあるオプションA、B、Cの実装を試すことができます
iSO標準std :: mutexでは、seq_cstではなく、取得と解放の順序のみが保証されています。
ただし、seq_cst
は操作のプロパティではないため、「seq_cst順序付け」が保証されているものはありません。
seq_cst
は、std::atomic
または代替のアトミッククラスの特定の実装のすべての操作に対する保証です。そのため、あなたの質問は不健全です。