Linux マルチスレッドの深い理解

進む人へ:

昨日はもっと増え、明日は減り、今日はまだそこにある、成功のために一生懸命働くのではなく、価値のある人になるために一生懸命働く. 人生の道は浮き沈みに満ちており、誰も順風満帆とは言えません。最も困難な瞬間にのみ、無力感の意味を理解できます。

目次

1. ページテーブルを理解する

1.1. アドレス空間とページテーブルの見方

2.2 ページテーブルを仮想アドレスから物理アドレスに変換する方法

2. スレッドの概念

3. 糸の特徴

4. スレッドの利点

5. スレッドの欠点

6.スレッド例外

7. スレッドの使用

8. Linux でのプロセスとスレッド

9. Linux スレッド制御

9.1. スレッドのバッチを作成します。

9.2. スレッドの終了

9.3 スレッド待ち

9.4 スレッドの切り離し

9.5 スレッド tid、スレッドスタックを理解する方法

9.6 pthread ライブラリのカプセル化

10.Linuxスレッドの相互排除

10.1 スレッド相互排除が存在する理由

10.2 プロセススレッド間の相互排除関連の概念

10.3 相互排除を実装するインターフェース

10.4 Mutex 実装の原理の調査

11. リエントラント vs スレッドセーフ

12.デッドロック

13. Linux スレッドの同期

14. 生産者消費者モデル

14.1 生産者消費者モデルを使用する理由

14.2 生産者消費者モデル

14.3 生産者消費者モデルの利点

14.4 BlockingQueue に基づく生産者と消費者のモデル

14.5 生産者と消費者の効率改善の問題を理解する方法

15. POSIX セマフォ

15.1 より前のコードの「不十分な」場所

15.2 セマフォとは

15.3 セマフォの特徴

15.4 セマフォの使い方

15.5 リング キューに基づく生産および消費モデル

16.スレッドプール

16.1 スレッドプールの概念

16.2 スレッドプールインスタンス

17. スレッドセーフなシングルトン パターン

17.1 シングルトンパターンとは

17.2 デザインパターンとは

17.3 シングルトンパターン

17.3.1 シングルトンモードを実現するハングリーな方法

17.3.2 シングルトン パターンを実装する怠惰な方法

17.3.3 ハングリー・マン・モードとレイジー・マン・モードの長所と短所

18. STL、スマートポインタ、スレッドセーフ

18.1 STL のコンテナはスレッドセーフですか?

18.2 スマートポインタはスレッドセーフですか?

19. その他の一般的なロック

20. リーダーライターモデル

20.1 リーダー/ライター モデルの概念的理解

20.2 読み書きロック

20.3 読み書きロックのロックとロック解除の疑似コード例

21. まとめ


1. ページテーブルを理解する

1.1. アドレス空間とページテーブルの見方

1.アドレス空間は、プロセスが見ることができるリソースウィンドウです

2. ページ テーブルは、プロセスが実際にリソースを所有する状況を決定します。

3.リソース分割のための合理的なアドレス空間+ページテーブル、プロセスのすべてのリソースを分割できます

2.2 ページテーブルを仮想アドレスから物理アドレスに変換する方法

以前の調査では、仮想アドレス空間から物理メモリへのページ テーブル マッピングが図に示されています。

32 ビット オペレーティング システムでは、アドレス番号の範囲は 0 から 2^32 で、各アドレスは 1 バイトを占有するため、合計 4GB のスペースを占有します。マッピングプロセスアドレス、ページテーブルのエントリが6バイトを占めると仮定すると、合計で24GBを占めることになります.この観点からすると、上図のページテーブルのマッピングは明らかに無理があるので、ページテーブルはどうですかマッピングされた?

リアル ページ テーブルは、インデックスを作成することで上記の問題を解決します。

1 バイトは合計 32 ビットで、オペレーティング システムは、最初の 10 ビットをページ ディレクトリとして配置し、中間の 10 ビットをページ テーブルとして配置し、後半の 12 ビットを仮想アドレスの業界オフセットとして配置します。

物理メモリ: 1 つずつページ フレームに分割され、各ページ フレームは 4 バイトを占有するため、ディスクがメモリにデータをロードするときも 4kb を基本単位としてロードします。

写真のように:

2. スレッドの概念

図に示すように、以前の調査を通じて、プロセスの関連する概念を既に知っています。

When the code and data is loaded into the memory, the PCB and virtual address space are created, and then maps through the page table. 複数のプロセスが作成されると、オペレーティング システムはプロセスを維持するために各プロセスの仮想アドレス空間を作成します。各プロセスの独立性. アドレス空間, ページテーブルなど. その中で, 仮想アドレス空間はプロセスが見ることができる「リソース」を決定します.

プロセスと比較すると、スレッドはプロセス内の実行フローであり、複数のスレッドを作成する場合、スレッドごとに個別に mm_struct とページ テーブルを作成するのではなく、プロセスが作成した mm_struct をポイントし、各 A スレッドがそれらを分割します。対応するリソースを解放し、スレッドが対応するタスクを実行できるようにします。

写真のように:

なぜこのようなスレッドを設計するのですか? これは作成するスレッドの役割に関係しており、あるタスクを実行するためにスレッドを作成する必要があり、それはプロセスの役割と一致するため、プロセスと同じスレッド設計の場合は、スレッドごとにスレッドを作成します。対応するオブジェクトは、カーネル データ構造を追加することによって管理されますが、これはコードの複雑さとプロセスとスレッド間の高い結合を増加させます. したがって、Linux でこの問題を解決するには、スレッドが PCB 構造を直接再利用する必要があります。 Linux には本当の意味でのスレッドはありません. いわゆるスレッドはプロセス内の実行フローであり, プロセスのリソースの一部を所有しています. 各スレッドは軽量のプロセスです. の観点からCPU、スレッドはスケジューリングの基本単位です。

これまでの研究で、プロセス=カーネルのデータ構造+コードとデータであることがわかっており、プロセスの学習は単一の実行フローの形で行われます.スレッドの概念を理解した後、プロセスの新しい理解が得られます.リソース割り当てを担当する基本エンティティです。

一般に、スレッドとプロセスの関係は、スレッドは CPU スケジューリングの基本単位であり、プロセスはリソース割り当てを担当する基本エンティティであり、プロセスは全体としてリソースを適用するために使用され、スレッドはプロセスからリソースを適用します。

写真のように:

前述のように、Linux には実際のスレッドがないため、Linux はスレッド用のシステム コール インターフェイスを直接作成することはできず、軽量プロセスを作成するためのインターフェイスしか提供できませんが、これは上級ユーザーの使用には不向きです。この問題を解決するために、軽量プロセスとスレッドの間にサードパーティ製ライブラリ pthread を追加し、軽量プロセスを「スレッド」の観点から見ることができるようにしました。

スレッドを作成:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                    void *(*start_routine) (void *), void *arg);

パラメータ:

thread は、スレッド id.attr がそれを nullptr に設定することを気にしないことを意味し、start_routine コールバック関数は、作成されたスレッドにタスクを実行させるために使用され、arg は関数に渡されるパラメーターです。

戻り値:

成功すると、pthread_create() は 0 を返します。エラーの場合はエラー番号を返し、*thread の内容は未定義です。
 

#include<iostream>
#include<pthread.h>
#include<cassert>
#include<unistd.h>
using namespace std;

//新线程
void* start_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,start_routine,(void*)"one thread");
    assert(n == 0);
    (void)n;
    //主线程
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }
    return 0;
}

コンパイルして実行: エラーが発生しました

