C ++ 11ミューテックスよりもロックを優先する(译)

前の投稿で何かが示された場合は、ミューテックスを慎重に使用する必要があります。そのため、それらをロックで包む必要があります。

  • 前回の記事では、ミューテックスを特別な注意を払って使用する必要があることを説明しました(そうしないと、デッドロックの問題が発生しやすくなります)。ロック付きのミューテックスを使用する必要があります。

ロック

ロックは、RAIIイディオムに従ってリソースを処理します。ロックは、コンストラクターでそのミューテックスを自動的にバインドし、デストラクタで解放します。これにより、ランタイムがミューテックスを処理するため、デッドロックのリスクが大幅に軽減されます。
ロックは、C ++ 11では2つのフレーバーで使用できます。単純な場合はstd :: lock_guard、高度なユースケースの場合はstd :: unique-lock。

  • ロックは、RAIIイディオムに従ってリソースを処理します。ロックは、コンストラクターでミューテックスを自動的にバインドし、デストラクタで解放します。これにより、ランタイムがミューテックスの処理を担当するため、デッドロックのリスクが大幅に軽減されます。
  • C ++ 11では2つのロックから選択できます。単純なユースケースはstd :: lock_guardであり、高度なユースケースはstd :: unique-lockです。
std :: lock_guard
  • まず、単純なユースケースです。

  • 1つ目は簡単な使い方です。

    mutex m;
    m.lock();
    sharedVariable= getVar();
    m.unlock();
    

コードが非常に少ないため、ミューテックスmは、クリティカルセクションsharedVariable = getVar()へのアクセスがシーケンシャルであることを保証します。シーケンシャルとは、この特殊なケースでは、各スレッドがクリティカルセクションに順番にアクセスすることを意味します。コードは単純ですが、デッドロックが発生しがちです。クリティカルセクションが例外をスローした場合、またはプログラマーが単にミューテックスのロックを解除するのを忘れた場合、デッドロックが発生します。std :: lock_guardを使用すると、これをよりエレガントに行うことができます。

  • コードが非常に少ないため、ミューテックスmは、クリティカルセクションsharedVariable = getVar()へのアクセスがシーケンシャルであることを保証します。この特殊なケースでは、シーケンシャルとは、各スレッドが順番にクリティカルセクションに入ることを意味します。コードは非常に単純ですが、デッドロックが発生する可能性があることが証明されています。クリティカルセクションが例外をスローした場合、またはプログラマーがミューテックスのロックを解除するのを忘れた場合、デッドロックが発生します。std :: lock_guardを使用すると、よりエレガントに実行できます。

    {
          
          
      std::mutex m,
      std::lock_guard<std::mutex> lockGuard(m);
      sharedVariable= getVar();
    }
    

