私は一定の間隔(約1kHz)でタイムクリティカルな処理を実行するワーカースレッドをいくつか持っています。各サイクルで、労働者は雑用を行うために目を覚まし、次のサイクルが始まる前にが(平均)になるはずです。それらは同じオブジェクト上で動作しますが、これはメインスレッドによって時折変更されることがあります。2つのアトミックでのスピンロックのための最小限のメモリオーダリング
レースを防ぐが、オブジェクトの変更は次のサイクルの前に発生できるようにするために、私はまだ仕事をしているどのように多くのスレッドを記録するために、原子カウンターと一緒にスピンロックを使用しています
class Foo {
public:
void Modify();
void DoWork(SomeContext&);
private:
std::atomic_flag locked = ATOMIC_FLAG_INIT;
std::atomic<int> workers_busy = 0;
};
void Foo::Modify()
{
while(locked.test_and_set(std::memory_order_acquire)) ; // spin
while(workers_busy.load() != 0) ; // spin
// Modifications happen here ....
locked.clear(std::memory_order_release);
}
void Foo::DoWork(SomeContext&)
{
while(locked.test_and_set(std::memory_order_acquire)) ; // spin
++workers_busy;
locked.clear(std::memory_order_release);
// Processing happens here ....
--workers_busy;
}
これにより、少なくとも1つのスレッドが開始されていれば、残りのすべての作業が即座に完了し、次の作業の作業を他の作業者が開始する前に常にブロックされます。
atomic_flag
は、C++ 11でスピンロックを実装するための受け入れられた方法であるように、「取得」および「解放」メモリオーダーでアクセスされます。
memory_order_acquire
:documentation at cppreference.comによれば、このメモリ順序ロード操作が影響を受けたメモリ・ロケーションに取得動作を行う:なしメモリは、現在のスレッドにアクセスしていないこの負荷の前に並べ替えることができます。これにより、同じアトミック変数を解放する他のスレッドのすべての書き込みが、現在のスレッドで確実に表示されます。
memory_order_release
:このメモリ順序付きストア動作は、レリーズ操作を行う:なしメモリはこのストアの後に並べ替えることができ、現在のスレッドにアクセスしていません。これにより、現在のスレッド内のすべての書き込みが、同じアトミック変数を取得する他のスレッドで確実に表示され、アトミック変数への依存関係を保持する書き込みは、同じアトムを消費する他のスレッドで可視になります。
私が上記を理解したように、これはスレッド間で保護されたアクセスを同期させて、メモリの順序付けを過度に保守的にすることなく、
私が知りたいのは、このパターンの副作用は私が別の原子変数を同期させるためにスピンロックミューテックスを使用しているためです。
++workers_busy
,--workers_busy
およびworkers_busy.load()
への呼び出しはすべて、デフォルトのメモリオーダーmemory_order_seq_cst
を現在持っています。このアトミックの唯一の興味深い用途は、ブロックModify()
を--workers_busy
(これはではなく、はスピンロックミューテックスによって同期化されています)をブロック解除することであることを考えれば、この変数で同じリリースメモリを使用できます? すなわち
void Foo::Modify()
{
while(locked.test_and_set(std::memory_order_acquire)) ;
while(workers_busy.load(std::memory_order_acquire) != 0) ; // <--
// ....
locked.clear(std::memory_order_release);
}
void Foo::DoWork(SomeContext&)
{
while(locked.test_and_set(std::memory_order_acquire)) ;
workers_busy.fetch_add(1, std::memory_order_relaxed); // <--
locked.clear(std::memory_order_release);
// ....
workers_busy.fetch_sub(1, std::memory_order_release); // <--
}
これは正しいですか?これらのメモリオーダリングのどれかをさらに緩和することは可能ですか?それは問題なの?
免責事項:アトミックの専門家ではありません。スピンロックの外側にある 'fetch_sub'は、他のスレッドからのカウントを確実に確認するために、少なくとも' memory_order_acq_rel'である必要はありませんか?何かを見落とすことができました。 – ShadowRanger
ハードウェアプラットフォームとは何ですか? C++のメモリ順序付け機能の中には、比較的難解なプラットフォーム用のものがあることに注意してください。あなたは直接の利益のために多くの仕事や学習をしているかもしれません! – Yakk
@ヤク:いくつかの 'memory_order'定数によって特別なハードウェア機能がないとしても、何もしないというわけではありません。 x86(メモリセマンティクスを強く秩序づけている)でも、 'memory_order'の選択はコンパイラの最適化/並べ替えの制限を変更します。行内の2つの 'memory_order_relaxed'操作はコンパイラーによってスワップされるので、2番目の操作が最初に起きるように見えます。同様に、 'relaxed'または' release'' store'sは一般にゼロオーバーヘッドですが、 'memory_order_seq_cst'ではコンパイラが明示的で高価な(〜100サイクル遅延)' mfence'命令を追加します。 – ShadowRanger