理由: pthread はサードパーティのライブラリであるため、使用する場合は最初にリンクする必要があります: -lpthread 

g++ -o mythread mythread.cc -std=c++11 -lpthread

スクリーンショットを実行します。

作成された新しいスレッドを確認するには?

PID: プロセス識別子 id、LWP スレッド識別子 id、異なるスレッドの PID が同じであることがわかります。これは、上記のスレッドがプロセス内の実行フローであり、異なるスレッドの LWP が異なることも証明しています。は、スレッドを基本単位として CPU がスケジュールされていることを示しています。

3. 糸の特徴

1.スレッドが作成されると、ほとんどすべてのリソースが共有されます

int g_val = 0;
void fun()
{
    cout << "我是一个方法" << endl;
}
void* start_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << g_val++ <<endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,start_routine,(void*)"one thread");
    assert(n == 0);
    while(true)
    {
        cout << "我是主线程" << g_val++ << endl;
        fun();
        sleep(1);
    }
    return 0;
}

スクリーンショットを実行します。

2.スレッドにも独自のプライベートリソースがあります

1.PCB属性非公開

2. 特定のプライベート コンテキスト構造が存在する必要があります

3. 各スレッドには独自のプライベート スタック構造があります

4. スレッドの利点

1. 新しいスレッドを作成するコストは、新しいプロセスを作成するよりもはるかに小さい
2. プロセス間の切り替えに比べて、スレッド間の切り替えに必要なオペレーティング システムの作業ははるかに少ない

a. プロセスの切り替え: ページ テーブルの切り替え && 仮想アドレス空間の切り替え && PCB の切り替え && コンテキストの切り替え

b. スレッドの切り替え: PCB の切り替え && コンテキストの切り替え

c. スレッドの切り替えでキャッシュを切り替える必要はなく、プロセスでキャッシュを切り替える必要がある


オペレーティングシステムは、プログラムの動作効率を向上させるために、CPU内部に高速なキャッシュを内蔵しており、メモリからデータを取得する際に、アクセスしたデータの前後のデータも一度にキャッシュに格納します。データを取得するレジスタが最初にキャッシュ内を検索し、キャッシュ内に大量のホット データが格納されているため、高い確率でアクセスできますが、プロセス間スイッチングにより、プロセス間の独立性が確保され、データがキャッシュは共有されませんが、キャッシュ内のデータを含め、スレッド間のデータは共有されるため、スレッドの切り替えはプロセスの切り替えよりもはるかに少ない作業で済みます。

3. スレッドはプロセスよりもはるかに少ないリソースを占有します
4. 並列プロセッサの数を最大限に活用できます
5. 遅い I/O 操作の終了を待っている間、プログラムは他の計算タスクを実行できます
6. 計算集約型のアプリケーションマルチプロセッサ システムで実行するために、計算は複数のスレッドに分解されて実現されます
スレッドは、異なる I/O 操作を同時に待機できます。

5. スレッドの欠点

パフォーマンスの低下
外部イベントによってめったにブロックされない計算集約型のスレッドは、多くの場合、同じプロセッサを他のスレッドと共有できません。計算集約型スレッドの数が利用可能なプロセッサよりも多い場合, パフォーマンスが大幅に低下する可能性があります. ここでのパフォーマンスの低下とは, 利用可能なリソースが変化しないままで, 追加の同期とスケジューリングのオーバーヘッドが追加されることを指します.
堅牢性の低下
マルチスレッド化を行うには、より包括的かつ綿密な検討が必要です.マルチスレッド化されたプログラムでは、時間配分のわずかなずれや、共有すべきでない変数の共有によって、悪影響が生じる可能性が高くなります.つまり、スレッド間の保護。
アクセス制御の欠如
プロセスはアクセス制御の基本的な粒度であり、1 つのスレッドで一部の OS 関数を呼び出すとプロセス全体に影響します。
プログラミングの難易度の増加
マルチスレッド プログラムの作成とデバッグは、シングルスレッド プログラムよりもはるかに困難です。

6.スレッド例外

単一のスレッドがゼロで除算すると、ワイルド ポインターの問題によりスレッドがクラッシュし、プロセスがクラッシュに追従します.
スレッドはプロセスの実行ブランチです. スレッドが異常な場合は、プロセスの異常に似ています.その後、プロセスを終了するシグナルメカニズムをトリガーします. プロセスは終了します. プロセス内のすべてのスレッド

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cassert>
using namespace std;

void* start_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << (const char*)args <<endl;
        //线程内部出现野指针,线程异常崩溃,进程也会崩溃
        int* p = nullptr;
        *p = 100;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,start_routine,(void*)"one thread");
    assert(n == 0);
    (void)n;
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }
    return 0;
}

スクリーンショットを実行します。

7. スレッドの使用

マルチスレッドを合理的に使用すると、CPU を集中的に使用するプログラムの実行効率を向上させることができます.
マルチスレッドを合理的に使用すると、IO を集中的に使用するプログラムのユーザー エクスペリエンスを向上させることができます.マルチスレッド操作の一種。パフォーマンス)

8. Linux でのプロセスとスレッド

プロセスはリソース割り当ての基本単位です.
スレッドはスケジューリングの基本単位です. スレッド
はプロセスデータを共有しますが,
スレッド ID,
一連のレジスタ,
スタック
errno
シグナルマスク,
スケジューリングの優先度などの独自のデータも持っています.

プロセスの複数のスレッドが同じアドレス空間を共有するため、テキスト セグメントとデータ セグメントが共有されます. 関数を定義すると、各スレッドで呼び出すことができます. グローバル変数を定義すると、それを除いて各スレッドでアクセスできます.さらに、各スレッドは次のプロセス リソースと環境も共有します:
ファイル記述子テーブル
、各シグナルの処理モード (SIG_IGN、SIG_DFL またはカスタム シグナル処理関数)、
現在の作業ディレクトリ、
ユーザー ID およびグループ ID
プロセスとスレッドの関係は次のとおりです。次のとおりです。

9. Linux スレッド制御

9.1. スレッドのバッチを作成します。

書き込み 1:

void* start_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << (const char*)args <<endl;
        sleep(1);
    }
}
#define NUM 10
int main()
{
    for(size_t i = 0; i < NUM; i++)
    {
        pthread_t tid;
        char namebuffer[64];
        snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);
        pthread_create(&tid,nullptr,start_routine,namebuffer);
    }
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }
    return 0;
}

スクリーンショットを実行します。

メインスレッドが新しいスレッドを作成すると、それ以降、新しいスレッドの実行メソッドが呼び出されなくなり、メインスレッドが新しいスレッドを作成して実行メソッドが上書きされる原因となるのは、ネームバッファバッファが各スレッドで共有されているためです。であるため、Execute メソッドが呼び出されるスレッドは 1 つだけです。

書き込み 2:

class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
};
void* start_routine(void* args)
{
    ThreadData* td = (ThreadData*)args;
    int cnt = 10;
    while(cnt--)
    {
        cout << "我是新线程" << td->namebuffer <<"cnt: " << cnt << endl;
        sleep(1);
    }
    delete td;
    return nullptr;
}
#define NUM 10
int main()
{
    vector<ThreadData*> threads;
    for(size_t i = 0; i < NUM; i++)
    {
        ThreadData* tid = new ThreadData();
        snprintf(tid->namebuffer,sizeof(tid->namebuffer),"%s:%d","thread",i);
        pthread_create(&tid->tid,nullptr,start_routine,tid);
        //创建好的线程结果保存下来
        threads.push_back(tid);
    }
    for(auto& e : threads)
    {
        cout << e->tid << e->namebuffer << "sucess" << endl;
    }
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }
    return 0;
}

