Linux マルチスレッド (2) C++ マルチスレッド プログラミングの基礎

1. C++ マルチスレッドについて

プロセス: システム メモリ リソースや CPU タイム スライスなどのリソースを割り当ててスケジュールするためのオペレーティング システムの基本単位であり、アプリケーションを実行するためのオペレーティング環境を提供します。

スレッド: オペレーティング システム/CPU が操作のスケジューリングを実行できる最小単位であり、プロセスに含まれ、プロセスには 1 つ以上のスレッドが含まれます。

マルチスレッド: 同時実行性/並列性を実現する手段です。つまり、複数のスレッドが同時に実行されます。一般に、プロセスは 1 つの物事に対する完全なソリューションとして理解できます。マルチスレッドとは、全体を分割することです。 1 つの処理を複数のサブプロセスに分けて実行するステップ、そしてこの複数のサブステップが同時に実行されます。

C++ マルチスレッド: 複数の関数を使用してそれぞれの関数を実現し、異なる関数は異なる関数を生成して同時に実行します。(異なるスレッドにはある程度の実行順序がある場合があり、これは一般に同時実行と見なされます)。

2. C++ マルチスレッドの基本

2.1 スレッドの作成

スレッド クラスを定義するヘッダー ファイル #include<thread> (C++11) を導入します。スレッドの作成とは、このクラスのオブジェクトをインスタンス化することを意味します。インスタンス化されたオブジェクトによって呼び出されるコンストラクターは、パラメーターを渡す必要があります。関数名、スレッド th1(proc1) 渡された関数がパラメーターを渡す必要がある場合は、オブジェクト、スレッド th1(proc1,a,b) をインスタンス化するときに、関数名の後にこれらのパラメーターを記述します。

スレッドのブロック方法:

  • 参加する()
  • デタッチ()

th1.join()、つまりステートメントが配置されているスレッドが main() 関数に書き込まれます。指定されたスレッド th1 の実行が完了した後、メイン スレッドは実行を継続します。つまり、現在のスレッドは一時停止され、指定されたスレッドの実行が完了すると、現在のスレッドが再開されます。

スレッドをブロックする目的は、各スレッドの実行順序を調整することです。

#include<iostream>
#include<thread>
using namespace std;
void proc(int a)
{
    cout << "我是子线程,传入参数为" << a << endl;
    cout << "子线程中显示子线程id为" << this_thread::get_id()<< endl;
}
int main()
{
    cout << "我是主线程" << endl;
    int a = 9;
    thread th2(proc,a);
    cout << "主线程中显示子线程id为" << th2.get_id() << endl;
    th2.join();
    return 0;
}

2.2 ミューテックスの使用法

ユニット内にプリンターがあり (共有データ a)、あなたはそのプリンターを使用する必要があります (スレッド 1 はデータ a を操作する必要があります)。同僚の Lao Wang もプリンターを使用する必要があります (スレッド 2 もデータ a を操作する必要があります)。ただし、プリンターは一度に1人しか使用できません。現時点では、誰がプリンターを使用しても、プリンターを使用する前にリーダーにライセンス(ロック)を申請し、返却する必要があると規定されています使用後にリーダーにライセンス (ロック解除) を与えます。ライセンスは合計 1 つだけです。プリンターを使用する同僚が使い果たされるまで待ってから、ライセンスを申請してください (ブロック、スレッド 1 がミューテックスをロックした後、他のスレッドはロックできなくなります)他のスレッドはスレッド 1 のロックが解除された後にのみロックできます。その場合、このライセンスはミューテックスです。ミューテックスにより、プリンターの使用プロセスが中断されないことが保証されます。

プログラムはミューテックスオブジェクト m をインスタンス化し、スレッドはメンバー関数 m.lock() を呼び出し、次のことが起こります。

  • ミューテックスが現在ロック解除されている場合、呼び出しスレッドはミューテックスをロックし、unlock() が呼び出されるまでロックを保持します。
  • ミューテックスが現在ロックされている場合、ミューテックスのロックが解除されるまで呼び出しスレッドはブロックされます。

ミューテックスの使用には最初に必要なものがあります

#include<ミューテックス>

lock() と lock()

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    m.lock();
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
    m.unlock();
}
 
void proc2(int a)
{
    m.lock();
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
    m.unlock();
}
int main()
{
    int a = 0;
    thread th1(proc1, a);
    thread th2(proc2, a);
    th1.join();
    th2.join();
    return 0;
}

