C++11 マルチスレッド プログラミング 1: マルチスレッドの概要
C++11 マルチスレッド プログラミング 2: マルチスレッド通信、スレッド同期、ロック
C++11 マルチスレッド プログラミング 3: ロック リソース管理と条件変数
C/C++ の基礎、ブースト スレッドの作成、スレッドの同期
3.1 RAii管理ミューテックスリソースロック自動解放を手動で実装する
自分で書いたコードは基本的にロックと解除が勝手に行われますが、特に解除を忘れやすくデッドロック(解除されていないロックと捉えることもできます)が発生します。RAiiリソースの取得と初期化と呼ばれるこのようなテクノロジでは、ローカル オブジェクトを使用してリソースを管理することが特徴です。ローカル オブジェクトがスタックからポップアウトされると解放されることは誰もが知っています。コード中括弧のペアは変数とオブジェクトの内部で生成されます。中括弧のペアが外されると、それらは解放されます。クラスのオブジェクトの場合は、デストラクターを呼び出して解放します。コードのこの部分は と呼ばれます。スタック内に生成されるスペース。そのため、ランタイムはオペレーティング システムによって維持されます。
ローカル オブジェクトを使用してリソースを管理するテクノロジは、リソースの取得と初期化と呼ばれます。そのライフ サイクルは、手動介入なしでオペレーティング システムによって管理されます。リソースの破棄は忘れられやすいため、デッドロックやメモリ リークが発生します。
#include <thread>
#include <iostream>
#include <string>
#include <mutex>
#include <shared_mutex>
//Linux -lpthread
using namespace std;
// RAII 这个是把上锁和解锁的过程放在类当中
class XMutex
{
public:
XMutex(mutex& mux) :mux_(mux)
{
cout << "Lock" << endl;
mux.lock();
}
~XMutex()
{
cout << "Unlock" << endl;
mux_.unlock();
}
private:
mutex& mux_;
};
static mutex mux;
void TestMutex(int status)
{
XMutex lock(mux);
if (status == 1)
{
cout << "=1" << endl;
return;
}
else
{
cout << "!=1" << endl;
return;
}
}
int main(int argc, char* argv[])
{
TestMutex(1);
TestMutex(2);
getchar();
return 0;
}
クラス名の後には lock(mux) が続きます。Lock は実際にはオブジェクトであり、実際のパラメータはパラメータ化されたコンストラクタに渡されます。
3.2 C++11 に付属する RAI 制御ロック ロック ガード
#include <thread>
#include <iostream>
#include <string>
#include <mutex>
#include <shared_mutex>
//Linux -lpthread
using namespace std;
static mutex gmutex; //这里可以是任意的互斥变量类型,只要里面包含lock函数的都可以
void TestLockGuard(int i)
{
gmutex.lock();
{
//假如外部已经有锁,那就不lock
lock_guard<mutex> lock(gmutex, adopt_lock);
//出了大括号,调用析构函数,释放锁
}
{
lock_guard<mutex> lock(gmutex);
cout << "begin thread " << i << endl;
}
for (;;)
{
{
lock_guard<mutex> lock(gmutex);
cout << "In " << i << endl;
}
this_thread::sleep_for(500ms);
}
}
int main(int argc, char* argv[])
{
for (int i = 0; i < 3; i++)
{
thread th(TestLockGuard, i + 1);
th.detach();
}
getchar();
return 0;
}
3.3 unique_lock はタイムアウトを制御するミューテックスを一時的にロック解除できます
実際の需要にはいくつかの状況が考えられます。つまり、モバイル割り当て、つまり、オブジェクト割り当てのプロセスでロックを別のロックに割り当てることが考えられます。この転送をサポートしたい場合は、次を使用します。 unique_lock、これはロック管理用の別のツールです。以前は中括弧を使用して制御していました。コード内で、ビジネス ロジックのニーズにより、最初にロックを解除してから再度ロックする必要がある場合は、提供されているインターフェイスを使用できます。手動でロックが解除され、ロックが追加され、最後にデストラクタによってロックが解放されます。より複雑な状況もサポートしています。unique_lock C++11 は、取り外し可能な
ミューテックス所有権ラッパーを実装しています。
ロックの一時的な解放をサポートしています。unlock は、adopt_lock
(すでにロックを所有しています。ロックしない場合、スタック領域は解放されます)
defer_lock をサポート(遅延所有、ロックなし、スタック領域は解放されません)
try_to_lock をサポート ブロックせずにミューテックスの所有権の取得を試みます。取得に失敗した場合、スタック領域から抜け出すことはありませんが、owns_lock() 関数によって判断されます
タイムアウトパラメータをサポートします タイムアウト後にロックは所有されなくなります
後でロック リソースを解放するときは、まずロック リソースを所有しているかどうかを判断します。コードは次のとおりです。
ソース コードをロックしてみます。
赤枠のとおり、try_lock()関数はTRUEかFALSEを返すので、上の2つと同じになります。
#include <thread>
#include <iostream>
#include <string>
#include <mutex>
#include <shared_mutex>
//Linux -lpthread
using namespace std;
int main(int argc, char* argv[])
{
{
static mutex mux;
{
unique_lock<mutex> lock(mux);//创建lock对象的时候就已经加锁了
lock.unlock();//可以临时释放锁
lock.lock(); //也可以临时加锁
}
{
//已经拥有锁 就不再锁定了,退出栈区解锁
mux.lock();
unique_lock<mutex> lock(mux, adopt_lock);
}
{
//延后加锁 不拥有 退出栈区不解锁
unique_lock<mutex> lock(mux, defer_lock);
//后面需要我们主动的去加锁 退出栈区解锁
lock.lock();
}
{
//mux.lock();
//尝试加锁 不阻塞 失败不拥有锁(退出栈区不解锁) 成功的话就拥有锁
unique_lock<mutex> lock(mux, try_to_lock);
if (lock.owns_lock())
{
cout << "owns_lock" << endl;
}
else
{
cout << "not owns_lock" << endl;
}
}
}
return 0;
}
3.4 C++14 共有ロック共有ロック ラッパー
#include <thread>
#include <iostream>
#include <string>
#include <mutex>
#include <shared_mutex>
//Linux -lpthread
using namespace std;
int main(int argc, char* argv[])
{
{
//共享锁
static shared_timed_mutex tmux;
//读取锁 共享锁
{
shared_lock<shared_timed_mutex> lock(tmux);//这行代码调用共享锁
cout << "read data" << endl;
//退出栈区 释放共享锁
}
//写入锁 互斥锁
{
unique_lock<shared_timed_mutex> lock(tmux);
cout << "write data" << endl;
}
}
getchar();
return 0;
}
3.5 C++17scoped_lock はインターロックによって引き起こされるデッドロックを解決します
このラッパーは C++17 でサポートされています。C++17
デッドロック問題を設定することを忘れないでください。以下の図に示すように、スレッド A はリソース 2 を保持し、スレッド B はリソース 1 を保持します。それぞれのロックはロックが解除されておらず、ロックが適用されます。ロック リソースを取得できないため、2 つのスレッドはお互いのリソースを同時に待機し、デッドロック状態になります。
シミュレートされたデッドロック
#include <thread>
#include <iostream>
#include <string>
#include <mutex>
#include <shared_mutex>
//Linux make: g++ -std=c++14 -o main main8.c -lpthread
using namespace std;
static mutex mux1;
static mutex mux2;
void TestScope1(){
//模拟死锁 停100ms等另一个线程锁mux2
//这里只是使用sleep函数模拟业务逻辑,真正的业务当中不应该有sleep,因为这个是占用CPU资源的。
this_thread::sleep_for(100ms);
cout << this_thread::get_id() << " TestScope1 mux1 lock" << endl;
mux1.lock();
cout << this_thread::get_id() << " begin mux2 lock" << endl;
mux2.lock(); //死锁
cout << "TestScope1" << endl;
this_thread::sleep_for(1000ms);
mux1.unlock();
mux2.unlock();
}
//上锁的顺序:2->1->2->1
void TestScope2(){
cout << this_thread::get_id() << " TestScope2 mux2 lock" << endl;
mux2.lock();
this_thread::sleep_for(500ms);
cout << this_thread::get_id() << " begin mux1 lock" << endl;
mux1.lock();//死锁
cout << "TestScope2" << endl;
this_thread::sleep_for(1500ms);
mux1.unlock();
mux2.unlock();
}
int main(int argc, char* argv[]){ //演示死锁情况
{
{
thread th(TestScope1);
th.detach();
}
{
thread th(TestScope2);
th.detach();
}
}
getchar();
return 0;
}
デッドロックの問題を解決します。他のコードは変更せず、TestScope1() のコードのみを変更します。
#include <thread>
#include <iostream>
#include <string>
#include <mutex>
#include <shared_mutex>
//Linux make: g++ -std=c++17 -o main main8.c -lpthread
using namespace std;
static mutex mux1;
static mutex mux2;
void TestScope1(){
//模拟死锁 停100ms等另一个线程锁mux2
//这里只是使用sleep函数模拟业务逻辑,真正的业务当中不应该有sleep,因为这个是占用CPU资源的。
this_thread::sleep_for(100ms);
cout << this_thread::get_id() << " begin mux1 lock" << endl;
//mux1.lock();
cout << this_thread::get_id() << " begin mux2 lock" << endl;
//mux2.lock(); //死锁
//c++11
//这种,它必须两个同时锁住,才会进行下一步操作,如果没有会释放锁,这样就不会占用到两个锁
//lock(mux1, mux2);
//c++17
scoped_lock lock(mux1, mux2); // 解决死锁,可以传多个锁。
cout << "TestScope1" << endl;
this_thread::sleep_for(1000ms);
//mux1.unlock();
//mux2.unlock();
}
//上锁的顺序:2->1->2->1
void TestScope2(){
cout << this_thread::get_id() << " begin mux2 lock" << endl;
mux2.lock();
this_thread::sleep_for(500ms);
cout << this_thread::get_id() << " begin mux1 lock" << endl;
mux1.lock();//死锁
cout << "TestScope2" << endl;
this_thread::sleep_for(1500ms);
mux1.unlock();
mux2.unlock();
}
int main(int argc, char* argv[]){
{
//演示死锁情况
{
thread th(TestScope1);
th.detach();
}
{
thread th(TestScope2);
th.detach();
}
}
getchar();
return 0;
}
3.7 条件変数アプリケーションシナリオプロデューサーコンシューマ信号処理
生産者・消費者モデル
プロデューサとコンシューマはリソース変数 (リスト キュー) を共有し、プロデューサはプロダクトを生成し、コンシューマに消費するように通知します。コンシューマはブロックしてシグナルを待ちます。シグナルを取得した後にプロダクトを消費します (リスト キュー内のデータを取り出します)。 )
コアの重要な点は、プロデューサーとコンシューマーが異なるスレッドに存在し、複数のプロデューサーとコンシューマーが存在する可能性があることです。それらはどのように相互に通信し、複数のスレッドのバランスをとって割り当てを行うのでしょうか?
以前の生産者と消費者はどのようにしてそれを行ったのでしょうか? プロデューサがデータを送信した後、コンシューマは一定の遅延に従ってそれを受信します。この 2 つは互いに独立しています。プロデューサがデータを生成すると、コンシューマはデータを処理するように直ちに通知されます。この通知はセマフォです。これにより、コンシューマーをそこでブロックすることができ、ブロックによって CPU リソースが占有されなくなります。セマフォは、コンシューマーにブロックを解除するように通知するために使用されます。
プロデューサー モデルの手順:
セマフォの準備完了: std::condition_variable cv;
1 std::mutex の取得 (多くの場合 std::unique_lock 経由): unique_lock lock(mux);
2 ロックの取得中に変更: msgs_.push_back(data);
3 ロックの解放と読み取り値の通知thread: lock.unlock();
cv.notify_one(); //待機中のシグナル スレッドに通知
cv.notify_all(); //待機中のすべてのシグナル スレッドに通知
消費者モデルの手順:
1 共有変数を変更するスレッドに共通のミューテックスを取得します: unique_lock lock(mux);
2 wait() シグナル通知を待ちます:
2.1 ランバダ式なし
2.2 ランバダ式 cv.wait(lock, [] {return !msgs_.empty () ;});
3.8 条件変数コード例の読み取りおよび書き込みスレッド
1 つの書き込みスレッドと複数の読み取りスレッドをシミュレートします
#include <thread>
#include <iostream>
#include <mutex>
#include <list>
#include <string>
#include <sstream>
#include <condition_variable> // std::condition_variable
using namespace std;
list<string> msgs_;
mutex mux;
condition_variable cv; //信号量的定义
void ThreadWrite(){
for(int i = 0;; i++){
stringstream ss;
ss << "Write msg " << i; //字符串拼接,把"Write msg "和i拼在一起,并存放在ss中
unique_lock<mutex> lock(mux); //在这里加锁,确保锁住
msgs_.push_back(ss.str()); //ss.str()<=>ss.data()都返回当前字符串的内容
//这里为什么要先进行解锁呢?假如这里没有解锁,那么notify_one在调用的时候,在下面
//读取线程中的wait函数,wait他需要先锁定,而这里的又没有释放,那么程序就会阻塞,就会造成死锁
lock.unlock(); //释放锁
//cv.notify_one(); //发送信号,通知一个读线程进入
cv.notify_all(); //即使发送通知信号给所有线程,也只有一个线程能进入
this_thread::sleep_for(500ms); //每500ms写一个数据,然后通知读取线程去处理
}
}
void ThreadRead(int i){
for(;;){
cout << "开始读取数据" << endl;
unique_lock<mutex> lock(mux);//加锁
//这个wait什么时候解除阻塞,需要等到cv.notify_one();按次序通知到他,
//如果是notify_all,所有在wait的线程都会返回"开始读取数据",最终还是只能有一个线程能进入读取
//如果是notify_one就只有一个会返回"开始读取数据",最终还是只能有一个线程能进入读取
cv.wait(lock, [i]
{
cout << "线程 " << i << " 等待" << endl;
//msgs_.empty()当是空的时候返回TRUE,TRUE的话相当于阻塞是不应该进行的,看wait源码可知
//return true;//当一直返回TRUE的话,不管有无信号wait都不会阻塞,而继续往下执行
//当一直返回FALSE的话,没有信号的话连这个wait都不进入
//有信号会一直阻塞在这儿,并按照信号间隔进入此wait函数
//return false;
return !msgs_.empty();
});
//获取信号后锁定
while (!msgs_.empty()){
cout << "线程 " << i << " 开始读取数据" << msgs_.front() << endl;
msgs_.pop_front();
}
}
}
void threadread(int i){//比较简单的演示
for(;;){
cout << "read msg" << endl;
unique_lock<mutex> lock(mux);//加锁
cv.wait(lock);//是先解锁、阻塞等待信号,获取信号会锁定,所以做消息的处理是线程安全的
//获取信号后锁定
while (!msgs_.empty()){
cout << "thread " << i << " read mumber " << msgs_.front() << endl;
msgs_.pop_front();
}
}
}
int main(int argc, char* argv[]){
thread th(ThreadWrite);
th.detach();
for (int i = 0; i < 3; i++){
//thread th(ThreadRead, i + 1);//带lambda表达式的wait
thread th(threadread, i + 1);//单纯的wait
th.detach();
}
getchar();//放在这里面,阻塞住当前的操作
return 0;
}
cv.notify_all(); に変更すると、実行結果は次のようになります。
つまり、3 つのスレッドすべてが応答しましたが、読み取りに入ったのは 1 つだけでした。
#include <thread>
#include <iostream>
#include <mutex>
#include <list>
#include <string>
#include <sstream>
#include <condition_variable> // std::condition_variable
using namespace std;
list<string> msgs_;
mutex mux;
condition_variable cv;
void ThreadWrite()
{
for (int i = 0;; i++)
{
stringstream ss;
ss << "Write msg " << i; //字符串拼接,把"Write msg "和i拼在一起,并存放在ss中
unique_lock<mutex> lock(mux); //在这里加锁,确保锁住
msgs_.push_back(ss.str()); //ss.str()<=>ss.data()都返回当前字符串的内容
//这里为什么要先进行解锁呢?假如这里没有解锁,那么notify_one在调用的时候,在下面
//读取线程中的wait函数,wait他需要先锁定,而这里的又没有释放,那么程序就会阻塞,就会造成死锁
lock.unlock(); //释放锁
cv.notify_one(); //先解完锁后,发送信号,通知一个读线程进入
//cv.notify_all(); //即使发送通知信号给所有线程,也只有一个线程能进入
this_thread::sleep_for(3s); //每3s写一个数据,然后通知读取线程去处理
}
}
void ThreadRead(int i)
{
for (;;)
{
cout << "开始读取数据" << endl;
unique_lock<mutex> lock(mux);//加锁
//这个wait什么时候解除阻塞,需要等到cv.notify_one();按次序通知到他,
//如果是notify_all,所有在wait的线程都会返回"开始读取数据",最终还是只能有一个线程能进入读取
//如果是notify_one就只有一个会返回"开始读取数据",最终还是只能有一个线程能进入读取
cv.wait(lock, [i]
{
cout << "线程 " << i << " 等待" << endl;
//msgs_.empty()当是空的时候返回TRUE,TRUE的话相当于阻塞是不应该进行的,看wait源码可知
//return true;//当一直返回TRUE的话,不管有无信号wait都不会阻塞,而继续往下执行
//当一直返回FALSE的话,没有信号的话连这个wait都不进入
//有信号会一直阻塞在这儿,并按照信号间隔进入此wait函数
//return false;
return !msgs_.empty();
});
//获取信号后锁定
while (!msgs_.empty())
{
cout << "线程 " << i << " 开始读取数据" << msgs_.front() << endl;
msgs_.pop_front();
}
}
}
void threadread(int i)//比较简单的演示
{
for (;;)
{
cout << "read msg" << endl;
unique_lock<mutex> lock(mux);//加锁
cv.wait(lock);//是先解锁、阻塞等待信号,获取信号会锁定,所以做消息的处理是线程安全的
//获取信号后锁定
while (!msgs_.empty())
{
cout << "thread " << i << " read mumber " << msgs_.front() << endl;
msgs_.pop_front();
}
}
}
int main(int argc, char* argv[])
{
thread th(ThreadWrite);
th.detach();
for (int i = 0; i < 3; i++)
{
thread th(ThreadRead, i + 1);//带lambda表达式的wait
//thread th(threadread, i + 1);//单纯的wait
th.detach();
}
getchar();//放在这里面,阻塞住当前的操作
return 0;
}