スクリーンショットを実行します。

 ノート:

start_routine は 10 個のスレッドによって呼び出されます. 異なる実行フローが同じ関数を呼び出します. この関数は再入可能な状態にあります. 関数内で定義された変数は一時的なものであり, マルチスレッドでも適用可能です. また, 各スレッドについて側面から説明しています.独自の独立したスタック構造であるため、データがあいまいになることはありません。そのため、start_routine は再入可能な関数です。

9.2. スレッドの終了

プロセス全体を終了せずに、特定のスレッドのみを終了する必要がある場合は、次の 3 つの方法があります。
1. スレッド関数から戻る。このメソッドはメイン スレッドには適用されず、メイン関数からの戻りは exit の呼び出しと同等です。
2. スレッドは、pthread_exit を呼び出すことによって自身を終了できます。
3. スレッドは、pthread_cancel を呼び出して、同じプロセス内の別のスレッドを終了できます。

pthread_exit 関数

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

pthread_exit または return によって返されたポインタが指すメモリ ユニットは、グローバルであるか、malloc によって割り当てられている必要があり、スレッド関数のスタックに割り当てることはできないことに注意してください。ポインターを返します。

pthread_cancel 関数 

功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

9.3 スレッド待ち

1. 新しいスレッドの終了情報を取得する

2. 新しいスレッドに対応する PCB などのコア リソースをリサイクルして、メモリ リークを防ぎます。

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

この関数を呼び出すスレッドはハングし、id が thread であるスレッドが終了するまで待機します。スレッド thread はさまざまな方法で終了し、pthread_join によって取得される終了ステータスも異なります. 概要は次のとおりです:
1. スレッド thread が return によって戻る場合、value_ptr が指すユニットには、スレッド thread 関数の戻り値が格納されます。 .
2. スレッド thread が他のスレッドによって pthread_cancel を呼び出して異常終了した場合、value_ptr が指すユニットに定数 PTHREAD_CANCELED (-1) が格納されます。
3. スレッド thread が pthread_exit 自体を呼び出して終了した場合、value_ptr が指すユニットには、pthread_exit に渡されたパラメータが格納されます。
4. スレッド thread の終了ステータスに関心がない場合は、value_ptr パラメータに NULL を渡すことができます。

注: pthread_join: デフォルトでは、関数呼び出しは成功したと見なされ、異常終了の問題は考慮されないため、スレッドが終了したときに対応するシグナルは取得されません.スレッドが異常でシグナルを受信した場合、プロセス全体が終了します!

9.4 スレッドの切り離し

デフォルトでは、新しく作成されたスレッドは結合可能です.スレッドが終了した後、pthread_join操作を実行する必要があります.そうしないと、リソースを解放できず、システムリークが発生します.
スレッドの戻り値を気にしないと、join が負担になります. この時点で、スレッドの終了時にスレッド リソースを自動的に解放するようにシステムに指示できます。

int pthread_detach(pthread_t thread);

スレッド グループ内の他のスレッドがターゲット スレッドを分離しているか、スレッド自体が分離している可能性があります。

pthread_detach(pthread_self());

pthread_self() はスレッドの ID を取得します

#include<iostream>
#include<pthread.h>
#include<stdio.h>
#include<cstring>
#include<string>
#include<assert.h>
#include<unistd.h>

using namespace std;
string changeId(const pthread_t& thread_id)
{
    char buf[64];
    snprintf(buf,sizeof buf,"0x%x",thread_id);
    return buf;
}
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    pthread_detach(pthread_self());//设置自己为分离状态
    int cnt = 5;
    while(cnt--)
    {
        string tid = changeId(pthread_self());
        cout << "我是新线程" << "线程名:"<< name << " " << tid <<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    cout << "新线程id: " << changeId(tid) << endl;
    //主线程id:
    string id = changeId(pthread_self());
    cout << "我是主线程" << "主线程id: " << id << endl;
    //一个线程默认是joinable的,如果设置了分离状态,就不会再进行等待了
    int wait = pthread_join(tid,nullptr);
    cout << "result: " << wait << ": " << strerror(wait) << endl;
    return 0;
}

スクリーンショットを実行します。

9.5 スレッド tid、スレッドスタックを理解する方法

Linux オペレーティング システムには実際のスレッドはありません. いわゆるスレッドのほとんどはプロセスの属性を再利用するため, スレッドは軽量プロセスとも呼ばれます. ただし, 上位のユーザー エクスペリエンスを向上させるために, 軽量プロセススレッドとして利用されるため, 軽量プロセス用にさらにパッケージ化を行い, pthread ライブラリを提供する. スレッドは多くのプロセス属性を再利用するが, スレッドも独自の属性を持っている. スレッドライブラリなどのスレッドリソースを利用者が申請すると,リソース管理の問題が設計され、管理方法が最初に組織に記述されるため、各スレッドは独自の構造体オブジェクトを持ち、各スレッド構造体オブジェクトは仮想アドレス空間の共有領域に格納されます、図に示すように

各スレッドのプロパティは、スレッドの tid を含む共有領域に保存されます. tid は基本的に、スレッドが作成された後の共有領域の開始アドレスであり、各スレッドには独自のスタック構造が含まれています. 

スレッド ローカル ストレージの理解:

//添加__thread选项,可以将一个内置类型设置为线程局部存储
__thread int g_val = 100;
string changeId(const pthread_t& thread_id)
{
    char buf[64];
    snprintf(buf,sizeof buf,"0x%x",thread_id);
    return buf;
}
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        string tid = changeId(pthread_self());
        cout << "我是新线程" << " 新线程id: " << tid <<" g_val: "<<g_val<<" &g_val: "<<&g_val<<endl;
        g_val++;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    string id = changeId(pthread_self());
    while(true)
    {
        cout << "我是主线程" << " 主线程id: " << id << " g_val: "<<g_val<<" &g_val: "<<&g_val<<endl;
        sleep(1);
    }
    return 0;
}

スクリーンショットを実行します。

9.6 pthread ライブラリのカプセル化

#pragma once
#include<iostream>
#include<stdio.h>
#include<functional>
#include<string>
#include<pthread.h>

class Thread;
class Context
{
public:
    Thread* _this;
    void* _args;
public:
    Context():_this(nullptr),_args(nullptr)
    {}
    ~Context()
    {}
};
class Thread
{
public:
    typedef std::function<void*(void*)> func_t;
    const int num = 1024;
    Thread(func_t fun,void* args,int number):_fun(fun),_args(args)
    {
        char buffer[num];
        snprintf(buffer,sizeof buffer,"thread->%d",number);
        _name = buffer;
    }
    static void* start_routine(void* args)
    {
        Context* ctx = static_cast<Context*>(args);
        void* ret = ctx->_this->run(ctx->_args);
        delete ctx;
        return ret;
    }
    void start()
    {
        Context* ctx = new Context();
        ctx->_this = this;
        ctx->_args = _args;
        int n = pthread_create(&_tid,nullptr,start_routine,ctx);
        //编译器debug发布的时候存在,以release方式发布,assert就不存在了,n就是一个定义了但是没有使用的变量
        assert(n == 0);
        (void)n;//在有些编译器下可能会会有告警,所以用(void)n消除告警!
    }
    void join()
    {
        int n = pthread_join(_tid,nullptr);
        assert(n == 0);
        (void)n;
    }
    void *run(void* args)
    {
        return _fun(args);
    }
    ~Thread()
    {}
private:
    std::string _name;
    pthread_t _tid;
    func_t _fun;
    void* _args;
};

10.Linuxスレッドの相互排除

