2016-02-02 76 views
7

私は一定の間隔(約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_acquiredocumentation 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);   // <-- 
} 

これは正しいですか?これらのメモリオーダリングのどれかをさらに緩和することは可能ですか?それは問題なの?

+0

免責事項:アトミックの専門家ではありません。スピンロックの外側にある 'fetch_sub'は、他のスレッドからのカウントを確実に確認するために、少なくとも' memory_order_acq_rel'である必要はありませんか?何かを見落とすことができました。 – ShadowRanger

+1

ハードウェアプラットフォームとは何ですか? C++のメモリ順序付け機能の中には、比較的難解なプラットフォーム用のものがあることに注意してください。あなたは直接の利益のために多くの仕事や学習をしているかもしれません! – Yakk

+1

@ヤク:いくつかの 'memory_order'定数によって特別なハードウェア機能がないとしても、何もしないというわけではありません。 x86(メモリセマンティクスを強く秩序づけている)でも、 'memory_order'の選択はコンパイラの最適化/並べ替えの制限を変更します。行内の2つの 'memory_order_relaxed'操作はコンパイラーによってスワップされるので、2番目の操作が最初に起きるように見えます。同様に、 'relaxed'または' release'' store'sは一般にゼロオーバーヘッドですが、 'memory_order_seq_cst'ではコンパイラが明示的で高価な(〜100サイクル遅延)' mfence'命令を追加します。 – ShadowRanger

答えて

3

Since you say you're targeting x86 only、あなたはguaranteed strongly-ordered memory anywayです。 memory_order_seq_cstを避けると便利です(高価で不必要なメモリフェンスが発生する可能性があります)が、他のほとんどの操作で特別なオーバーヘッドが発生することはないため、誤ったコンパイラ命令の並べ替え以外の余分なリラクゼーションからは何も得られません。これは安全であるべきであり、C++ 11のアトミック使用して、他のソリューションよりも遅く:最悪

void Foo::Modify() 
{ 
    while(locked.test_and_set(std::memory_order_acquire)) ; 
    while(workers_busy.load(std::memory_order_acquire) != 0) ; // acq to see decrements 
    // .... 
    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); // Lock provides acq and rel free 
    locked.clear(std::memory_order_release); 
    // .... 
    workers_busy.fetch_sub(1, std::memory_order_acq_rel); // No lock wrapping; acq_rel 
} 

を、x86で、これは制約を注文いくつかのコンパイラを課していません。ロックされる必要のない追加のフェンスまたはロック命令を導入すべきではありません。

-4

テストのC++バージョンを使用したり、ロックを設定したりしないでください。代わりに、コンパイラが提供するアトミック命令を使用する必要があります。これは実際に大きな違いをもたらします。これはgccで動作し、テストとテストとセットロックで、これは標準テストとセットロックよりも少し効率的です。

unsigned int volatile lock_var = 0; 
#define ACQUIRE_LOCK() {                   
        do {                  
         while(lock_var == 1) {            
          _mm_pause;              
         }                 
        } while(__sync_val_compare_and_swap(&lock_var, 0, 1) == 1);    
       } 
#define RELEASE_LOCK() lock_var = 0 
// 

_mm_pauseは、プロセッサーのためにIntelによって推奨されているため、ロックが更新される時間があります。

ロックを取得してクリティカルセクションに入ると、スレッドはdo whileループを終了するだけです。

__sync_val_compare_and_swapのドキュメントを参照すると、これはxchgcmp命令に基づいており、この命令が実行されている間にバスをロックするために、生成されたアセンブリ内にlockという単語があります。この保護者は、原子リード・モディファイ・ライトを行います。

+0

"大きな違いを生む"の背後に数字がありますか?指定するデザインは、取得/解放のセマンティクスでアトミックを正しく使用するよりもいくぶん高速かもしれませんが、コンパイラーに順序制約を課すこともありません。非常に「RELEASE_LOCK」の動作は、ロックによって保護されるべき突然変異の前に起こるように(コンパイラによって、x86では強力な順序保証のおかげではなく、CPU上ではなく)並べ替えることができるので、あなたが避けようとしている競争状態を避けてください。また、非Intel(非​​x86可能)アーキテクチャに対しても移植性がありません。 – ShadowRanger

+0

ええ、私は '_mm_pause'について知っており、既にそれを使用しています。私はそれが関係していないので、私の質問からそれを残しました。私の知る限り、 'std :: atomic_flag'はまったく同じ' xchgcmp'命令に変換されます。しかし、原子をクリアするのではなく、単に値をゼロに設定するだけで、原子レベルで終わることはなく、予測可能なメモリ順序もないと私は確信していません。 – paddy

+0

ええ、これは私の修士論文科目の一部です。それはパフォーマンスと約10%の違いがあります。注文する場合は、スレッドがロックを取得しようとしたときに基づいて注文するベーカリーロックのようなものを見る必要があります。黒と白のベーカリーまたはMCSのロックは順序も提供しますが、シンプルではありません –

関連する問題