C ++ 11マルチスレッドプログラミング-mutex、unique_lockおよび条件変数(条件変数)

mutex和unique_lock:

 

ミューテックスロックはスレッド間の同期を保証しますが、並列操作をシリアル操作に変換します。これはパフォーマンスに大きな影響を与えるため、ロック領域を可能な限り減らす必要があります。つまり、きめ細かいロックを使用します

これはlock_guard十分に行われておらず、十分な柔軟性がありません。これlock_guardは、解体中にロック解除操作が実行されることを保証することしかできlock_guardません。それ自体をロックおよびロック解除するためのインターフェイスを提供しませんが、そのようなニーズがある場合があります。以下の例を見てください。

class LogFile {
    std::mutex _mu;
    ofstream f;
public:
    LogFile() {
        f.open("log.txt");
    }
    ~LogFile() {
        f.close();
    }
    void shared_print(string msg, int id) {
        {
            std::lock_guard<std::mutex> guard(_mu);
            //do something 1
        }
        //do something 2
        {
            std::lock_guard<std::mutex> guard(_mu);
            // do something 3
            f << msg << id << endl;
            cout << msg << id << endl;
        }
    }

};

上記のコードでは、保護する必要のある関数内に2つのコードがあります。現時点でlock_guardは、同じミューテックスを管理するために2つのローカルオブジェクトを作成する必要があります(実際には、1つしか作成できませんが、ロックが強すぎて効率が良くない。)、修正方法はを使用することunique_lockです。それが提供するlock()unlock()、それが現在ロックまたはロック解除されているかどうかを記録することができるインターフェースを、それが破壊されると、それは(現在の状態に応じてロックを解除するかどうかを決定するlock_guardことが確実にロック解除されます)。上記のコードは次のように変更されています。

class LogFile {
    std::mutex _mu;
    ofstream f;
public:
    LogFile() {
        f.open("log.txt");
    }
    ~LogFile() {
        f.close();
    }
    void shared_print(string msg, int id) {

        std::unique_lock<std::mutex> guard(_mu);
        //do something 1
        guard.unlock(); //临时解锁

        //do something 2

        guard.lock(); //继续上锁
        // do something 3
        f << msg << id << endl;
        cout << msg << id << endl;
        // 结束时析构guard会临时解锁
        // 这句话可要可不要,不写,析构的时候也会自动执行
        // guard.ulock();
    }

};

上記のコードからわかるように、操作をロックする必要がない場合は一時的にロックを解除し、保護を継続する必要がある場合はロックを継続できるため、繰り返し行う必要はありません。lock_guardオブジェクトをインスタンス化すると、ロック領域を減らすこともできます。同様に、このstd::defer_lock設定を使用して、デフォルトのロック操作を実行せずに初期化できます。

void shared_print(string msg, int id) {
    std::unique_lock<std::mutex> guard(_mu, std::defer_lock);
    //do something 1

    guard.lock();
    // do something protected
    guard.unlock(); //临时解锁

    //do something 2

    guard.lock(); //继续上锁
    // do something 3
    f << msg << id << endl;
    cout << msg << id << endl;
    // 结束时析构guard会临时解锁
}

このようlock_guard使用する方が柔軟性がありますこれも、内部でロックの状態を維持する必要があるため、効率がlock_guard低下するため、コストがかかります。lock_guard問題が解決できる場合は使用されlock_guard、その逆も同様unique_lockです。

条件変数の学習の背後には、がunique_lock入ります。

また、そのノートを喜ばunique_locklock_guard、コピーすることはできませんlock_guard移動することはできませんが、unique_lockすることができます!

// unique_lock 可以移动,不能复制
std::unique_lock<std::mutex> guard1(_mu);
std::unique_lock<std::mutex> guard2 = guard1;  // error
std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok

// lock_guard 不能移动,不能复制
std::lock_guard<std::mutex> guard1(_mu);
std::lock_guard<std::mutex> guard2 = guard1;  // error
std::lock_guard<std::mutex> guard2 = std::move(guard1); // error

 

 

 

 