10.1 スレッド相互排除が存在する理由

複数のスレッドが同じリソースに同時にアクセスすると、データ例外が発生します。たとえば、次のコードの一部:

int tickets = 10000;
void *getTickets(void* args)
{
    string use_name = static_cast<const char*>(args);
    while(true)
    {
        if(tickets > 0)
        {
            usleep(1234);
            cout << use_name <<" 正在进行抢票 " << tickets-- << endl;
        }
        else
        {
            break;
        }
    }
}
int main()
{
    unique_ptr<Thread> thread1(new Thread(getTickets,(void*)"user1",1));
    unique_ptr<Thread> thread2(new Thread(getTickets,(void*)"user2",2));
    unique_ptr<Thread> thread3(new Thread(getTickets,(void*)"user3",3));
    thread1->start();
    thread2->start();
    thread3->start();
    thread1->join();
    thread2->join();
    thread3->join();
    return 0;
}

スクリーンショットを実行します。

複数のユーザーがチケットを取得すると、投票数が -1 になり、この時点でデータ例外が発生します。

この問題を解決するために、オペレーティング システムはスレッドをロックすることを提案し、各スレッドが公開リソースに順次アクセスできるようにします。この方法は相互排除と呼ばれます。

10.2 プロセススレッド間の相互排除関連の概念

クリティカル リソース: 共有リソースにアクセスするマルチスレッド実行ストリームはクリティカル リソースと呼ばれますクリティカル セクション: 各スレッド内で、クリティカル リソースに
アクセスするコードはクリティカル セクションと呼ばれます
クリティカル セクション ゾーンは、クリティカル リソースにアクセスし、通常はクリティカル リソースを保護します。 原子性
: スケジューリング メカニズムによって中断されない操作。操作には、完了または未完了の 2 つの状態しかありません。

10.3 相互排除を実装するインターフェース


ミューテックスを初期化する方法は 2 つあります。方法
1、静的割り当て:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法 2、動的割り当て:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex:要初始化的互斥量
attr:NULL


ミューテックスの
破棄 ミューテックスの破棄には注意が必要です:
        PTHREAD_ MUTEX_ INITIALIZER で初期化されたミューテックスを破棄する必要はありません.
        既にロックされているミューテックスを破棄しないでください
        . スレッドが後でそれを追加しようとしないことを確認してください. ロック

int pthread_mutex_destroy(pthread_mutex_t *mutex);

ミューテックスのロックとロック解除

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

pthread_lock を呼び出すと、次の状況が発生する可能性があります:
ミューテックスがロック解除された状態で、関数がミューテックスをロックし、同時に、関数呼び出しが正常に開始されたときに、他のスレッドがミューテックスをロックしたか、またはミューテックスがロックされています。同時に他のスレッド ミューテックスを申請しても、そのミューテックスをめぐる競争がない場合、pthread_lock 呼び出しはブロックされ (実行フローが中断され)、ミューテックスがロック解除されるのを待ちます。

上記の発券システムを改善します。

#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using namespace std;

int tickets = 10000;
pthread_mutex_t lock;
void *getTickets(void* args)
{
    string use_name = static_cast<const char*>(args);
    while(true)
    {
        //加锁:
        pthread_mutex_lock(&lock);
        if(tickets > 0)
        {
            usleep(1234);
            cout << use_name <<" 正在进行抢票 " << tickets-- << endl;
            //解锁
            pthread_mutex_unlock(&lock);
        }
        else
        {
            //解锁
            pthread_mutex_unlock(&lock);
            break;
        }
        //让该线程生成订单,其它线程再申请抢票
        usleep(1234);
    }
}
int main()
{
    //初始化锁
    pthread_mutex_init(&lock,nullptr);
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,nullptr,getTickets,(void*)"thread 1");
    pthread_create(&t2,nullptr,getTickets,(void*)"thread 2");
    pthread_create(&t3,nullptr,getTickets,(void*)"thread 3");
    pthread_create(&t4,nullptr,getTickets,(void*)"thread 4");

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);
    //销毁锁
    pthread_mutex_destroy(&lock);
    return 0;
}

スクリーンショットを実行します。

ロックを追加したら、異常なデータの問題が解決されました! 

10.4 Mutex 実装の原理の調査

1.ロックをどのように扱うのですか?

a. ロック自体が共有リソースです。グローバル変数は保護する必要があり、ロックはグローバル リソースを保護するために使用され、ロック自体もグローバル リソースです. 誰がロックのセキュリティを保護しますか?

b. pthread_mutex_lock、pthread_mutex_unlock: ロックのプロセスは安全でなければなりません! ロック プロセスは実際にはアトミックです。

c. 申請が成功した場合は、逆方向に実行を続行します. 申請が一時的に失敗した場合、実行フローはブロックされます!

d. ロックを保持している人は誰でも、クリティカル セクションに入ります。

2. ロックとロック解除の本質を理解する方法

重要なリソースにアクセスする際にスレッドが切り替わる可能性があるため、相互排他ロック操作を確実に実現するために、スレッドはロックで切り替わる. スレッドが切り替わっても、他のスレッドは正常にロックを申請できない. オペレーティングシステムが実装するメカニズム.このメソッドは、スワップまたは交換命令を提供することです。この命令の機能は、レジスタとメモリユニットのデータを交換することです。命令は 1 つしかないため、原子性が保証されます。

ロックの実装プロセス:

ロック解除の実現プロセス:

 3. ロックをカプセル化する

#include<iostream>
#include<pthread.h>

using namespace std;