それは簡単でした。しかし、開き括弧と閉じ括弧はどうですか?std :: lock_guardの有効期間は、角かっこ(http://en.cppreference.com/w/cpp/language/scope#Block_scope)によって制限されます。つまり、クリティカルセクションを離れると、その寿命は終了します。まさにその時点で、std :: lock_guardのデストラクタが呼び出され、ミューテックスが解放されます。これは自動的に発生し、さらに、sharedVariable = getVar()のgetVar()が例外をスローした場合に発生します。もちろん、関数本体スコープまたはループスコープもオブジェクトの存続期間を制限します。

  • それは簡単です。しかし、開き角かっことはどういう意味ですか?std :: lock_guardのライフサイクルは、角かっこによって制限されます(http://en.cppreference.com/w/cpp/language/scope#Block_scope)。これは、クリティカルゾーンを離れると、ライフサイクルが終了することを意味します。その時点で、std :: lock_guardのデストラクタが呼び出され、ミューテックスが解放されます。これは自動的に発生し、sharedVariable = getVar()のgetVar()が例外をスローすると、それも発生します。もちろん、関数本体スコープまたはループスコープもオブジェクトのライフサイクルを制限します。
std :: unique_lock

std :: unique_lockは強力ですが、弟のstd :: lock_guardよりも拡張性があります。
std :: unique_lockを使用すると、std :: lock_guardに加えて使用できます。

  • ミューテックスを関連付けずに作成する
  • ロックされた関連ミューテックスなしで作成する
  • 関連するミューテックスのロックを明示的かつ繰り返し設定または解放します
  • ミューテックスを移動する
  • ミューテックスをロックしてみてください
  • 関連するミューテックスの遅延ロック

しかし、なぜそれが必要なのですか?ミューテックスのポストリスクからのデッドロックを覚えていますか?デッドロックの理由は、ミューテックスが異なる順序でロックされていたためです。

  • unique_lockは、兄弟のstd :: lock_guardよりも強力でスケーラブルです。

  • std :: unique_lockを使用すると、std :: lock_guardの機能に加えて、

    • 作成時にミューテックスを関連付ける必要はありません
    • 作成時に関連するミューテックスをロックする必要はありません
    • 関連するミューテックスを明示的かつ繰り返し設定または解放する
    • モバイルミューテックス
    • ミューテックスをロックしてみてください
    • 遅延ロック関連ミューテックス
  • しかし、なぜこれが必要なのですか?以前のミューテックスのリスクのデッドロックを覚えていますか?デッドロックの原因は、ミューテックスが別のスレッドでロックされていることです。

    // deadlock.cpp
    
    #include <iostream>
    #include <chrono>
    #include <mutex>
    #include <thread>
    
    struct CriticalData{
          
          
      std::mutex mut;
    };
    
    void deadLock(CriticalData& a, CriticalData& b){
          
          
    
      a.mut.lock();
      std::cout << "get the first mutex" << std::endl;
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
      b.mut.lock();
      std::cout << "get the second mutex" << std::endl;
      // do something with a and b
      a.mut.unlock();
      b.mut.unlock();
      
    }
    
    int main(){
          
          
    
      CriticalData c1;
      CriticalData c2;
    
      std::thread t1([&]{
          
          deadLock(c1,c2);});
      std::thread t2([&]{
          
          deadLock(c2,c1);});
    
      t1.join();
      t2.join();
    
    }
    

解決策は簡単です。関数のデッドロックは、アトミックな方法でミューテックスをロックする必要があります。それはまさに次の例で起こることです。

  • 解決策は簡単です。デッドロック関数は、ミューテックスをアトミックにロックする必要があります。次の例は次のようなものです。

    // deadlockResolved.cpp
    
    #include <iostream>
    #include <chrono>
    #include <mutex>
    #include <thread>
    
    struct CriticalData{
          
          
      std::mutex mut;
    };
    
    void deadLock(CriticalData& a, CriticalData& b){
          
          
    
      std::unique_lock<std::mutex>guard1(a.mut,std::defer_lock);
      std::cout << "Thread: " << std::this_thread::get_id() << " first mutex" <<  std::endl;
    
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
    
      std::unique_lock<std::mutex>guard2(b.mut,std::defer_lock);
      std::cout << "    Thread: " << std::this_thread::get_id() << " second mutex" <<  std::endl;
    
      std::cout << "        Thread: " << std::this_thread::get_id() << " get both mutex" << std::endl;
      std::lock(guard1,guard2);
      // do something with a and b
    }
    
    int main(){
          
          
    
      std::cout << std::endl;
    
      CriticalData c1;
      CriticalData c2;
    
      std::thread t1([&]{
          
          deadLock(c1,c2);});
      std::thread t2([&]{
          
          deadLock(c2,c1);});
    
      t1.join();
      t2.join();
    
      std::cout << std::endl;
    
    }
    

引数std :: defer_lockを指定してstd :: unique_lockのコンストラクターを呼び出す場合、ロックは自動的にロックされません。これは14行目と19行目で発生します。ロック操作は23行目で可変個引数テンプレートstd :: lockを使用してアトミックに実行されます。可変個引数テンプレートは、任意の数の引数を受け入れることができるテンプレートです。ここで、引数はロックです。std :: lockは、アトミックステップですべてのロックを取得しようとします。それで、彼は失敗するか、それらすべてを手に入れます。

  • パラメータstd :: defer_lockを指定してstd :: unique_lockのコンストラクタを呼び出すと、ロックは自動的にロックされません。14行目と19行目で発生します。可変パラメータテンプレートstd :: lockを使用することにより、ロック操作は23行目でアトミックに実行されます。可変個引数テンプレートは、任意の数のパラメーターを受け入れることができるテンプレートです。ここでのパラメータはlockです。std :: lockは、1つのアトミックステップですべてのロックを取得しようとします。それで、彼は失敗したか、それを手に入れました。

この例では、std :: unique_lockがリソースの有効期間を処理し、std :: lockは関連するミューテックスをロックします。しかし、あなたはそれを逆にすることができます。最初のステップではミューテックスをロックし、2番目のステップではstd :: unique_lockがリソースの存続期間を処理します。これが2番目のアプローチのスケッチです。

  • この例では、std :: unique_lockがリソースのライフサイクルを担当し、std :: lockが関連するミューテックスをロックします。ただし、その逆は可能です。最初のステップでミューテックスをロックし、2番目のステップでstd :: unique_lockがリソースのライフサイクルを処理します。以下は、2番目の方法の概要です。

    std::lock(a.mut, b.mut);
    std::lock_guard<std::mutex> guard1(a.mut, std::adopt_lock);
    std::lock_guard<std::mutex> guard2(b.mut, std::adopt_lock);
    

今、すべてが大丈夫です。プログラムはデッドロックなしで実行されます。

  • 今ではすべてが順調です。プログラムの実行中にデッドロックは発生しません。
    ここに画像の説明を挿入
補足:特別なデッドロック

ミューテックスだけがデッドロックを引き起こすことができるというのは幻想です。スレッドがリソースを保持している間、スレッドがリソースを待機する必要があるたびに、デッドロックが近くに潜んでいます。
スレッドでさえリソースです。

  • ミューテックスだけがデッドロックを引き起こすことができると考えるのは幻想です。スレッドがリソースを待機する必要があり、現在リソースを保持している場合は常に、デッドロックが近くに潜んでいます。
  • スレッドもリソースです。
    // blockJoin.cpp
    
    #include <iostream>
    #include <mutex>
    #include <thread>
    
    std::mutex coutMutex;
    
    int main(){
          
          
    
      std::thread t([]{
          
          
        std::cout << "Still waiting ..." << std::endl;
        std::lock_guard<std::mutex> lockGuard(coutMutex);
        std::cout << std::this_thread::get_id() << std::endl;
        }
      );
    
      {
          
          
        std::lock_guard<std::mutex> lockGuard(coutMutex);
        std::cout << std::this_thread::get_id() << std::endl;
        t.join();
      }
    
    }
    

プログラムはすぐに停止します。

  • プログラムはすぐに動きを止めました。
    ここに画像の説明を挿入

何が起こっていますか?出力ストリームstd :: coutのロックと、メインスレッドの子tの待機が、デッドロックの原因です。出力を観察することで、ステートメントが実行される順序を簡単に確認できます。

  • 何が起こったのですか?出力ストリームstd :: coutのロックと、メインスレッドがその子スレッドtを待機していることが、デッドロックの原因です。出力を観察することで、ステートメントの実行順序を簡単に確認できます。

最初のステップでは、メインスレッドは19行目から21行目を実行します。メインスレッドは、子tがワークパッケージで完了するまで、t.join()呼び出しを使用して21行目で待機します。メインスレッドは、出力ストリームをロックしている間、待機しています。しかし、それはまさに子供が待っているリソースです。このデッドロックを解決する2つの方法が思い浮かびます。

  • 最初のステップでは、メインスレッドが19〜21行目を実行します。21行目でt.join()を呼び出し、子スレッドtが作業を完了するまで待機します。メインスレッドは待機中であり、出力ストリームをロックしています。しかし、これは子スレッドが待機しているリソースです。このデッドロックを解決するには2つの方法があります。

    • メインスレッドは、t.join()の呼び出し後に、出力ストリームstd :: coutをロックします。
    • メインスレッドは、t.join()を呼び出した後、出力ストリームstd :: coutをロックします。
      {
              
              
        t.join();
        std::lock_guard<std::mutex> lockGuard(coutMutex);
        std::cout << std::this_thread::get_id() << std::endl;
      }
      
    • メインスレッドは、追加のスコープによってロックを解放します。これは、t.join()呼び出しの前に行われます。
    • メインスレッドは、追加のスコープを介してロックを解放します。これは、t.join()を呼び出す前に行われます。

      {
              
              
        {
              
              
          std::lock_guard<std::mutex> lockGuard(coutMutex);
          std::cout << std::this_thread::get_id() << std::endl;
        }
        t.join();
      }
      

次は何ですか?

では次の投稿私は、リーダライタロックについて話しましょう。リーダーライターロックは、C ++ 14以降、スレッドの読み取りと書き込みを区別するための機能を提供します。したがって、任意の数の読み取りスレッドが同時に共有変数にアクセスできるため、共有変数での競合が軽減されます。

  • では次の記事、私は読み書きロックについて説明します。C ++ 14以降、読み取り/書き込みロックを使用すると、リーダースレッドとライタースレッドを区別できます。したがって、任意の数のリーダースレッドが同時に共有変数にアクセスできるため、共有変数の競合が減少します。

おすすめ

転載: blog.csdn.net/luoshabugui/article/details/110429770