================================================== ============

================================================== ============

 

 

 

 

条件変数:

 

 

ミューテックスロックstd::mutexは、スレッド間の同期の最も一般的な手段の1つですが、場合によってはあまり効率的ではありません。

単純な消費者プロデューサーモデルを実装する場合、1つのスレッドがデータをキューに入れ、1つのスレッドがキューからデータをフェッチするとします。データをフェッチする前に、キューにデータがあるかどうかを判断する必要があります。このキューはしたがって、保護のためにミューテックスロックを使用する必要があります。1つのスレッドがデータをキューに追加すると、別のスレッドはそのデータをフェッチできなくなります。その逆も同様です。ミューテックスロックの実装は次のとおりです。

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>

std::deque<int> q;
std::mutex mu;

void function_1() {
    int count = 10;
    while (count > 0) {
        std::unique_lock<std::mutex> locker(mu);
        q.push_front(count);
        locker.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        count--;
    }
}

void function_2() {
    int data = 0;
    while ( data != 1) {
        std::unique_lock<std::mutex> locker(mu);
        if (!q.empty()) {
            data = q.back();
            q.pop_back();
            locker.unlock();
            std::cout << "t2 got a value from t1: " << data << std::endl;
        } else {
            locker.unlock();
        }
    }
}
int main() {
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();
    return 0;
}

//输出结果
//t2 got a value from t1: 10
//t2 got a value from t1: 9
//t2 got a value from t1: 8
//t2 got a value from t1: 7
//t2 got a value from t1: 6
//t2 got a value from t1: 5
//t2 got a value from t1: 4
//t2 got a value from t1: 3
//t2 got a value from t1: 2
//t2 got a value from t1: 1

ミューテックスは実際にこのタスクを実行できることがわかりますが、パフォーマンスの問題があります。

まず、function_1関数はプロデューサーです。生産プロセスでstd::this_thread::sleep_for(std::chrono::seconds(1));は遅延を意味する1sため、生産プロセスは非常に遅くなります。function_2関数はコンシューマーであり、whileループがあり、終了を受信したときにのみ停止します。 data。、ループ内でロックが追加されるたびに、キューは空ではないと判断され、次に番号が取り出され、最後にロックが解除されます。だから、1s内部では、私は多くの無駄な努力をしています!この場合、CPUの占有率は非常に高くなり、100%(シングルコア)に達する可能性があります。図に示すように:

解決策の1つは、コンシューマーにわずかな遅延を追加することです。判断の結果、キューが空であることが判明した場合は500ms、CPU使用率を減らすために、自分を罰して遅延させます

void function_2() {
    int data = 0;
    while ( data != 1) {
        std::unique_lock<std::mutex> locker(mu);
        if (!q.empty()) {
            data = q.back();
            q.pop_back();
            locker.unlock();
            std::cout << "t2 got a value from t1: " << data << std::endl;
        } else {
            locker.unlock();
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
        }
    }
}

図に示すように:

次に、この遅延時間を決定する方法に問題があります。プロデューサーが高速で生成するが、コンシューマーが遅延する500ms場合、それはあまり良くありません。プロデューサーが低速で生成する場合500msでも、コンシューマーの遅延は不要です。CPUを占有します。

これにより、条件変数(条件変数)が生成されます。これ、組み合わせで使用できるヘッダーファイルc++11を提供し、2つの重要なインターフェイスがありコンシューマープロデューサーモデルでスレッドをスリープ状態にすることができます。プロデューサーがキューに何もないことを発見した場合、彼は自分自身をスリープ状態にすることはできますプロセス中の条件変数の1つウェイクアップすることである仕事から離れることできません(おそらく多くの条件変数はその時の状態)。それで、いつそれを使用するのが良いですか?もちろん、プロデューサーがデータをキューに入れる時が来ました。キューにデータがある場合は、待機中のスレッドをすばやく起動して動作させることができます。#include <condition_variable>std::condition_variablestd::mutexnotify_one()wait()wait()notify_one()waitwaitnotify_one()