class Mutex
{
public:
    Mutex(pthread_mutex_t* lock_p = nullptr):lock_p_(lock_p)
    {}
    void lock()
    {
        if(lock_p_)
            pthread_mutex_lock(lock_p_);
    }
    void unlock()
    {
        if(lock_p_)
            pthread_mutex_unlock(lock_p_);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t* lock_p_;
};
class LockGuard
{
public:
    LockGuard(pthread_mutex_t* mutex):mutex_(mutex)
    {
        mutex_.lock(); //在构造函数中加锁
    }
    ~LockGuard()
    {
        mutex_.unlock();//在析构函数中解锁
    }
private:
    Mutex mutex_;
};

このとき、ロックを利用する場合は、オブジェクトを定義し、ロックをオブジェクトに渡し、オブジェクトにロックを自動で処理させる処理方法を RAII グリッドと呼びます。

11. リエントラント vs スレッドセーフ

スレッド セーフ: 複数のスレッドが同じコードを同時に実行しても、異なる結果は表示されません。この問題は、グローバル変数または静的変数が一般的に操作され、ロック保護がない場合に発生します。
再入性: 同じ関数が異なる実行フローから呼び出され、現在のプロセスが実行される前に、別の実行フローが再び入ることを再入と呼びます。再入可能でも実行結果に違いや問題がない場合を再入可能関数、そうでない場合を非再入可能関数と呼びます。

一般的なスレッドの危険な状況

共有変数を保護しない関数
呼び出されると状態が変化する関数
静的変数へのポインターを返す関数
スレッドセーフでない関数を呼び出す関数

一般的なスレッド セーフティの状況

各スレッドには、グローバル変数または静的変数への読み取りアクセスのみがあり、書き込みアクセスはありません. 一般的に言えば、これらのスレッドは安全です

クラスまたはインターフェースは、スレッドのアトミック操作です

複数のスレッドを切り替えても、このインターフェースの実行結果があいまいになることはありません

一般的な再入不可のケース

malloc 関数はグローバル リンク リストを使用してヒープを管理するため、malloc/free 関数が呼び出されます。標準
I/O ライブラリ関数が呼び出されます。標準 I/O ライブラリの多くの実装では、非再入可能な方法 再入可能な
関数 本体は静的データ構造を使用します

一般的なリエントラント ケース

グローバル変数または静的変数を使用しないでください
malloc または new によって開かれたスペースを使用しないでください
再入不可の関数を呼び出さないでください静的データまたはグローバル データを返さないでください。すべてのデータは、ローカル データを使用して
関数の呼び出し元によって提供されます。
グローバル データをローカル コピーしてグローバル データを保護する

再入可能性とスレッド セーフ

関数は再入可能であり、スレッドセーフであることを意味します.
関数は再入可能でないため、複数のスレッドで使用できず、スレッドセーフの問題が発生する可能性があります
. 関数にグローバル変数がある場合、この関数はスレッドセーフではありません.安全でも再入可能でもない。

リエントラントとスレッドセーフの違い

Reentrant functions are a type of thread-safe functions. スレッド セーフは
必ずしも再入可能ではありませんが、再入可能関数はスレッド セーフである必要があります。
重要なリソースへのアクセスがロックされている場合、この関数はスレッドセーフですが、リエントラント関数がロックを解除していない場合、デッドロックが発生するため、リエントラントではありません。

12.デッドロック

デッドロックとは、プロセスのグループ内の各プロセスが解放されないリソースを占有する永続的な待機状態を指しますが、他のプロセスによって占有され解放されないリソースを相互に適用します。

デッドロックに必要な 4 つの条件は
        相互に排他的です: リソースは一度に 1 つの実行フローでしか使用できません
        要求と保留の条件: リソースの要求によって実行フローがブロックされた場合、取得したリソースを保持し、奪わない
        条件: 1 つ実行 ストリームによって取得されたリソースは、それが使い果たされる前に強制的に循環待機状態を奪うことはできません
        : デッドロックを回避するために、いくつかの実行ストリーム間で head-to-tail 循環待機リソース関係が形成されます
. デッドロック
        を打破するために必要な 4 つの条件
        順次整合性により、ロックが解除されていない         シーン リソースの 1 回限りの割り当て
        が回避されます

デッドロック回避アルゴリズム
デッドロック検出アルゴリズム
バンカーのアルゴリズム

13. Linux スレッドの同期

条件変数
スレッドが変数への排他的アクセスを持っている場合、他のスレッドが状態を変更するまで、スレッドは何もできないことに気付く場合があります。
たとえば、スレッドがキューにアクセスし、キューが空であることを検出した場合、他のスレッドがノードをキューに追加するまでしか待機できません。この場合、条件変数が使用されます。

凡例の説明:

条件変数関数の初期化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL

破壊

int pthread_cond_destroy(pthread_cond_t *cond)

条件が満たされるのを待つ

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释

起きて待って

//一次唤醒一批线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//一次唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);

テストコード: 

#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using namespace std;

//对条件变量和锁进行初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int tickets = 1000;
void* getTickets(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        cout << name << " -> " << tickets-- << endl;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t[5];
    for(int i = 0; i < 5; i++)
    {
        char *name = new char[64];
        snprintf(name,64,"Thread %d",i+1);
        pthread_create(t+i,nullptr,getTickets,name); 
    }
    while(true)
    {
        sleep(1);
        //唤醒线程:
        pthread_cond_signal(&cond);
    }
    for(int i = 0; i < 5; i++)
    {
        pthread_join(t[i],nullptr);
    }
    return 0;
}

スクリーンショットを実行します。

上の図からわかるように、条件変数が追加されると、各スレッドは順番にチケットを取得しています!
同期の概念と競合状態
同期: データのセキュリティを確保することを前提として、スレッドが特定の順序で重要なリソースにアクセスできるようにすることで、同期競合状態と呼ばれる飢餓の問題を効果的に回避します: タイミングの問題、プログラムの例外、これを競合状態と呼び
ます. スレッドシナリオでは、この種の問題は理解するのが難しくありません

14. 生産者消費者モデル

14.1 生産者消費者モデルを使用する理由

生産者と消費者のモデルでは、コンテナーを使用して、生産者と消費者の間の強い結合の問題を解決します。プロデューサとコンシューマは互いに直接通信しませんが、ブロッキング キューを介して通信します. したがって、データを生成した後、プロデューサはコンシューマがデータを処理するのを待つ必要はありませんが、データをブロッキング キューに直接投げます. コンシューマはプロデューサにデータを要求しませんデータですが、プロデューサーとコンシューマーの処理能力のバランスをとるバッファーに相当するブロッキング キューから直接取得します。このブロッキング キューは、プロデューサーとコンシューマーを分離するために使用されます。

14.2 生産者消費者モデル

「321」の原則として要約すると、次のようになります。

3 種類の関係: 生産者と生産者の相互排除、消費者と消費者の相互排除、生産者と消費者の相互排除と同期

2 つの役割: 生産者スレッド、消費者スレッド

1 取引場所: 特定の構造を持つバッファー

 写真のように:

14.3 生産者消費者モデルの利点

プロデューサとコンシューマが分離され、プロデューサとコンシューマが不均一に忙しくなり、効率が向上します (同時実行をサポートします)

並行操作をサポートする場合、パブリック リソースにアクセスするコンシューマーとプロデューサーが関与するため、相互排除を保証する必要があり、相互排除により、プロデューサーまたはコンシューマーがパブリック リソースを常に占有するという問題が発生するため、並行性が保証されないため、この問題を解決するには、条件変数を使用する必要があります。

14.4 BlockingQueue に基づく生産者と消費者のモデル

ブロッキング キュー (ブロッキング キュー) は、マルチスレッド プログラミングでプロデューサー モデルとコンシューマー モデルを実装するために一般的に使用されるデータ構造です。通常のキューとの違いは、キューが空の場合、要素がキューに配置されるまで、キューから要素を取得する操作がブロックされ、キューがいっぱいになると、要素をキューに格納する操作もブロックされることです。要素がキューから取り出されるまでブロックされます (上記の操作は異なるスレッドに基づいており、ブロッキング キュー プロセスで操作する場合、スレッドはブロックされます)

C++ キューは、ブロッキング キューの生産消費モデルをシミュレートします。

//BlockQueue.hpp:
#include<iostream>
#include<queue>
#include<pthread.h>

using namespace std;
const int maxCapacity = 5;
template<class T>
class BlockQueue
{
public:
    BlockQueue(const int& capacity = maxCapacity)
    :_maxCapacity(capacity)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_pcond,nullptr);
        pthread_cond_init(&_ccond,nullptr);
    }

    void push(const T& in)
    {
        pthread_mutex_lock(&_mutex);
        //充当条件的是必须是while不能是if,因为可能会存在一次唤醒多个生产者线程
        //而此时只有一个空间,就可能存在异常
        while(is_full())
        {
            //pthread_cond_wait:这个函数的参数必须是正在使用的互斥锁
            //1.该函数调用的时候,会以原子性的方式,将锁释放,并将它挂起
            //2.该函数在唤醒的时候会自动重新获取你传入的锁
            pthread_cond_wait(&_pcond,&_mutex);
        }
        _q.push(in);
        //pthread_cond_signal:可以放在临界区的内部,也可以放在外部
        pthread_cond_signal(&_ccond);
        pthread_mutex_unlock(&_mutex);
    }
    void pop(T* out)
    {
        pthread_mutex_lock(&_mutex);
        //这里while判断和上面的同理
        while(is_empty())
        {
            pthread_cond_wait(&_ccond,&_mutex);
        }
        *out = _q.front();
        _q.pop();
        pthread_cond_signal(&_pcond);
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }
private:
    bool is_empty()
    {
        return _q.empty();
    }
    bool is_full()
    {
        return _q.size() == _maxCapacity;
    }
