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_lock
とlock_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_variable
std::mutex
notify_one()
wait()
wait()
notify_one()
wait
wait
notify_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つの考慮事項があります。
- では
function_2
、キューが空であるかどうかを判断するときに、while(q.empty())
代わりに使用されますif(q.empty())
。これはwait()
、ブロックからリターンまで、必ずしもnotify_one()
関数が原因であるとは限らず、システムの不確実な理由(おそらく条件変数を使用)によってウェイクアップされる可能性があるためです。 )の実装メカニズムに関連しており、そのタイミングと頻度は不確実であり、疑似ウェイクアップと呼ばれます。間違ったときにウェイクアップすると、次のステートメントの実行が間違ってしまうため、次のことを行う必要があります。キューが再び空であるかどうかを判断します。それでも空の場合は、wait()
ブロックを続けます。 - ミューテックスを管理する場合は、
std::unique_lock
代わりstd::lock_guard
に使用され、実際には使用できません。関数の機能std::lock_guard
を説明する必要wait()
があります。ご覧のとおり、wait()
関数の前はミューテックスが保護されていますwait
が、その時点で何もしていなければ、ミューテックスは常に保持されているのではないでしょうか。プロデューサーはスタックしたままになり、データをキューに入れることができません。したがって、このwait()
関数は最初unlock()
にミューテックスロック関数を呼び出し、次にそれ自体をスリープ状態にします。起動後、関数はロックを保持し続け、後続のキュー操作を保護します。そして、インターフェースはありlock_guard
ませんlock
、そしてそれは提供します。これが、使用する必要がある理由です。unlock
unique_lock
unique_lock
- ロックの範囲を最小限に抑えるために、きめ細かいロックを使用します。その
notify_one()
時点では、ミューテックスの保護範囲内にある必要はないため、条件変数がウェイクアップされる前にロックをロックできますunlock()
。
あなたもできるcond.wait(locker);
ライティングの方法を変更する。wait()
2番目のパラメータはチェック条件を示すために、関数に渡すことができます。lambda
機能が最も簡単です、ここ。関数から復帰した場合true
、wait()
関数はブロックせずに直接返します。関数が戻った場合はfalse
、wait()
関数はウェイクアップの待機をブロックし、疑似ウェイクアップされた場合、関数の戻り値を判断し続けます。
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
。