C++11スレッド同期の使用について

15064 ワード

反発量と条件変数は、マルチスレッドが同時にアクセスする共有データを保護するために、スレッドの同期を制御する一般的な手段である.c++11はこれらの操作を提供するとともに、原子変数と1回の呼び出しの操作も提供し、非常に便利である.ここではC++でこれらの同期機構をどのように使用するかだけを紹介し,概念に関する紹介はここではあまり述べない.
はんぱつりょう
C++11では、次の4つの意味の反発量(mutex)が提供されています.
  • std::mutex:排他的反発量で再帰的に使用できません.
  • std::timed_mutex:タイムアウト付きの排他的反発量で、再帰的に使用できません.
  • std::recursive_mutex:再帰反発量、タイムアウト機能なし.
  • std::recursive_timed_mutex:タイムアウト付き再帰反発量.

  • 排他反発量std::mutex
    これらの反発量の基本インタフェースは類似しており,反発量の所有権が得られるまでlock()法によりスレッドをブロックするのが一般的である.スレッドが反発量を取得し、タスクを完了した後、unlock()を使用して反発量の占有を解除する必要があります.lock()とunlock()はペアで表示する必要があります.try_lock()は反発量をロックしようとし、成功すればtrueを返し、失敗すればfalseを返し、ブロックされていない.std::mutexの基本的な使い方コードは以下の通りです.
    #include
    #include
    #include
    #include
    using namespace std;
    
    
    std::mutex g_lock;
    
    void func()
    {
        g_lock.lock();
    
        std::cout<<"entered thread "<<std::this_thread::get_id()<<std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout<<"leaving thread "<<std::this_thread::get_id()<<std::endl;
    
        g_lock.unlock();
    }  
    
    int main()
    {
        std::thread t1(func);
        std::thread t2(func);
        std::thread t3(func);
    
        t1.join();
        t2.join();
        t3.join();
    
        return 0;
    }
    
    

    lock_の使用guardはlock/unlockの書き方を簡略化し、lock_guardは構造時に反発量を自動的にロックし、役割ドメインを終了した後に析構を行うと自動的にロックを解除し、反発量の正確な操作を保証し、unlock操作を忘れないようにするため、できるだけlokc_guard.lock_guardはRAII技術を用い,クラスの構造関数にリソースを割り当て,構造関数にリソースを解放し,役割ドメインが発生した後にリソースを解放することを保証する.上の例ではlock_を使用しますguardの後はより簡潔になります.コードは次のとおりです.
    void func()
    {
        lock_guard<std::mutex> locker(g_lock);   //          
        cout<<"entered thread "<<this_thread::get_id()<this_thread::sleep_for(std::chrono::seconds(1));
        cout<<"leaving thread "<<std::this_thread::get_id()<

    再帰的排他反発量std::recursive_mutex
    再帰ロックでは、同じスレッドがこの反発ロックを複数回取得することができ、同じスレッドが反発量を複数回取得する必要がある場合のデッドロックの問題を解決するために使用できます.1つのスレッドが同じ反発量を複数回取得するとデッドロックが発生します.このデッドロックの問題を解決するには、再帰ロック:std::recursive_を使用します.mutexは、同じスレッドで反発量を複数回得ることができます.コードの例:
    #include
    #include
    #include
    using namespace std;
    
    
    struct Complex
    {
        std::recursive_mutex mutex;
        int i;
    
        Complex():i(0){}
    
        void mul(int x)
        {
            std::lock_guard<std::recursive_mutex> lock(mutex);
            i*=x;
        }
        void div(int x)
        {
            std::lock_guard<std::recursive_mutex> lock(mutex);
            i/=x;
    
        }
        void both(int x,int y)
        {
            std::lock_guard<std::recursive_mutex> lock(mutex);
            mul(x);
            div(y);
        }
    };
    
    int main()
    {
        Complex complex;
        complex.both(32,23);  //                 ,      
    
        return 0;
    }
    

    注意しなければならないのは、できるだけ再帰ロックを使用しないことです.主な原因は以下の通りです.
  • は、再帰ロックを必要とするマルチスレッド反発処理自体が簡略化され、再帰反発が複雑な論理生成を放縦にしやすくなり、いくつかのマルチスレッド同期による難解な問題を引き起こすことが多い.
  • 再帰ロックは非再帰ロックよりも効率が低い.
  • 再帰ロックは、同じスレッドで同じ反発量を複数回取得することを許可するが、反復可能な最大回数は具体的には説明されず、一定回数を超えるとlockを呼び出すとstd::systemエラーが放出される.

  • タイムアウトを伴う反発量
    std::timed_mutexはタイムアウトの独占ロック、std::recursive_timed_mutexはタイムアウトの再帰ロックであり、主にロックを取得する際にタイムアウト待機機能に用いられる.ロックを取得するのにどのくらいかかるか分からない場合があるため、反発量を取得するのを待つことがないように、待機タイムアウト時間を設定し、タイムアウト後に他のことをすることもできる.std::timed_mutexはstd::mutexより2つのタイムアウト取得ロックのインタフェースを多くしました:try_lock_forとtry_lock_until、この2つのインタフェースは、反発量を取得するタイムアウト時間を設定するために使用されます.std::timed_mutexの基本的な使い方は次のコードです.
    
    #include<iostream>
    #include<thread>
    #include<mutex>
    using namespace std;
    
    std::timed_mutex mutex1;
    
    void work()
    {
        chrono::milliseconds timeout(100);
    
        while(true)
        {
            if(mutex1.try_lock_for(timeout))
            {
                cout<<this_thread::get_id()<<":do work with the mutex"<<endl;
                chrono::milliseconds sleepDuration(250);
                this_thread::sleep_for(sleepDuration);
    
                mutex1.unlock();
                this_thread::sleep_for(sleepDuration);
    
            }
            else
            {
                cout<<this_thread::get_id()<<": do work without mutex"<<endl;
    
                chrono::milliseconds sleepDuration(100);
                this_thread::sleep_for(sleepDuration);
            }
        }
    }
    
    
    int main()
    {
        thread t1(work);
        thread t2(work);
    
        t1.join();
        t2.join();
    
        return 0;
    }
    
    

    上記の例では、whileサイクルによってタイムアウトロックを取得し続け、タイムアウトがまだロックを取得していない場合は100ミリ秒休止し、タイムアウトロックの取得を継続する.比較std::timed_mutex,std::recursive_timed_mutexは再帰ロックの機能が多く、同じスレッドで複数回反発量を得ることができます.
    じょうけんへんすう
    条件変数は、C++11が提供する別の待機同期メカニズムであり、1つ以上のスレッドをブロックし、別のスレッドから通知またはタイムアウトが受信されるまで、現在のブロックされているスレッドを呼び覚ますことができます.条件変数は反発量と組み合わせて使用する必要がある.C++11は、2つの条件変数を提供します.
  • condition_variable,配合std::unique_lock<:mutex>はwait操作を行います.
  • condition_variable_anyは、lock、unlockの意味を持つ任意のmutexと組み合わせて使用し、比較的柔軟ですが、conditionよりも効率的です.variableは少し悪いです.

  • condition_が表示されますvariable_any比condition_variableはもっと柔軟で、それはもっと通用するため、すべてのロックに適用して、condition_variableはパフォーマンスが優れています.
    条件変数の使用手順は次のとおりです.
  • 条件変数を持つスレッドは反発量を取得する.
  • ある条件を循環検査し、条件が満たされなければ、条件が満たされるまでブロックする.条件が満たされている場合は、下に進みます.
  • スレッドが条件を満たして実行された後にnotifyを呼び出すoneまたはnotify_allは1つまたはすべての待機スレッドを起動します.

  • 古典的な生産者-消費者の例を見ることができます.
    #include
    #include
    #include
    #include
    #include
    using namespace std;
    
    
    mutex m_mutex;   //   
    int s=0;         //    
    condition_variable m_notempty;   //    
    
    void producer()     //   
    {
        while(1)
        {
            sleep(1);
            m_mutex.lock();  //  
            s++;
            cout<<"increase one ,s="<void consumer()     //   
    {
        while(1)
        { 
            sleep(1);
            unique_lock locker(m_mutex);  
            while(s==0)
            m_notempty.wait(locker);      //      
            s--;
            cout<<"decrease one,s="<int main()
    {
        //    
        thread thread1(producer);
        thread thread2(consumer);
    
    
        thread1.join();
        thread2.join();
    }
    

    上記の例では、while(s==0) m_notempty.wait(locker);、このコードは、sが0に等しいとき、条件が満たされるまでブロックされることを意味する.私たちもこのように使うことができて、m_notempty.wait(locker,[]{return s>0;});、判断条件を関数の中に置いて、waitがずっとブロックしていることを意味して、判断条件が満たすことを知っている時、呼び覚まされます.
    げんしへんすう
    C++11は原子型std::atomicを提供し、任意のタイプをテンプレートパラメータとして使用することができ、C++11は整数型の原子変数を内蔵し、原子変数をより便利に使用することができ、原子変数を使用すると反発量を使用してこの変数を保護する必要はありません.この変数の操作は原子であることを保証し、中断できないからです.もっと簡潔に使います.タイマーを作るにはmutexを使用すると、コードは次のようになります.
    
    #include
    #include
    using namespace std;
    
    
    
    struct Counter
    {
        public:
        int value;
        std::mutex m_mutex;
    
        void increment()
        {
            std::lock_guard<std::mutex> lock(mutex);
            value++;
        }
    
        void decrement()
        {
            std::lock_guard<std::mutex> lock(mutex);
            value--;
        }
    
        int get()
        {
            return value;
        }
    };
    
    

    原子変数を使用すると、反発量を定義する必要がなくなり、より簡単に使用できます.
    #include
    #include
    using namespace std;
    
    
    
    struct Counter
    {
        public:
    
        std::atomic<int> value;
    
        void increment()
        {
            value++;
        }
    
        void decrement()
        {
            value--;
        }
    
        int get()
        {
            return value;
        }
    };
    
    

    call_one/once_flagの使用
    マルチスレッド環境で関数が1回のみ呼び出されることを保証するために、たとえばオブジェクトを初期化する必要があり、このオブジェクトが1回しか初期化できない場合、std::call_onceは、関数がマルチスレッド環境で1回しか呼び出されないことを保証します.std::call_を使用するonceの場合、once_が必要です.flagをcallとしてoneの入参は、使い方が簡単です.
    #include
    #include
    #include
    using namespace std;
    
    
    std::once_flag flag;
    
    void do_once()
    {
        std::call_once(flag,[]{std::cout<<"Called once"<int main()
    {
        std::thread t1(do_once);
        std::thread t2(do_once);
        std::thread t3(do_once);
    
        t1.join();
        t2.join();
        t3.join();
    }
    

    実行結果:
    Called once