private:
    queue<T> _q;
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond;
    pthread_cond_t _ccond;
    int _maxCapacity;//队列中最大元素上线
};
//mainCP.cc
#include<iostream>
#include<pthread.h>
#include<ctime>
#include<unistd.h>
#include"BlockQueue.hpp"
using namespace std;

void* producer(void* bq_)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*> (bq_);
    while(true)
    {
        //生产活动:
        int data = rand() % 10 + 1;
        bq->push(data);
        cout << "生产数据:" << data << endl;
        sleep(1);
    }
}
void* cosumer(void* bq_)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*> (bq_);
    while(true)
    {
        //消费活动:
        int data;
        bq->pop(&data);
        cout << "消费数据:" << data << endl;
    }
}
int main()
{
    srand((unsigned int)time(nullptr));
    BlockQueue<int>* bq = new BlockQueue<int>();
    pthread_t p,c;
    pthread_create(&p,nullptr,producer,bq);
    pthread_create(&c,nullptr,cosumer,bq);

    pthread_join(p,nullptr);
    pthread_join(c,nullptr);
    delete bq;
    return 0;
}

スクリーンショットを実行します。

14.5 生産者と消費者の効率改善の問題を理解する方法

プロデューサがブロッキング キューにデータを書き込むための前提条件は、最初にデータを取得することであり、データの取得に時間がかかる場合があります。このとき、複数のスレッドが同時にデータを取得してから、データをブロッキングキュー. 同様に, コンシューマにとっては, ブロッキングキューからデータを取得した後, データを処理するのに長い時間がかかることがあります. このとき, データを並行して処理するために複数のスレッドに渡すことができるので, の効率生産者モデルと消費者モデルは、データとデータを処理する時間を取得することで改善されます!

写真のように:

15. POSIX セマフォ

15.1 より前のコードの「不十分な」場所

スレッドがクリティカル リソースにアクセスする場合、クリティカル リソースは条件を満たす必要がありますが、パブリック リソースが生産または消費の条件を満たしているかどうかは事前にわかりません。検出され、再操作してからロックを解除し、ロックの各アプリケーションにはコストがかかります。この問題を解決するには?

15.2 セマフォとは

上記の問題に対応して、公開リソースへのアクセスは全体としてロックされるのではなく、一部の公開リソースが細分化され、複数のスレッドが同時に公開リソースの一部の公開リソースにアクセスできるようになりました。パブリック リソースの小さな断片へのアクセス ブロック パブリック リソースの管理では、セマフォが導入されます。セマフォは基本的に、重要なリソース内のリソースの数を測定するために使用されるカウンターです。

今後、重要なリソースにアクセスする際に、直接ロックして確認する必要はなく、セマフォの申請を先に行えば、申請が成功すれば、確実に重要なリソースにアクセスできるようになります。アプリケーションが失敗した場合、それは重要なリソースがないことを意味します。

15.3 セマフォの特徴

セマフォは複数のスレッドで適用できるため、セマフォ自体は公開リソースであるため、セマフォに対する操作では、次の 2 つの状況を含めて原子性を確保する必要があります。

1. セマフォ -- -- 資源申請 -- 原子性保証 (P操作)

2. Semaphore++ - リソースを返す - 原子性を確保する (V 操作)

上記の操作メソッドは、PV プリミティブと呼ばれます。

15.4 セマフォの使い方

セマフォを初期化する

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

セマフォを破壊する

int sem_destroy(sem_t *sem);

セマフォを待つ

功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()

ポストセマフォ

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()

15.5 リング キューに基づく生産および消費モデル

1.リングキューのデータ構造の理解

リングキューは配列シミュレーションを採用し、モジュロ演算を使用してリング特性をシミュレートします

リング構造の開始状態と終了状態は同じで、空か満杯かの判断が難しいため、カウンターやフラグを追加することで満杯か空かを判断できます。また、空きポジションをフル状態として予約することもできます

2.リングキューに基づく生産者消費者モデルの実装方法

生産モデルと消費モデルを実現するためにリング キューが満たす必要がある条件は次のとおりです。

1. コンシューマーはプロデューサーの背後にいる必要があります - データを取得するときにコンシューマーがデータを持っている必要があることを確認するため

2. コンシューマは、プロデューサを複数の円で超えることはできません。これは、データを取得するときにコンシューマがデータを持っている必要があることを保証するためです。

3. 生産者と消費者が同じ場所にいる状況:

        a. キューが空の場合 - プロデューサーが最初に実行されます

        b. キューがいっぱいの場合 - 消費者が最初に行く

リング キューが生産モデルと消費モデルの条件を確実に実現する方法: シグナルを介して確実に実現してください!

プロデューサーは、スペース リソースを識別するためのセマフォを定義します。

コンシューマーは、データ リソースを識別するためのセマフォを定義します。

3. リング キューに基づいてプロデューサー/コンシューマー モデル コードを実装します。

3.1. 実装ロジックの詳細:

3.2 コードの実装:

#include<iostream>
#include<vector>
#include<semaphore.h>
#include<cassert>
#include<pthread.h>

using namespace std;
const int gCapcity = 10;
template<class T>
class RingQueue
{
public:
    RingQueue(const int& capcity = gCapcity)
    :_queue(capcity),_capacity(capcity)
    {
        //信号量进行初始化:
        sem_init(&_PSem,0,_capacity);
        sem_init(&_CSem,0,0);
        //对锁进行初始化:
        pthread_mutex_init(&_Pmutex,nullptr);
        pthread_mutex_init(&_Cmutex,nullptr);
        _PIndex = _CIndex = 0;
    }
    //生产者放入数据:
    void Push(const T& in)
    {
        //1.通过申请信号量来判断是否有空间:
        int n = sem_wait(&_PSem); //信号量的值减1
        assert(n == 0);
        pthread_mutex_lock(&_Pmutex);
        //2.申请信号量成功:放入数据
        _queue[_PIndex++] = in;
        _PIndex %= _capacity;
        pthread_mutex_unlock(&_Pmutex);
        //3._CSem++
        sem_post(&_CSem);//信号量的值加1
    }
    //消费者拿数据:
    void Pop(T* out)
    {
        //1.通过申请信号量来判断是否有数据:
        int n = sem_wait(&_CSem);
        assert(n == 0);
        pthread_mutex_lock(&_Cmutex);
        //2.申请信号量成功:拿出数据
        *out = _queue[_CIndex++];
        _CIndex %= _capacity;
        pthread_mutex_unlock(&_Cmutex);
        //3._PSem++;
        sem_post(&_PSem);
    }
    ~RingQueue()
    {
        //销毁信号量:
        sem_destroy(&_PSem);
        sem_destroy(&_CSem);
        //销毁锁:
        pthread_mutex_destroy(&_Pmutex);
        pthread_mutex_destroy(&_Cmutex);
    }
private:
    vector<T> _queue;
    int _capacity;
    sem_t _PSem;//标识生产者
    sem_t _CSem;//标识消费者
    int _PIndex;
    int _CIndex;
    pthread_mutex_t _Pmutex;
    pthread_mutex_t _Cmutex;
};

 コード テスト:

#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>
#include"RingQueue.hpp"
using namespace std;