lock_guardまたは unique_lockを使用すると、ロックの解除忘れの問題を回避できます。

  • ロックガード():

原則は、ローカルの lock_guard オブジェクトを宣言し、そのコンストラクターでロックし、デストラクターでロックを解除することです。最終結果は、作成がロックされ、スコープが終了するとスコープが自動的にロック解除されます。したがって、lock_guard() を使用すると、lock() と lock() を置き換えることができます。
スコープを設定することで、適切な場所でlock_guardが破棄されます(ミューテックスロックとミューテックスアンロックの間のコードをクリティカルセクションと呼びます(共有リソースへの相互排他アクセスを必要とするコードをクリティカルセクションと呼びます))。クリティカル セクションはできるだけ小さくする必要があります。つまり、ミューテックスをロックした後、できるだけ早くロックを解除する必要があります)。{} を使用してスコープを調整することで、ミューテックス m を適切な場所でロック解除できます

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    lock_guard<mutex> g1(m);//用此语句替换了m.lock();lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁
 
void proc2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout << "proc2函数正在改写a" << endl;
        cout << "原始a为" << a << endl;
        cout << "现在a为" << a + 1 << endl;
    }//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
    cout << "作用域外的内容3" << endl;
    cout << "作用域外的内容4" << endl;
    cout << "作用域外的内容5" << endl;
}
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

lock_gurad は 2 つのパラメータを渡すこともできます。最初のパラメータが Adopt_lock フラグの場合、それはロックされていることを意味し、ミューテックスはコンストラクタ内でロックされていないため、この時点で事前に手動でロックする必要があります

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    m.lock();//手动锁定
    lock_guard<mutex> g1(m,adopt_lock);
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//自动解锁
 
void proc2(int a)
{
    lock_guard<mutex> g2(m);//自动锁定
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
}//自动解锁
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

unique_lock :

unique_lock は lock_guard に似ていますが、unique_lock の方がより多くの用途があり、lock_guard() の元の関数をサポートしています。
手動 lock() および手動ロック解除() は、lock_guard の使用後は使用できません。手動ロック() および手動ロック解除() は、unique_lock の使用後に使用できます。unique_lock の 2 番目のパラメータは、adopt_lock に加えて、try_to_lock および defer_lock にすることもできます

try_to_lock: ロックを試みるには、ロックがロック解除状態であることを確認してから、今すぐロックの取得を試みる必要があります。mutx の lock() を使用してミューテックスをロックしようとしますが、ロックが成功しない場合はすぐに戻ります。 defer_lock:
初期化 ロックされていないミューテックスを作成しました。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
    unique_lock<mutex> g1(m, defer_lock);//始化了一个没有加锁的mutex
    cout << "不拉不拉不拉" << endl;
    g1.lock();//手动加锁,注意,不是m.lock();注意,不是m.lock();注意,不是m.lock()
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
    g1.unlock();//临时解锁
    cout << "不拉不拉不拉"  << endl;
    g1.lock();
    cout << "不拉不拉不拉" << endl;
}//自动解锁
 
void proc2(int a)
{
    unique_lock<mutex> g2(m,try_to_lock);//尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里;
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
}//自动解锁
int main()
{
    int a = 0;
    thread proc1(proc1, a);
    thread proc2(proc2, a);
    proc1.join();
    proc2.join();
    return 0;
}

Unique_lock 所有権の譲渡

mutex m;
{  
    unique_lock<mutex> g2(m,defer_lock);
    unique_lock<mutex> g3(move(g2));//所有权转移,此时由g3来管理互斥量m
    g3.lock();
    g3.unlock();
    g3.lock();
}

2.3 非同期スレッド

ヘッダーファイルを追加する

#include<future>

  • 非同期と将来

async は、非同期タスクを開始するために使用される関数テンプレートです。Future クラス テンプレート オブジェクトを返します。Future オブジェクトはプレースホルダーとして機能します。インスタンス化されたばかりの Future には格納された値はありませんが、Futureオブジェクトの get() を呼び出すとき、メンバー関数が使用されると、メインスレッドは非同期スレッドの実行が終了するまでブロックされ、戻り結果は Future に渡されます。つまり、関数の戻り値は FutureObject.get() を通じて取得されます。