変更された条件変数は次のとおりです。

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>

std::deque<int> q;
std::mutex mu;
std::condition_variable cond;

void function_1() {
    int count = 10;
    while (count > 0) {
        std::unique_lock<std::mutex> locker(mu);
        q.push_front(count);
        locker.unlock();
        cond.notify_one();  // Notify one waiting thread, if there is one.
        std::this_thread::sleep_for(std::chrono::seconds(1));
        count--;
    }
}

void function_2() {
    int data = 0;
    while ( data != 1) {
        std::unique_lock<std::mutex> locker(mu);
        while(q.empty())
            cond.wait(locker); // Unlock mu and wait to be notified
        data = q.back();
        q.pop_back();
        locker.unlock();
        std::cout << "t2 got a value from t1: " << data << std::endl;
    }
}
int main() {
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();
    return 0;
}

現時点では、CPU使用率も非常に低くなっています。

上記のコードには3つの考慮事項があります。

  1. ではfunction_2、キューが空であるかどうかを判断するときに、while(q.empty())代わりに使用されますif(q.empty())。これはwait()、ブロックからリターンまで、必ずしもnotify_one()関数が原因であるとは限らず、システムの不確実な理由(おそらく条件変数を使用)によってウェイクアップされる可能性があるためです。 )の実装メカニズムに関連しており、そのタイミングと頻度は不確実であり、疑似ウェイクアップと呼ばれます。間違ったときにウェイクアップすると、次のステートメントの実行が間違ってしまうため、次のことを行う必要があります。キューが再び空であるかどうかを判断します。それでも空の場合は、wait()ブロックを続けます。
  2. ミューテックスを管理する場合は、std::unique_lock代わりstd::lock_guard使用され、実際には使用できません。関数の機能std::lock_guardを説明する必要wait()があります。ご覧のとおり、wait()関数の前はミューテックスが保護されていますwaitが、その時点で何もしていなければ、ミューテックスは常に保持されているのではないでしょうか。プロデューサーはスタックしたままになり、データをキューに入れることができません。したがって、このwait()関数は最初unlock()ミューテックスロック関数を呼び出し、次にそれ自体をスリープ状態にします。起動後、関数はロックを保持し続け、後続のキュー操作を保護します。そしてインターフェースはありlock_guardませんlockそしてそれは提供します。これが、使用する必要がある理由です。unlockunique_lockunique_lock
  3. ロックの範囲を最小限に抑えるために、きめ細かいロックを使用します。そのnotify_one()時点では、ミューテックスの保護範囲内にある必要はないため、条件変数がウェイクアップされる前にロックをロックできますunlock()

あなたもできるcond.wait(locker);ライティングの方法を変更する。wait()2番目のパラメータはチェック条件を示すために、関数に渡すことができます。lambda機能が最も簡単です、ここ。関数から復帰した場合truewait()関数はブロックせずに直接返します。関数が戻った場合はfalsewait()関数はウェイクアップの待機をブロックし、疑似ウェイクアップされた場合、関数の戻り値を判断し続けます。

void function_2() {
    int data = 0;
    while ( data != 1) {
        std::unique_lock<std::mutex> locker(mu);
        cond.wait(locker, [](){ return !q.empty();} );  // Unlock mu and wait to be notified
        data = q.back();
        q.pop_back();
        locker.unlock();
        std::cout << "t2 got a value from t1: " << data << std::endl;
    }
}

notify_one()関数に加えて状態内のすべての条件変数を同時にウェイクアップできる関数c++も提供さnotify_all()れますwait

参照

  1. C ++並行プログラミングの練習
  2. C ++スレッド#6:条件変数

おすすめ

転載: blog.csdn.net/yxpandjay/article/details/109302863