void* ProductorRoutine(void* args)
{
    RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);
    while(true)
    {
        int data;
        data = rand() % 10;
        rq->Push(data);
        cout << "生产完成,生产的数据是: " << data << endl;
        sleep(1);
    }
    return nullptr;
}
void* ConsumerRoutine(void* args)
{
    RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);
    while(true)
    {
        int data;
        rq->Pop(&data);
        cout << "消费完成,消费的数据是: "  << data << endl;
    }
    return nullptr;
}
int main()
{
    srand((unsigned int)time(nullptr));
    //1.创建线程:单生产,单消费
    pthread_t tidP,tidC;
    RingQueue<int> * rq = new RingQueue<int>();
    pthread_create(&tidP,nullptr,ProductorRoutine,rq);
    pthread_create(&tidC,nullptr,ConsumerRoutine,rq);
    pthread_join(tidP,nullptr);
    pthread_join(tidC,nullptr);
    return 0;
}

スクリーンショットを実行します。

16.スレッドプール

16.1 スレッドプールの概念

スレッド プールは、一般的なマルチスレッドの実装です。プログラムの開始時に一定数のスレッドを事前に作成し、実行中のプロセスでこれらのスレッドを再利用できるため、スレッドの頻繁な作成と破棄によるパフォーマンスのオーバーヘッドを回避できます。 .

16.2 スレッドプールインスタンス

1. 一定数のスレッドプールを作成し、タスクキューからタスクオブジェクトを取得する
2. タスクオブジェクトを取得後、タスクオブジェクト内でタスクインターフェースを実行する

//Thread.hpp
#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<cstdio>
#include<pthread.h>
using namespace std;

class Thread 
{
    static void* start_routine(void* args)
    {
        Thread* _this = static_cast<Thread*> (args);
        return _this->fun();
    }
public:
    using func_t = function<void*(void*)>;
    Thread()
    {
        char buf[128];
        snprintf(buf,sizeof buf,"thread->%d",threadnum++);
        _name = buf;
    }
    void start(func_t fun,void* args = nullptr)
    {
        _fun = fun;
        _args = args;
        pthread_create(&_tid,nullptr,start_routine,this);
    }
    void join()
    {
        pthread_join(_tid,nullptr);
    }
    string threadname()
    {
        return _name;
    }
    ~Thread()
    {}
    void* fun()
    {
        return _fun(_args);
    }
private:
    string _name;//线程名
    pthread_t _tid;//线程id
    void* _args;//线程调用函数传递参数
    func_t _fun;
    static int threadnum;
};
int Thread::threadnum = 1;
//ThreadPool.hpp
#pragma once

#include<iostream>
#include<vector>
#include<queue>
#include<pthread.h>
#include<mutex>
#include"Thread.hpp"
#include"LockGuard.hpp"
using namespace std;

//将数据封装成结构体传给线程函数
template <class T>
class ThreadPoll;
template<class T>
class ThreadData
{
public:
    ThreadPoll<T>* threadpool;
    string name;
    ThreadData(ThreadPoll<T>* tp,const string& n)
    :threadpool(tp),name(n) {}
};
const int gnum = 5; //默认线程数
template<class T>
class ThreadPoll
{
private:
    static void* handlerTask(void* args)
    {
        ThreadData<T>* tp = static_cast<ThreadData<T>*>(args);
        while(true)
        {
            T t;
            {
                LockGuard lock_guard(tp->threadpool->mutex());
                while(tp->threadpool->isQueueEmpty())
                {
                    tp->threadpool->threadWait();
                }
                //将任务从公共队列中拿到自己独立的栈中
                t = tp->threadpool->pop();
            }
            std::cout << tp->name << " 获取了一个任务: " << t.toTaskString() << " 并处理完成,结果是:" << t() << std::endl;
        }
        delete tp;
        return nullptr;
    }
public:
    void lockQueue()
    {
        pthread_mutex_lock(&_mut);
    }
    void unlock()
    {
        pthread_mutex_unlock(&_mut);
    }
    bool isQueueEmpty()
    {
        return _task.empty();
    }
    void threadWait()
    {
        pthread_cond_wait(&_cond,&_mut);
    }
    T pop()
    {
        T t = _task.front();
        _task.pop();
        return t;
    }
    ThreadPoll(int num = gnum)
    :_num(num)
    {
        pthread_mutex_init(&_mut,nullptr);
        pthread_cond_init(&_cond,nullptr);
        for(int i = 0; i < _num; i++)
        {
            _threads.push_back(new Thread());
        }
    }
    pthread_mutex_t* mutex()
    {
        return &_mut;
    }
    void run()
    {
        for(const auto&t : _threads)
        {
            ThreadData<T>* tp = new ThreadData<T>(this,t->threadname());
            t->start(handlerTask,tp);
            std::cout << t->threadname() << " start ..." << std::endl;
        }
    }
    void push(const T& in)
    {
        LockGuard lock_guard(&_mut);
        _task.push(in);
        pthread_cond_signal(&_cond);
    }
    ~ThreadPoll()
    {
        pthread_mutex_destroy(&_mut);
        pthread_cond_destroy(&_cond);
        for(const auto& t: _threads)
        {
            delete t;
        }
    }
private:
    int _num;//最大开辟线程数
    vector<Thread*> _threads;
    queue<T> _task;
    pthread_mutex_t _mut;
    pthread_cond_t _cond;
};
//Task.hpp
#pragma once
#include<iostream>
#include<cstdio>
#include<functional>
#include<string>
class Task
{
public:
    using fun_c = std::function<int(int,int,char)>;
    Task() {}
    //构造任务对象:
    Task(int x,int y,char op,fun_c fun)
    :_x(x),_y(y),_op(op),_fun(fun){}
    //重载operator():对象调用任务:
    std::string operator()()
    {
        int result = _fun(_x,_y,_op);
        //结果转换成字符串返回:
        char buf[64];
        snprintf(buf,sizeof buf,"%d %c %d = %d",_x,_op,_y,result);
        return buf;
    }
    std::string toTaskString()
    {
        int result = _fun(_x,_y,_op);
        char buf[64];
        snprintf(buf,sizeof buf,"%d %c %d = ?",_x,_op,_y);
        return buf;
    }
private:
    int _x;
    int _y;
    char _op;
    fun_c _fun;
};
const string oper = "+-*/%";
int mymath(int x,int y,char op)
{
    int result = 0;
    switch(op)
    {
    case '+':
        result = x + y;
        break;
    case '-':
        result = x - y;
        break;
    case '*':
        result = x * y;
        break;
    case '/':
    {
        if(y == 0)
        {
            std::cerr<<"div 0 error" << endl;
            result = -1;
        }
        else
        result = x / y;
    }
        break;
    case '%':
    {
        if(y == 0)
        {
            std::cerr<<"mod 0 error" << endl;
            result = -1;
        }
        else
        result = x % y;
    }
        break;
    }
    return result;
}
//LockGuard.hpp
#include<iostream>
#include<pthread.h>

using namespace std;

