C++11のstd::atomic操作

6177 ワード

0.簡単な応用
原子操作とは、「原子は最小で分割不可能な最小個体」という意味であり、複数のスレッドが同じグローバルリソースにアクセスする際に、他のすべてのスレッドが同じ時間内に同じリソースにアクセスしないことを確保できることを意味する.つまり、彼は同じ時点で唯一のスレッドだけがこのリソースにアクセスすることを確保した.これは、共有リソースへの反発オブジェクトのアクセスの保護に似ていますが、原子操作は最下位に近づき、効率が向上します.従来のC++規格では原子操作を規定していないが,アセンブリ言語を用いるか,intelのpthreadのようなサードパーティのスレッドライブラリを用いて実現することが多い.新しい規格C++11では,原子操作の概念を導入し,この新しいヘッダファイルにより,atomic_bool,atomic_intなど、複数のスレッドでこれらのタイプの共有リソースを操作すると、コンパイラはこれらの操作が原子的であることを保証します.すなわち、任意の時点で1つのスレッドだけがこのリソースにアクセスすることを確保し、コンパイラは、複数のスレッドがこの共有リソースの正確性を尋ねることを保証します.これにより、ロックの使用が回避され、効率が向上します.
std::atomicはint,char,boolなどのデータ構造を原子的にカプセル化し,マルチスレッド環境ではstd::atomicオブジェクトへのアクセスは競合-冒険をもたらさない.std::atomicを用いてデータ構造のロックレス設計を実現できる.
#include                                                                                     
#include                                                                                     
#include                                                                                   
                                                                                                     
using namespace std;                                                                                 
                                                                                                     
atomic_long total(0);                                                                                
                                                                                                     
void click(int j) {                                                                                  
                                                                                                     
    cout << "thread:" << j << endl;                                                                  
                                                                                                     
    for(int i = 0; i < 1000; ++i) {                                                                  
        total +=1;                                                                                   
    }                                                                                                
}                                                                                                    
                                                                                                     
int main() {                                                                                         
                                                                                                     
    std::thread threads[10];                                                                         
                                                                                                     
    for (int i = 0; i < 10; ++i) {                                                                   
        threads[i] = std::thread(click, i);                                                          
    }                                                                                                
                                                                                                     
    for (auto &t : threads) {                                                                        
        t.join();                                                                                    
    }                                                                                                
                                                                                                     
    cout << "value:" << total << endl;                                                               
}                    

1.高次用法
C++11は原子性を保証する一連の原子操作クラスをもたらした.これらの方法は再分割できません.これらの変数の値を取得すると、変更前の値や変更後の値は永遠に取得され、変更中の中間値は取得されません.
これらのクラスは、原子読み取りと原子書き込みが2つの独立した原子操作であり、2つの独立した操作が加算されても原子性が保証されないため、コピー構造関数を無効にしています.
これらのクラスの中で最も簡単なのはatomic_です.flag(実はatomicと似ています)はtest_だけですand_set()とclear()メソッド.ここでtest_and_setは変数の値がfalseであるかどうかをチェックし、falseである場合は値をtrueに変更します.
atomic以外はflag以外のタイプはatomicで得ることができる.atomicは、一般的で理解しやすい方法を提供します.
  • store
  • load
  • exchange
  • compare_exchange_weak
  • compare_exchange_strong

  • storeは原子書き込み動作であり,loadは対応する原子読み取り動作である.
    Exchangeは2つの数値を交換し,プロセス全体が原子であることを保証する.
    そしてcompare_exchange_Weakとcompare_exchange_strongは有名なCAS(compare and set)です.パラメータは、ここで期待される数値と新しい数値を入力する必要があります.変数の値と期待値が一致するかどうかを比較し、そうであれば、ユーザーが指定した新しい数値に置き換えます.そうでない場合は、変数の値と所望の値を交換します.
    WeakバージョンのCASでは、フィールド値が期待値と同じときにfalseを返すなど、偶然予想外の戻りを許可していますが、いくつかのループアルゴリズムでは、これは受け入れられます.通常はstrongよりも高い性能を持っています.
    2例
    簡単な例を挙げると、CAS操作を使用して、ロックを持たない同時スタックを実現する.この例は『C++同時プログラミング』から抜粋した.
    Push
    非同時条件では、スタックのPush操作を実現するには、次のような操作があります.
  • 新規ノード
  • ノードのnextポインタを既存のスタックトップ
  • に向ける.
  • 更新スタックトップ
  • しかし、同時条件下では、上記の保護されていない操作に問題が発生する可能性が明らかになった.次に例を示します.
  • 元のスタックトップはAです.(このときのスタック状態:A->P->Q->...、左から右の最初の値をスタックトップとし、P->Qはp.next=Qを表す)
  • スレッド1は、Bをスタックする準備をする.スレッド1は、ステップ2を実行した後に強制的に占有される.(ノードBを新規作成し、B.next=A、すなわちB->A)
  • .
  • スレッド2は、cpuタイムスライスを取得し、Cスタックの操作、すなわち、ステップ1、2、3を完了する.このときスタック状態(このときスタック状態:C->A->...)
  • このときスレッド1はcpuタイムスライスを再取得し、ステップ3を実行する.スタック状態が(このときスタック状態:B->A->...)
  • になる
  • 結果スレッド2の動作が失われ、これは明らかに私たちが望んでいる結果ではありません.

  • では、私たちはどのようにこの問題を解決しますか?ステップ3がスタックトップを更新することを保証する限り、スタックトップはステップ2でトップスタックトップを取得すればよい.他のスレッドが操作されている場合、スタックトップは必然的に変更されるからです.
    CASを使用して、この問題を簡単に解決できます.スタックトップがステップ2でトップスタックトップを取得した場合、ステップ3を実行します.そうでなければ、スピン(すなわち、ステップ2を再実行します).
    従って、ロックなしのスタックPush操作は比較的簡単である.
    template
    class lock_free_stack
    {
    private:
     struct node
     {
       T data;
       node* next;
    
       node(T const& data_): 
        data(data_)
       {}
     };
    
     std::atomic head;
    public:
     void push(T const& data)
     {
       node* const new_node=new node(data); 
       new_node->next=head.load(); 
       while(!head.compare_exchange_weak(new_node->next,new_node));
     }
    };