これは、あなたが政府の業務 (メインスレッド) を行い、フロントデスクに情報を引き渡し、フロントデスクがそれを処理する人員を手配し (async がサブスレッドを作成します)、フロントデスクがあなたにレシート (将来のオブジェクト)、あなたのビジネスについて話しています。これはあなたのために処理されており (サブスレッドが実行中です)、しばらくしてから戻ってきて、このレシートに基づいた結果を取得します。しばらくすると、結果を取得するためにフォアグラウンドに移動しますが、結果がまだ出ていない(子スレッドがまだ戻っていない)ため、フォアグラウンドで待機(ブロック)し、そこから離れません結果を取得するまで (get()) (ブロックされなくなります)。

#include <iostream>
#include <thread>
#include <mutex>
#include<future>
using namespace std;
double t1(const double a, const double b)
{
	double c = a + b;
	return c;
}
 
int main() 
{
	double a = 2.3;
	double b = 6.7;
	future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位;
	cout << "正在进行计算" << endl;
	cout << "计算结果马上就准备好,请您耐心等待" << endl;
	cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return
        //cout << "计算结果:" << fu.get() << endl;//取消该语句注释后运行会报错,因为future对象的get()方法只能调用一次。
	return 0;
}

共有未来

future と shard_future はどちらもspace を占有するために使用されますが、それらの間にはいくつかの違いがあります。
future の get() メンバー関数はデータの所有権を転送し、shared_future の get() メンバー関数はデータをコピーします。
したがって:

  • future オブジェクトの get() は 1 回だけ呼び出すことができます。複数のスレッドは同じ非同期スレッドを待つことはできません。いずれかのスレッドが非同期スレッドの戻り値を取得すると、他のスレッドはそれを再度取得することはできません。
  • shared_future オブジェクトの get() は複数回呼び出すことができ、複数のスレッドが同じ非同期スレッドを待機することができ、各スレッドは非同期スレッドの戻り値を取得できます。

2.4 アトムタイプ自動

  • アトミック操作は最小限の並列化不可能な操作です。

これは、たとえマルチスレッドであっても、アトミック オブジェクトを同期的に操作する必要があることを意味し、ミューテックスのロックとロック解除にかかる時間を節約します。

// C++ Standard: C++17
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic_int n(0);
void count10000() {
	for (int i = 1; i <= 10000; i++) {
		n++;
	}
}
int main() {
	thread th[100];
	for (thread &x : th)
		x = thread(count10000);
	for (thread &x : th)
		x.join();
	cout << n << endl;
	return 0;
}

3 スレッド プール

スレッドプールを使用しない場合:

スレッドを作成 -> スレッドでタスクを実行 -> タスクの実行後にスレッドを破棄します。多数のスレッドが必要な場合でも、このプロセスに従って各スレッドの作成、実行、破棄を行う必要があります。

スレッドの作成と破棄に消費される時間はスレッドの実行時間に比べてはるかに短いですが、大量のスレッドを頻繁に作成するタスクの場合、スレッドの作成と破棄に費やされる時間と CPU リソースも大きな割合を占めます

スレッドの作成と破棄による時間の消費とリソースの消費を削減するために、スレッド プール戦略が採用されています。

プログラムの開始後、一定数のスレッドが事前に作成されてアイドル キューに配置されますが、これらのスレッドはブロック状態にあり、基本的に CPU を消費せず、小さなメモリ領域のみを占有します。

タスクを受信した後、スレッド プールはタスクを実行するアイドル スレッドを選択します。

タスクの実行後、スレッドは破棄されず、スレッドはプール内の次のタスクを待ち続けます。

スレッドプールによって解決される問題:

(1) 大量のスレッドの作成と破棄を頻繁に行う必要がある場合、スレッドの作成と破棄に伴う時間のオーバーヘッドと CPU リソースの占有が軽減されます。(時間とエネルギーを節約)

(2) リアルタイム性の高い要求の場合、事前に大量のスレッドを作成しておくため、スレッドの作成手順を省略し、タスク受信後すぐにスレッドプールからスレッドを呼び出してタスクを処理することができます。これにより、リアルタイム パフォーマンスが向上します。(リアルタイム)

参考リンク:C++ マルチスレッド基本チュートリアル - zizbee - Blog Park (cnblogs.com)

おすすめ

転載: blog.csdn.net/weixin_46267139/article/details/131087034