class Mutex
{
public:
    Mutex(pthread_mutex_t* lock_p = nullptr):lock_p_(lock_p)
    {}
    void lock()
    {
        if(lock_p_)
            pthread_mutex_lock(lock_p_);
    }
    void unlock()
    {
        if(lock_p_)
            pthread_mutex_unlock(lock_p_);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t* lock_p_;
};
class LockGuard
{
public:
    LockGuard(pthread_mutex_t* mutex):mutex_(mutex)
    {
        mutex_.lock(); //在构造函数中加锁
    }
    ~LockGuard()
    {
        mutex_.unlock();//在析构函数中解锁
    }
private:
    Mutex mutex_;
};
//Test.cpp
#include<iostream>
#include<pthread.h>
#include<memory>
#include<cstdlib>
#include<ctime>
#include<unistd.h>
#include"ThreadPool.hpp"
#include"Task.hpp"
using namespace std;

int main()
{
    srand((unsigned int)time(nullptr));
    unique_ptr<ThreadPoll<Task>> tp(new ThreadPoll<Task>());
    tp->run();
    while(true)
    {
        int x = rand() % 10;
        int y = rand() % 5;
        char op = oper[rand()%oper.size()];
        Task t(x,y,op,mymath);
        tp->push(t);
        sleep(1);
    }
    return 0;
}

スクリーンショットを実行

17. スレッドセーフなシングルトン パターン

17.1 シングルトンパターンとは

定番でよく使われるデザインパターンです

17.2 デザインパターンとは

いくつかの古典的な一般的なシナリオでは、いくつかの対応するソリューションが与えられた場合、これは設計パターンです

17.3 シングルトンパターン

オブジェクト インスタンスを 1 つだけ持つ必要がある特定のクラスは、シングルトンと呼ばれます。

17.3.1 シングルトンモードを実現するハングリーな方法

template <typename T>
class Singleton {
    static T data;
public:
    static T* GetInstance() {
        return &data;
    }
};

T オブジェクトがラッパー クラス Singleton を介して使用されている限り、プロセスには T オブジェクトのインスタンスが 1 つだけ存在します。

17.3.2 シングルトン パターンを実装する怠惰な方法

template <typename T>
class Singleton 
{
    static T* inst;
public:
    static T* GetInstance() 
    {
        if (inst == NULL) 
        {
            inst = new T();
        }
        return inst;
    }
};

重大な問題があります。スレッドは安全ではありません。
初めて GetInstance を呼び出すときに、2 つのスレッドが同時に呼び出すと、T オブジェクトの 2 つのインスタンスが作成される可能性があります。

シングルトンモードを実現する怠惰な方法 (スレッドセーフバージョン)

// 懒汉模式, 线程安全
template <typename T>
class Singleton {
	volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
	static std::mutex lock;
public:
	static T* GetInstance() {
		if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
			lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
			if (inst == NULL) {
				inst = new T();
			}
			lock.unlock();
		}
		return inst;
	}
};

17.3.3 ハングリー・マン・モードとレイジー・マン・モードの長所と短所

飢えた男モード

利点: マルチスレッドで同時実行性の高い環境で使用すると、リソースの競合を回避でき、対応する速度を向上させることができます

欠点:

1.初期化データが多すぎると、起動が遅くなります

2. 複数のシングルトン クラスの初期化に依存関係がある場合、シーケンスを制御できません

レイジーモード:

アドバンテージ:

  1. リソースの節約: プログラムの開始時にオブジェクトが作成されず、必要なときにのみオブジェクトが作成されるため、リソースを節約できます。
  2. より高いスレッド セーフ: レイジー マン モードは、最初に呼び出されたときにオブジェクトを作成するため、マルチスレッド環境では 1 つのスレッドのみがオブジェクトを作成できるため、スレッド セーフが保証されます。
  3. 遅延初期化: レイジー モードは必要に応じて再初期化できるため、不必要な初期化を回避できます。

欠点:

  1. スレッドが安全でない: 複数のスレッドが同時に getInstance() メソッドを呼び出すと、複数のインスタンスが作成され、シングルトン パターンが壊れる可能性があります。
  2. パフォーマンスの問題が発生する可能性があります。getInstance() メソッドが呼び出されるたびに、ロックを追加する必要があり、プログラムのパフォーマンスに影響を与える可能性があります。
  3. 複雑な実装: スレッドの同期とスレッドの安全性の問題を考慮する必要があり、実装はより複雑です。

要約すると、レイジーマンモードは、オブジェクトの作成が比較的リソースを大量に消費する場合に使用するのに適しており、プログラムの実行中に必ずしも作成する必要はありませんが、スレッドの安全性の問題に注意を払う必要があります。

18. STL、スマートポインタ、スレッドセーフ

18.1 STL のコンテナはスレッドセーフですか?

その理由は、STL の本来の目的はパフォーマンスを最大化することであり、スレッドの安全性を確保するためにロックを使用すると、パフォーマンスに大きな影響を与えるためです.また、異なるコンテナーでは、ロック方法
によってパフォーマンスが異なる場合があります.
(ハッシュ テーブルのロック テーブルやロック バケットなど) したがって
、STL は既定ではスレッド セーフではありません。

18.2 スマートポインタはスレッドセーフですか?

unique_ptr については、現在のコード ブロックのスコープ内でのみ有効になるため、スレッド セーフの問題は発生しません.shared_ptr については、
複数のオブジェクトが参照カウント変数を共有する必要があるため、スレッド セーフの問題が発生します.ただし、この問題は、標準ライブラリの実装時に考慮されるのは、アトミック操作 (CAS) メソッドに基づいて、shared_ptr が効率的かつアトミック操作の参照カウントを行えるようにすることです。

19. その他の一般的なロック

悲観的ロック: データをフェッチするたびに、データが他のスレッドによって変更されることを常に心配しているため、データをフェッチする前にロック (読み取りロック、書き込みロック、行ロックなど) を追加します。データがブロックされ、ハングします。
楽観的ロック: データがフェッチされるたびに、データが他のスレッドによって変更されないことが常に楽観的であるため、ロックされません。ただし、データを更新する前に、他のデータが更新前のデータを変更したかどうかを判断します。主な方法として、バージョン番号メカニズムと CAS 操作の 2 つがあります。
CAS 演算:データの更新が必要な場合、現在のメモリ値が以前に取得した値と等しいかどうかを判断します。等しい場合は、新しい値で更新します。等しくない場合は失敗し、失敗した場合は再試行されます. 通常、これはスピン プロセスです。
スピンロック、フェアロック、アンフェアロック?

スピンロックの紹介:

クリティカル リソースの適用に成功したスレッドがクリティカル領域に長時間留まる場合、スピン ロックを使用して効率を向上させることができます. 時間の長さの測定は、特定のシナリオでプログラマによって評価されます.

スピンロック共通インターフェースの紹介:

スピンロックの作成と破棄:

#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

スピン ロック ロックのインターフェイス:

#include <pthread.h>

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);

スピンロックによってロック解除されるインターフェイス:

#include <pthread.h>
int pthread_spin_unlock(pthread_spinlock_t *lock);

20. リーダーライターモデル

20.1 リーダー/ライター モデルの概念的理解

消費者と消費者の関係は相互に排他的であり、リーダーとリーダーは相互に排他的であるため、生産と消費者のモデルと比較して、リーダーとライターのモデルの関係は、消費者と消費者とリーダーとリーダーの間で異なります。コンシューマーはデータを取得しますが、リーダーはデータを移動しないため、それらの間に関係はありません。したがって、この機能では、リーダーとライターのモデルは、通常、ほとんどの時間の読み取りと少量の書き込みに使用されます。時間入ります。

20.2 読み書きロック

読み取り/書き込みロックは、コードの書き込み中にリーダーとライターを保証する機能です

注: 書き込み専用、読み取り共有、読み取りロックの優先度が高い 

読み書きロック インターフェイス

初期化と破棄

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t
*restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

ロックとロック解除

//读加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//写加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

20.3 読み書きロックのロックとロック解除の疑似コード例

21. まとめ

以上、スレッドを概念から使用まで理解する過程で遭遇するスレッドセーフの問題を解決する方法と、マルチスレッドで書かれたプロダクションモデルとコンシューマーモデル、そしてリーダーライターモデルを紹介しました。 、あなた スレッドの理解と使用は非常に役立ちます!

おすすめ

転載: blog.csdn.net/qq_65307907/article/details/129474658