C++プログラムの同時実行時に共有データを保護する問題を探究する

4842 ワード

まず簡単なコードでこの問題を理解します.どうきもんだい
単純な構造体Counterを使用して、この値を変更する方法と値を含む.

struct Counter {
  int value;
 
  void increment(){
    ++value;
  }
};

次に、複数のスレッドを起動して、構造体の値を変更します.
 

int main(){
  Counter counter;
 
  std::vector<:thread> threads;
  for(int i = 0; i < 5; ++i){
    threads.push_back(std::thread([&counter](){
      for(int i = 0; i < 100; ++i){
        counter.increment();
      }
    }));
  }
 
  for(auto& thread : threads){
    thread.join();
  }
 
  std::cout << counter.value << std::endl;
 
  return 0;
}

5つのスレッドを起動してカウンタの値を増やし,各スレッドを100回増やし,スレッド終了時にカウンタの値を印刷した.
しかし、私たちがこのプログラムを実行するとき、私たちはそれが500に承諾することを望んでいますが、事実はそうではありません.プログラムがどのような結果を印刷するかを正確に知る人はいません.次は私の機械で実行した後に印刷されたデータで、毎回違います.

442
500
477
400
422
487

問題の原因は、カウンタ値を変更することは原子操作ではなく、次の3つの操作を経てカウンタの増加を完了する必要があることです.
  • まずvalueの値
  • を読み出す.
  • value値を1
  • 加算
  • 新しい値をvalue
  • に割り当てます.
    しかし、このプログラムを実行するために単一のスレッドを使用する場合は、もちろん問題はありません.そのため、プログラムは順番に実行されますが、マルチスレッド環境では面倒です.次の実行順序を想像してみてください.
  • Thread 1:valueを読み出し、0、1を加算するので、value=1
  • Thread 2:valueを読み出し、0を得る、1を加えるので、value=1
  • Thread 1:1をvalueに割り当て、1
  • を返します.
  • Thread 2:1をvalueに割り当て、1
  • を返します.
    この場合、マルチスレッドのインタリーブ実行と呼ばれます.つまり、マルチスレッドは同じ時点で同じ文を実行する可能性があります.2つのスレッドしかありませんが、インタリーブの現象も明らかです.より多くのスレッド、より多くの操作を実行する必要がある場合は、このインタリーブは必然的に発生します.
    スレッドのインタリーブの問題を解決するには、次のような方法があります.
  • 信号量Semaphores
  • 原子参照Atomic references
  •     Monitors
  •     Condition codes
  •     Compare and swap

  • この文章では,この問題を解決するために信号量をどのように使用するかを学ぶ.信号量も反発量(Mutex)と呼ばれるものが多く,同じ時間に1つのスレッドで1つの反発オブジェクトのロックを取得することが許され,Mutexの単純な属性によってインタリーブの問題を解決することができる.
    Mutexを使用してカウンタプログラムをスレッドで安全にします
    C++11スレッドライブラリでは、反発量がmutexヘッダファイルに含まれており、対応するクラスはstd::mutexであり、mutex:lock()とunlock()の2つの重要な方法があり、名前からオブジェクトをロックしたり、ロックオブジェクトを解放したりするために使用されていることがわかります.反発量がロックされると、lock()が再び呼び出され、オブジェクトが解放される価値のある閉塞が返される.
    先ほどのカウンタ構造体がスレッドで安全であるように、set:mutextメンバーを追加し、各メソッドでlock()/unlock()メソッドで保護します.
    
    struct Counter {
      std::mutex mutex;
      int value;
     
      Counter() : value(0) {}
     
      void increment(){
        mutex.lock();
        ++value;
        mutex.unlock();
      }
    };
    

    そして私たちは再びこのプログラムをテストして、印刷の結果は500で、毎回同じです.
    例外とロック
    別の状況を見てみましょう.カウンタに減算操作があり、値が0のときに異常を放出することを想像してみましょう.
    
    struct Counter {
      int value;
     
      Counter() : value(0) {}
     
      void increment(){
        ++value;
      }
     
      void decrement(){
        if(value == 0){
          throw "Value cannot be less than 0";
        }
     
        --value;
      }
    };
    

    次にクラスを変更してこの構造体にアクセスする必要はありません.カプセルを作成します.
    
    struct ConcurrentCounter {
      std::mutex mutex;
      Counter counter;
     
      void increment(){
        mutex.lock();
        counter.increment();
        mutex.unlock();
      }
     
      void decrement(){
        mutex.lock();
        counter.decrement();    
        mutex.unlock();
      }
    };
    

    ほとんどの場合、このパッケージはよく動作しますが、decrementメソッドを使用すると異常が発生します.これは大きな問題で、異常が発生するとunlockメソッドが呼び出されず、反発量が占有され続け、プログラム全体が詰まっている(デッドロック)ため、try/catch構造で異常を処理する必要があります.
    
    void decrement(){
      mutex.lock();
      try {
        counter.decrement();
      } catch (std::string e){
        mutex.unlock();
        throw e;
      }
      mutex.unlock();
    }
    

    このコードは難しくありませんが、醜いように見えます.もし関数に10の終了点があれば、終了点ごとにunlockメソッドを呼び出さなければなりません.どこかでunlockを忘れたかもしれません.さまざまな悲劇が発生し、悲劇が発生すると、プログラムのデッドロックが直接発生します.
    次に、この問題をどのように解決するかを見てみましょう.
    自動ロック管理
    セグメント全体のコードを含める必要がある場合(ここでは方法であり、循環体または他の制御構造である可能性もある)、ロックの解放を忘れることを避ける良い解決方法があります.それはstd::lock_guardです.
    このクラスは単純なスマートロックマネージャですが、std::lock_を作成します.guardの場合、lock_guardプロファイルでは、ロックが自動的に解放されます.次のコードを参照してください.
     
    
    struct ConcurrentSafeCounter {
      std::mutex mutex;
      Counter counter;
     
      void increment(){
        std::lock_guard<:mutex> guard(mutex);
        counter.increment();
      }
     
      void decrement(){
        std::lock_guard<:mutex> guar(mutex);
        mutex.unlock();
      }
    };
    

    だいぶ爽やかに見えたのではないでしょうか.
    lock_の使用guard、いつロックを解除するか考える必要はありません.この仕事はstd::lock_guardインスタンスが完成します.
    結論
    この論文では,信号量/反発量によって共有データを保護する方法を学習した.ロックを使用すると、プログラムのパフォーマンスが低下することを覚えておいてください.いくつかの高同時応用環境では他のより良い解決策があるが,これは本論文の議論の範疇内ではない.
    Githubで本明細書のソースコードを取得できます.