C++ 戦闘 - Linux マルチスレッド (習熟度への入門)

目次

スレッドの概念

Linux カーネルスレッドの実装原理

プロセスとスレッドの違いのまとめ

スレッド間の共有および非共有リソース

スレッドの長所と短所

スレッド関連の関数 (スレッド制御プリミティブ)

ねじ山特性

スレッド使用上の注意

スレッド同期

ミューテックス(ミューテックスロック)

デッドロック

読み書きロック

条件変数

生産者消費者モデル

Producer Consumer モデルの実装 (条件変数版)

信号量

Producer Consumer モデルの実装 (セマフォ版)

ファイルロック


スレッドの概念

1. プロセスと同様に、スレッドは、アプリケーションが複数のタスクを同時に実行できるようにするメカニズムです。プロセスには複数のスレッドを含めることができます。同じプログラム内のすべてのスレッドは、同じプログラムを個別に実行し、初期化されたデータ セグメント (.data)、初期化されていないデータ セグメント (.bss)、およびスタック メモリ セグメントを含む同じグローバル メモリ領域を共有します。

[注: 共有スタック メモリとコード セグメントはありません]

2. プロセスは CPU によって割り当てられるリソースの最小単位であり、スレッドはオペレーティング システムによるスケジューリング実行の最小単位です。

3. スレッドは軽量プロセス (LWP: light weight process) であり、Linux 環境におけるスレッドの本質は依然としてプロセスです。

4. 指定したプロセスの LWP を表示します: ps -lf pid (注: lwp はスレッドの ID ではありません)

Linux カーネルスレッドの実装原理

1. 軽量プロセス (軽量プロセス) にも PCB があり、スレッドを作成するために使用される基になる関数は、クローンであるプロセスと同じです。

2. カーネルの観点からは、プロセスとスレッドは同じで、それぞれに異なる PCB がありますが、PCB のメモリ リソースを指す 3 レベルのページ テーブルは同じです。

3 レベルのマッピング: プロセス PCB --> ページ ディレクトリ (配列であることがわかります。最初のアドレスは PCB にあります) --> ページ テーブル --> 物理ページ --> メモリ ユニット

3. プロセスはスレッドに変換できます (上記の仮想アドレス空間を見て分析できます)。

4. スレッドは、レジスタとスタックの集合と見なすことができます

5. LWP とプロセス ID の違い: LWP 番号は Linux カーネルがタイム スライスをスレッドに分割するための基礎であり、スレッド ID はプロセス内のスレッドを区別するためのものです。

a

6. プロセスの場合、競合することなく、同じアドレスが異なるプロセスで繰り返し使用されます。その理由は、それらの仮想アドレスは同じですが、ページ ディレクトリ、ページ テーブル、および物理ページが異なるためです。同じ仮想アドレスが異なる物理ページ メモリ ユニットにマップされ、最終的に異なる物理ページにアクセスします。

ただし、スレッドは異なります。2 つのスレッドには独立した PCB がありますが、同じページ ディレクトリ、ページ テーブル、および物理ページを共有します。したがって、両方の PCB が同じアドレス空間を共有します。

7. プロセスを作成する fork() であれ、スレッドを作成する pthread_creat() であれ、基礎となる実装はカーネル関数 clone() を呼び出します。相手のアドレス空間をコピーしている場合は「プロセス」が生成され、相手のアドレス空間を共有している場合は「スレッド」が生成されますしたがって、Linux カーネルはプロセスとスレッドを区別しません。ユーザーレベルで区別するだけです。したがって、すべてのスレッドの操作関数 pthrad_* は、システム コールではなくライブラリ関数です。

プロセスとスレッドの違いのまとめ

1. 工程間での情報共有が難しい。親プロセスと子プロセスは読み取り専用コード セグメントを除いてメモリを共有しないため、プロセス間で情報を交換するには、何らかの形式のプロセス間通信を使用する必要があります。

2. プロセスを作成するために fork() を呼び出すコストは比較的高くなります (アドレス空間のコピー)。「コピー オン ライト」メカニズムが採用されている場合でも、メモリ ページ テーブルやファイル記述子テーブルなどのさまざまなプロセス属性は依然としてをコピーする必要があります。つまり、fork() 呼び出しの時間オーバーヘッドは依然として高価です。

3. スレッドは情報を便利かつ迅速に共有できます。データを共有 (スタックは機能していません。上の図を参照) 変数にコピーするだけです。

4. 通常、スレッドの作成は、プロセスの作成よりも 10 倍以上高速です。仮想アドレス空間はスレッド間で共有され、コピーオンライトを使用してメモリをコピーする必要はなく、ページテーブルをコピーする必要もありません (仮想アドレス空間の実現は実際には4G のメモリを割り当て、対応するデータ構造、ページ テーブル、ページ ディレクトリを実装するだけで済みます)

スレッド間の共有および非共有リソース

この写真は重要すぎるので、もう一度見てみましょう

リソースを共有する 非共有リソース
ファイル記述子テーブル スレッドID
各信号の処理方法 プロセッサ コンテキストとスタック ポインタ (カーネル スタック)
現在の作業ディレクトリ、ファイルのアクセス許可 独立スタック空間(ユーザー空間スタック)
ユーザー ID とグループ ID とセッション ID errno 変数
仮想アドレス空間 (stack.txt を除く) シグナルマスクワード
スケジューリングの優先度

NPTL (サードパーティ ライブラリ) の導入:

        Linux が最初に開発されたとき、カーネル内のスレッドは実際にはサポートされていませんでした。プロセスは、clone() システム コールを介してスケジュール可能なエンティティです。この呼び出しは、呼び出しと同じアドレス空間を共有する呼び出しプロセスのコピーを作成します。しかし、これは POSIX の要件に準拠しており、信号処理、スケジュール間の同期などに問題があります。

        NPTL またはネイティブ POSIX スレッド ライブラリ。標準に準拠しながら、LinuxThreads の欠点を克服する LinuxThreads の新しい実装です。

        現在のプロセス ライブラリのバージョンを表示します: getconf GNU_LIBPTHREAD_VERSION

スレッドの長所と短所

利点: 1. プログラムの並行性が向上する 2. オーバーヘッドが小さい 3. 便利なデータ通信とデータ共有

短所: 1. ライブラリ関数、不安定 3. デバッグと書き込みが困難 (gdb はサポートしていません) 4. シグナル サポートが不十分

利点は比較的顕著であり、欠点は欠点ではありません。Linuxでは、実装方法により、プロセスとスレッドの違いはそれほど大きくありません

スレッド関連の関数 (スレッド制御プリミティブ)

一般に、メイン関数が配置されているスレッドはメイン スレッドと呼ばれます。作成された残りのスレッドは、子スレッドと呼ばれます。

fork() 関数 ---> プロセスを作成

プログラムにはデフォルトで 1 つのスレッドしかなく、pthread_create() 関数呼び出し --> 2 つのスレッド


pthread_create 関数

        int pthread_create(pthread_t *thread,const pthread_attr_t *attr,

                                        void*(*start_routine)(void*),void *arg);

        機能: 子スレッドを作成する

        パラメータ:

                   スレッド: 送信パラメーター。スレッドが正常に作成された後、子スレッドのスレッド ID がこの変数に書き込まれます。

                   attr はスレッドの属性を設定します。通常はデフォルト値 NULL を使用します

                   start_routine: 関数ポインタ。この関数は、子スレッドが処理する必要があるロジック コードです。

                   arg: 3 番目のパラメーターに使用されます

        戻り値:

                   成功: 0

                   失敗しました: エラー番号を返します

補足:スレッドはNPTLライブラリ経由のサードパーティ製ライブラリのため、弊社システムコールとは異なります。

                  以前は perror(ret) を使用していましたが、現在は使用できません。char *strerror(int errnum) を使用してください。 

 

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

void *func(void *arg)
{
    printf("pthread:%lu\n",pthread_self());
    return NULL;
}

int main(void)
{

    //typedef unsigned long 
    pthread_t tid;

    int ret = pthread_create(&tid,NULL,func,NULL);
    if(ret)
    {
        printf("%s\n",strerror(ret));
        exit(-1);
    }
    sleep(1);
    printf("我是主线程:%d\n",getpid());

    return 0;
}

コンパイルに関する注意事項: スレッド ペッパーはサードパーティ ライブラリを介して実装されます

g++ createPthread.cpp -o createPthread -lpthread(-pthread)


pthread_exit 関数

        int pthread_exit(void *retval);

        機能: 終了するスレッドに代わって呼び出されるスレッドを終了する

        パラメータ:

                retval: pthread_join() で取得できる戻り値としてポインターを渡す必要があります。

                           スレッドの終了ステータス。通常は NULL を渡します

       補足:メインスレッドが終了すると、他の正常に実行されているスレッドには影響しません

                  スレッド内で exit 関数を使用することは禁止されています。これにより、プロセス内のすべてのスレッドが終了します。

        

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void *func(void *arg)
{
   int i = (int)arg;
   if(i == 2)
   {
        pthread_exit(NULL);
   }
   sleep(2);

    printf("我是第%d号线程,线程ID:%lu\n",i,pthread_self());

   return NULL;
}

int main(void)
{

    pthread_t tid;
    for(int i=0;i<5;++i)
    {
        pthread_create(&tid,NULL,func,(void *)i);
    }

    sleep(5);
    printf("我是主线程,线程ID:%lu\n",pthread_self());

    return 0;
}

pthread_self 関数

        pthread_t pthread_self(void)

        機能: 現在のスレッドのスレッド ID を取得する


pthread_equal 関数

        int pthread_equal(pthread_t t1,pthread_t t2);

        機能: 2 つのスレッド ID が等しいかどうかを比較する

        戻り値: 非ゼロに等しい、0 に等しくない

        補足: オペレーティング システムが異なれば、pthread_t 型の実装も異なります。一部は符号なし long 整数であり、一部は構造体である可能性があります。      

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_t tid_one;

void *func(void *arg)
{

    if(pthread_equal(tid_one,pthread_self()))
    {
        printf("相等\n");
    }
    else
    {
        printf("不相等\n");
    }

}

int main(void)
{

    pthread_t tid;
    
    pthread_create(&tid_one,NULL,func,NULL);

    pthread_join(tid_one,NULL);

    return 0;
}

pthread_join 関数

        int pthread_join(pthread_t スレッド、void ** retval);

        機能: 終了したスレッドに接続し、リソースを再利用する

                   子プロセスのリソースをリサイクルする

                   この関数はブロックされており、一度に 1 つの子プロセスしかリサイクルできません

                   通常、メインスレッドで使用されます

        パラメータ: リサイクルが必要な子プロセスのスレッド ID

                   retval は、子プロセスが終了したときに戻り値を受け取ります

        戻り値:

                   0 成功

                   0 以外、失敗、エラー番号を返す


糸の分離

        int pthread_detach(pthread_t スレッド);

        機能: スレッドを分離します。切り離されたスレッドが終了すると、自動的にリソースが解放され、システムに返されます。

        予防:

                1). 複数回分離できず、予期しない動作が発生する

                2). 分離されたスレッドに接続できず、エラーが報告されます [pthread_join]

        パラメータ:

                デタッチする必要があるスレッドの ID

        戻り値:

                成功 0 失敗 エラー番号

ハイライト:

        スレッド分離状態: この状態を指定すると、スレッドはメイン制御スレッドからアクティブに切断されます。スレッドの終了後、その終了ステータスは他のスレッドによって取得されるのではなく、スレッド自体によって自動的に直接解放されます。一般的に使用されるネットワーク、マルチスレッド サーバー。

        プロセスにそのようなメカニズムがある場合, ゾンビ プロセスは生成されません. ゾンビ プロセスが生成される主な理由は, プロセスが停止した後, ほとんどのリソースが解放され, 一部の残りのリソースがシステム内に残っているためです.プロセスがまだ存在しているとカーネルが考える

        通常の状況では、スレッドが終了した後、他のスレッドが pthread_join を呼び出してその状態を取得するまで、その終了状態が保持されますが、スレッドを切り離し状態に設定することもできます. そのようなスレッドが終了すると、占有していたすべてのリソースがすぐに回復されます終了状態のままにします。すでに切断状態にあるスレッドで pthread_join を呼び出すことはできません. そのような呼び出しは EINVAL エラーを返します.

#include <stdio.h>
#include <pthread.h>

void* func(void *arg)
{
    printf("我是子线程:%lu\n",pthread_self());
    return NULL;
}

int main(void)
{
    pthread_t tid;
    pthread_create(&tid,NULL,func,NULL);

    //设置线程分离
    pthread_detach(tid);

    sleep(3);

    return 0;
}

スレッドのキャンセル

        int pthread_cancel(pthread_t スレッド);

        機能: スレッドをキャンセルする (スレッドを終了させる)

                   プロセスをキャンセルすると、スレッドの実行が終了する可能性があります。

                   ただし、スレッドはすぐに終了するのではなく、子スレッドがキャンセル ポイントまで実行されたときにのみ終了します。

                   解除ポイント:システムコール(ユーザーモードからカーネルモードへの切り替え時) creat open pause read write..

               スレッドの最後にキャンセル ポイントがない場合は、pthread_testcancel 関数を呼び出してキャンセル ポイントを設定できます。

        キャンセルされたスレッドの場合、終了値は Linux pthread ライブラリで定義されています。定数 PTHREAD_CANCELED の値は -1 です。その定義は pthread.h にあります。

        

        したがって、pthread_join を使用してキャンセルされたスレッドをリサイクルすると、戻り値 -1 が返されます。

#include <stdio.h>
#include <pthread.h>

void *func(void *arg)
{

    //printf("我是子线程:%lu\n",pthread_self()); //printf会产生一个系统调用

    pthread_testcancel();   //自己设置一个取消点


    return NULL;
}

int main(void)
{

    pthread_t tid;

    pthread_create(&tid,NULL,func,NULL);

    //取消这个线程
    pthread_cancel(tid);

    //回收
    int childRet;
    pthread_join(tid,(void **)&childRet);   //阻塞的
    printf("回收的返回值:%d\n",childRet);  

    return 0;
}

        

ねじ山特性

Linux でのスレッド プロパティは、実際のプロジェクト要件に従って設定できます. スレッドのデフォルト プロパティについては以前に説明しました. デフォルトのプロパティは、すでにほとんどの問題を解決しています。プログラムのパフォーマンスに対してより高い要件を提示する場合は、スレッド属性を設定する必要があります.たとえば、スレッドスタックのサイズを設定してスレッドの最大数を増やすことにより、メモリ使用量を減らすことができます.

主な属性: スコープ、スタック サイズ、スタック アドレス、優先度、分離状態、スケジューリング ポリシー

スレッドの属性値を直接設定することはできず、関連する関数 (インターフェイスとして理解できる) を介して操作する必要があります。

int pthread_attr_init(pthread_attr_t *attr); //スレッド属性変数の初期化
int pthread_attr_destroy(pthread_attr_t *attr); //スレッド属性のリソースを解放する
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); //スレッド切り離しのステータス属性を取得
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); //スレッド分離の状態属性を設定します

スレッド属性メソッドを表示: man pthread_attr_XXX

場合:

        //スレッド属性変数を作成する

          pthread_attr_t 属性;

        //属性変数を初期化する

          pthread_attr_init(&attr);

        // プロパティを設定

        pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);

        //スレッドスタックのサイズを設定

        pthread_attr_setstacksize(&attr,size);

        .....

#include <stdio.h>
#include <pthread.h>
#include <string.h>
void* func(void *arg)
{
    printf("子线程:%lu\n",pthread_self());
    return NULL;
}

int main(void)
{

    pthread_t tid;
    
    //创建线程属性变量
    pthread_attr_t attr;
    //初始化
    pthread_attr_init(&attr);
    //设置线程分离
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    //设置栈大小
    int size = 256*1024;
    pthread_attr_setstacksize(&attr,size);

    pthread_create(&tid,&attr,func,NULL);
    
    while(1)
    {
        sleep(1);
        void* retval;
        int err = pthread_join(tid,&retval);
        if(err)
            printf("-------------err= %s\n", strerror(err));
        else
            printf("-----------%d\n",(int)retval);
    }
    

    return 0;
}

スレッド使用上の注意

1. メイン スレッドが終了し、他のスレッドは終了せず、メイン スレッドは pthread_exit を呼び出します。

2.ゾンビスレッドを避ける

        pthread_join

        pthread_detach

        スレッド属性をseparateに設定してから、pthread_create

3. malloc および mmap によって要求されたメモリは、他のスレッドによって解放される可能性があります

4. マルチスレッド モデルで fork を呼び出すのは、すぐに実行しない限り避けてください。子プロセスには frok を呼び出すスレッドのみが存在し、他のすべてのスレッド pthread_exit は子プロセスに存在します。

5. シグナルの複雑なセマンティクスはマルチスレッドと共存することが困難であり、マルチスレッドへのシグナルメカニズムの導入は避けるべきです

スレッド同期

最初に同期の概念について話しましょう (冗長に感じないでください。スレッド同期を理解すると便利です)。

        いわゆる同期は、研究対象ごとに異なる意味を持ちます。例: デバイスの同期とは、2 つのデバイス間で共通の時間基準を指定することを指します。Qin Shihuang の「同じテキストの本と同じトラックの車」も一種のシンクロではないでしょうか。プログラミングにおける同期とは、主にステップを調整して所定の順序で実行することを目的とした、調整、支援、および相互協力を指します

スレッド同期

        同期は、あらかじめ決められた順序で実行される調整されたステップです。主に同じプロセス内のスレッドがリソースを共有しているため、事前に決められた順序を強調する理由と、スレッドが各データを変更する必要があると仮定した場合の同時実行の理由について考えたことはありますか?変更が完了する前に、別のスレッドがそれを取り出しますが、それは問題を引き起こしますか?

        より専門的に言うと、スレッド同期とは、スレッドが関数呼び出しを発行したときに、結果が得られるまで呼び出しが返されないことを意味します。同時に、データの一貫性を確保するために、他のスレッドがこの関数を呼び出すことはできません。(スレッドは共有リソースの呼び出しを完了しておらず、他のスレッドはそれを呼び出すことができません)

        

詳細な分析:

1. スレッドの主な利点は、グローバル変数を通じて情報を共有できることです。ただし、この便利さ (便利さはプロセス間通信と比較してください) 共有には代償が伴います. 複数のスレッドが同じ変数を同時に変更しないこと、またはスレッドが変更されている変数をスレッドが読み取らないことを保証する必要があります。他のスレッド (両方を同時に読むことができることがわかります)。

2. クリティカル セクションは、共有リソースにアクセスするコード フラグメントを指し、このコードの実行はアトミック操作である必要があります。つまり、同じ共有リソースに同時にアクセスする他のスレッドがフラグメントの実行を中断してはなりません。

3.スレッドがメモリ上で動作している場合、他のスレッドはこのメモリ アドレス プロセスを操作できません.スレッドが操作を完了するまで、他のスレッドはメモリ上で操作できますが、他のスレッドは待機状態になります.

では、このアトミック操作をより適切に維持するにはどうすればよいでしょうか?

ミューテックス、セマフォ、XXX ロック... メカニズム

多くの場合、開発は特定の問題を解決するためのものであることを発見したかどうかはわかりません。どんな状況下でも、完璧であることは難しい、あるいは完璧な円を描くことは難しい. 私たちは常に革新を続け、この完璧な円に近づきました. π は数え切れないようです。

ミューテックス(ミューテックスロック)

最初に共通の理解についてお話ししましょう。今は部屋があり、この部屋は一度に 1 人しか収容できません。2 人以上が入室するのを防ぐために、この部屋の鍵を購入してください。誰かが入室すると、ゲートキーパーが人は部屋に鍵をかけ、人が出てきたら鍵を開ける。他人が入ってきたら、鍵を閉める、それだけです。実際、コンピューターの問題に対する解決策の多くは、私たちの現実の生活に大きく関係しています。

1. スレッドが共有変数を更新 (変更) する際の問題を回避するために、mutex を使用して、同時に 1 つのスレッドだけが共有リソースにアクセスできるようにすることができます。ミューテックスを使用して、任意の共有リソースへのアトミック アクセスを保証できます。

2. ミューテックスには、ロック (ロック) とロック解除 (ロック解除) の 2 つの状態があります。一度に最大 1 つのスレッドがミューテックスをロックできます。既にロックされているミューテックスを再ロックしようとすると、ロック時に使用される方法によっては、スレッドがブロックされるか、エラーで失敗する場合があります。(上記の例に戻りましょう。管理者は一度に 1 つのロックのみを開きます。2 つのロックが追加された場合、部屋にいる人は出られず、部屋の外にいる人は入ることができず、管理者は既にロックを開いています。 、彼は部屋が空っぽで、誰かが入るのを待っていると思っています

3. スレッドがミューテックスをロックすると、すぐにミューテックスの所有者になり、所有者だけがミューテックスをロック解除できます。一般に、共有リソースごとに異なるミューテックスが使用され、各スレッドは同じリソースにアクセスするときに次のプロトコルを使用します。

  •         共有リソースのミューテックスをロックする (ロック)
  •         共有リソースへのアクセス (アクセス)
  •         ミューテックスのロックを解除 (ロック解除)

4. 複数のスレッドがこのコード (クリティカル セクション) を実行しようとすると、実際には 1 つのスレッドだけがミューテックスを保持できます (他のスレッドはブロックに遭遇します)。つまり、同時にこのコード領域に入ることができるのは 1 つのスレッドだけです。

例えば:

        「ロック」することにより、リソースへのアクセスが長くなり、相互に排他的になり、時間関連のエラーが発生しなくなります (期待される順序で実行されます)。

 説明: スレッド A がグローバル変数をロックしてアクセスすると、B はアクセスする前にそれをロックしようとしますが、ロックを取得できない場合、B はブロックします。C スレッドはロックせず、グローバル変数に直接アクセスします。グローバル変数には引き続きアクセスできますが、データの混乱が発生します。

したがって、ミューテックスは本質的に「推奨されるロック」(「協調ロック」とも呼ばれます) です。複数のスレッドがプログラム内の共有リソースにアクセスする場合は、このメカニズムを使用することをお勧めしますが、必須ではありません。(つまり、あるスレッドが共有リソースにアクセスする前に、ロックにアクセスするのではなく、共有リソースに直接アクセスし、アクセスすることもできます。データの混乱を防ぐために、所定の手順に従う必要があります。ミューテックスを使用する場合. 私は部屋に直接行きます, 管理者がいるかどうかは気にしません)

関連機能

ミューテックスのタイプ: pthread_mutex_t

pthread_mutex_init 関数        

        int pthread_mutex_init(pthread_mutex_t *ミューテックスを制限し、

                                             const pthread_mutexattr_t *restrict attr);

        役割: ミューテックスを初期化する

        パラメータ: mutex 初期化する必要があるミューテックス変数

                   attr Mutex 関連の属性で、通常は NULL を渡します

        restrict: C 言語の修飾子。変更されたポインターは別のポインターによって操作できません。

pthread_mutex_destroy 関数

        int pthread_mutex_destroy(pthread_mutex_t *mutex);

        役割: ミューテックスのリソースを解放する

pthread_mutex_lock 関数

        int pthread_mutex_lock(pthread_mutex_t *mutex);

        機能: ロック、ブロック (スレッドがロックされている場合、他のスレッドはブロックして待機することしかできません)

pthread_mutex_trylock 関数

        int pthread_mutex_trylock(pthread_mutex_t *mutex);

        機能: ロックを試みる、ノンブロッキング (ロックに失敗した場合、ブロックせずに直接戻ります)

pthread_mutex_unlock 関数

       int pthread_mutex_unlock(pthread_mutex_t *mutex);

        機能: ロックを解除

最後のケース: (目標 -> HELLO WORLD または hello world を完全に印刷できるようにする)

       ミューテックスなし:

                

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex;      //定义为全局变量,不能定义为栈上的临时变量

void *func(void *arg)
{

    srand(time(NULL));
    while(1)
    {
        //pthread_mutex_lock(&mutex); //加锁
        printf("hello");
        sleep(rand()%3);
        printf("world\n");
        //pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
       
    }

    return NULL;
}

int main(void)
{

    int n = 5;
    pthread_t tid;
    srand(time(NULL));  //设置随机种子

    //初始化互斥量,在创建线程之前
    pthread_mutex_init(&mutex,NULL);
    //创建线程
    pthread_create(&tid,NULL,func,NULL);

    while(n--)
    {
        //pthread_mutex_lock(&mutex); //加锁
        printf("HELLO");
        sleep(rand()%3);
        printf("WORLD\n");
        //pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }

    //销毁锁
    pthread_mutex_destroy(&mutex);
    //关闭子线程
    pthread_cancel(tid);
    //回收子线程,或者设置线程分离
    pthread_join(tid,NULL);
    //pthread_detach(tid);

    return 0;
}

       ミューテックスを追加

  

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex;      //定义为全局变量,不能定义为栈上的临时变量

void *func(void *arg)
{

    srand(time(NULL));
    while(1)
    {
        pthread_mutex_lock(&mutex); //加锁
        printf("hello");
        sleep(rand()%3);
        printf("world\n");
        pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
       
    }

    return NULL;
}

int main(void)
{

    int n = 5;
    pthread_t tid;
    srand(time(NULL));  //设置随机种子

    //初始化互斥量,在创建线程之前
    pthread_mutex_init(&mutex,NULL);
    //创建线程
    pthread_create(&tid,NULL,func,NULL);

    while(n--)
    {
        pthread_mutex_lock(&mutex); //加锁
        printf("HELLO");
        sleep(rand()%3);
        printf("WORLD\n");
        pthread_mutex_unlock(&mutex); //解锁
        sleep(rand()%3);
    }

    //销毁锁
    pthread_mutex_destroy(&mutex);
    //关闭子线程
    pthread_cancel(tid);
    //回收子线程,或者设置线程分离
    pthread_join(tid,NULL);
    //pthread_detach(tid);

    return 0;
}

私たちの議論はここまでですか?

もちろんそうではありません。特別なケースを見てみましょう

 コードをこれに変更すると、どのような結果が得られるでしょうか....

   無限ループに陥り、メイン スレッドが CPU を奪い合うことができなくなります。

スレッドは、共有リソースを操作した直後にロックを解除する必要がありますが、変更後、スレッドはロックを保持したままスリープします。起きて解錠後、すぐに再度施錠されます。これら 2 つのライブラリ関数自体はブロックしません。したがって、これら 2 行のコードの間で CPU が失われる可能性は非常に低くなります。したがって、別のスレッドがロックする機会を得ることは困難です。

コードをもう一度変更しましょう。

 子スレッドが終了していないことが判明し、子スレッドのリサイクルを待機して親スレッドがブロックされている

 理由は明らかで、 pthread_join は子スレッドが終了するのを待ってブロックし、子スレッドは無限ループに入ります..そう...

デッドロック

オペレーティング システムを学習するときにデッドロックが発生するための 4 つの必要条件 (教科書で定義):

1. 相互排除条件 (ミューテックス ロックは相互に排他的な共有リソースであり、特定の時間に 1 つのスレッドしか入ることができません)

2. リクエストとホールド条件 (すべてのプロセスは既存の状態を維持する必要があります)

3. 条件を剥奪しない(外部の影響を受けない)

4. ループ待ち状態(他のプロセスがリソースを解放するのを待っている状態)

これは教科書で与えられた定義であり、理解を容易にするためにいくつかの説明が続きます。

次に、実際のプログラミング プロセスのシーン (主に 3 つの状況があります)。

1.ロック解除忘れ

2. ロックを繰り返す

3. マルチスレッドとマルチロック、ロック リソースの先取り

(1つ目のケースは分かりやすいので、ここではあまり説明しません。2つ目と3つ目のケースの分析に焦点を当てましょう)

        

        場合によっては、スレッドが 2 つ以上の異なる共有リソースに同時にアクセスする必要があり、各共有リソースが異なるミューテックスによって管理されます。複数のスレッドが同じ一連のミューテックスをロックすると、デッドロックが発生する可能性があります。(同じミューテックスを 2 回ロックする)

解決策: 共有リソースにアクセスした直後にロックを解除し、手順が完了するのを待ってから再度ロックします。

        実行過程では、リソースの競合により2つ以上のプロセスが待ち合い、外力がなければ先に進むことができません。この時点で、システムはデッドロック状態にある、またはシステムでデッドロックが発生したと言われます。(スレッド 1 はロック A を所有し、ロック B を要求し、スレッド 2 はロック B を所有し、ロック A を要求します)

解決策: trylock はロック機能を置き換えてロックを解除します (すべてのロックが取得されていない場合、すべてのロックを積極的に放棄します)。        

上記のケース:
 

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex1;


void* deadlock1(void *arg)
{

    pthread_mutex_lock(&mutex1);
    printf("hello");
    pthread_mutex_lock(&mutex1);
    printf("world1\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex1);

    return NULL;
}


int main(void)
{

    pthread_t tid1;
    //初始化
    pthread_mutex_init(&mutex1,NULL);
    
    //创建线程
    pthread_create(&tid1,NULL,deadlock1,NULL);
   

    //设置线程分离
    pthread_detach(tid1);
    
    //退出主线程
    pthread_exit(0);

    return 0;
}

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex1;
pthread_mutex_t mutex2;

void* deadlock1(void *arg)
{

    pthread_mutex_lock(&mutex1);
    printf("hello");
    sleep(4);
    pthread_mutex_lock(&mutex2);
    printf("world1\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);

    return NULL;
}

void* deadlock2(void *arg)
{
    // sleep(1);
    pthread_mutex_lock(&mutex2);
    printf("HELLOE");
    sleep(3);
    pthread_mutex_lock(&mutex1);
    printf("WORLD\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);


    return NULL;
}

int main(void)
{

    pthread_t tid1,tid2;
    //初始化
    pthread_mutex_init(&mutex1,NULL);
    pthread_mutex_init(&mutex2,NULL);
    //创建线程
    pthread_create(&tid1,NULL,deadlock1,NULL);
    pthread_create(&tid2,NULL,deadlock2,NULL);

    //设置线程分离
    pthread_detach(tid1);
    pthread_detach(tid2);
    //退出主线程
    pthread_exit(0);

    return 0;
}

読み書きロック

ミューテックスに似ていますが、読み取り/書き込みロックにより、より高い並列処理が可能になります。その特徴は次のとおりです。書き込み専用、読み取り共有

        スレッドが既にミューテックスを保持している場合、ミューテックスは、クリティカル セクションに入ろうとするすべてのスレッドをブロックします。ただし、現在ミューテックスを保持しているスレッドが共有リソースの読み取りとアクセスのみを希望し、同時に他のいくつかのスレッドも共有リソースの読み取りを希望している状況を考えてみてください。共有リソースへのアクセス. ロックを取得できず、共有リソースへのアクセスを取得することはできませんが、実際には複数のスレッドが同時に共有リソースを読み込んでアクセスしても問題はありません.

        データの読み取り操作と書き込み操作では、読み取り操作が多く、書き込み操作が比較的少ない。例: データベース データのアプリケーションの読み取りと書き込み。複数の読み取りを許可し、書き込みは 1 回だけ許可するという現在の要件を満たすために、スレッドは読み取り/書き込みロックを提供します。

読み書きロック機能:

        1. 他のスレッドがデータを読み取る場合、他のスレッドは読み取り操作を実行できますが、書き込み操作は許可されません

        2. 他のスレッドがデータを書き込む場合、他のスレッドは操作の読み取りおよび書き込みを許可されません

        3.書き込みは排他的であり、書き込みは優先度が高い(書き込みの枯渇を防ぐため)

そういえば、OSの教科書の読み書き問題であることが分かった……笑

関連機能

読み取り/書き込みロック タイプ: pthread_rwlock_t

pthread_rwlock_init 関数

        int pthread_rwlock_init(pthread_rwlock_t *rwlock を制限し、

                                             const pthread_rwlockattr_t *restrict attr);

        機能: 読み書きロックを初期化する

        パラメータ:

                   attr は読み取り/書き込みロックの属性を示します。通常はデフォルトの属性を使用し、NULL を渡すだけです

pthread_rwlock_destroy 関数

        int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

        機能: 読み書きロックを破棄する

pthread_rwlock_rdlock 関数

        int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

        機能: 読み取りモードでの読み取り/書き込みロックの要求 (読み取りロックの要求)

pthread_rwlock_wrlock 関数

        int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

       役割: 書き込みモードで読み取り/書き込みロックを要求する (書き込みロックを要求する)

pthread_rwlock_unlock 関数

        int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

        機能: ロックを解除

pthread_rwlock_tryrdlock 関数

        int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

        役割: ノンブロッキング リクエスト読み取りロック

pthread_rwlock_trywrlock 関数

        int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

        役割: ノンブロッキング リクエスト書き込みロック

前のケース (複数のスレッドが同じ共有データを同時に読み書きする)

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_rwlock_t rwlock;

int counter = 0;

//写线程
void *th_write(void *arg)
{
    int t;
    int i = (int)arg;
    while(1)
    {
        pthread_rwlock_wrlock(&rwlock);    //上写锁
        sleep(2);
        printf("writer:%d %lu counter:%d\n",i,pthread_self(),++counter);
        pthread_rwlock_unlock(&rwlock);    //解锁
        usleep(10000);
    }
}

//读线程
void *th_read(void *arg)
{
    int i = (int)arg;
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);    //上读锁
        printf("read:%d %lu counster:%d\n",i,pthread_self(),counter);
        pthread_rwlock_unlock(&rwlock);
        sleep(3);
    }

}


int main(void)
{

    int i;
    pthread_t tid[8];
    pthread_rwlock_init(&rwlock,NULL);

    for(i=0;i<3;++i)
    {
        pthread_create(&tid[i],NULL,th_write,(void *)i);
    }
    for(i=0;i<5;++i)
    {   
        pthread_create(&tid[i],NULL,th_read,(void *)i);
    }

    //回收子线程
    for(i=0;i<8;++i)
    {
        pthread_join(tid[i],NULL);
    }

    //销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

条件変数

条件変数自体はロックではありませんが、スレッドがブロックされる可能性があります通常、ミューテックスと組み合わせて使用​​されます。マルチスレッドに適した場所を提供します。(スレッドの同期問題、スレッドの相互排除を解決するためのミューテックスと読み書きロック)

関連機能

条件変数の型: pthread_cond_t

pthread_cond_init 関数

       int pthread_cond_int(pthread_cond_t *restrict cond,

                                             const pthread_condattr_t *restrict attr);

        パラメータ: attr は条件変数の属性を示し、通常は NULL を渡します

        静的初期化メソッドを使用して条件変数を初期化できます

        pthread_cond_t cond = PTHREAD_COND_INITIALIZER

pthread_cond_destroy 関数

        int pthread_cond_destroy(pthread_cond_t *cond);

        役割: 条件変数を破棄する

pthread_cond_wait 関数

        int pthread_cond_wait(pthread_cond_t *条件の制限,pthread_mutex_t *ミューテックスの制限);

        機能: 1. ブロックして条件変数 cond が満たされるのを待つ [満たされない: ブロックされる 満たされるのを待つ 満足される: ブロックされない]

                   2. マスターしたミューテックスを解放する (ミューテックスのロックを解除する) は、pthread_mutex_unlock(&mutex); と同等です。

                   [1,2 はアトミック操作であり、分割できません]

                   3. 目覚めたら、pthread_cond_wait 関数が戻ったら、ブロックを解除してミューテックスを再適用します。

                        pthread_mutex_lock(&mutex);

pthread_cond_timedwait 関数

        int pthread_cond_timewat(pthread_cond _t *制限条件,

                                pthread_mutex_t *mutex を制限します。

                                const struct timespec *restrict abstime);

        機能: 条件変数を一定時間待機する

        パラメータ:

        abstime は絶対時間、time(NULL) は絶対時間を返します。

        alarm(1) は相対時間で、現在の時間に対して 1 秒です。      

 手順:

        間違った使い方:

        struct timespec t = {1,0};

        pthread_cond_timedwait(&cond,&mutex,&t); //1970.1.1 00:00:01 までの時間のみ

        正しい使い方:

        time_t cur = time(NULL); //現在時刻を取得

        struct timespec t;

        t.tv_sec = cur+1; // 1 秒を定義

         pthread_cond_timedwait(&cond,&mutex,&t);

pthread_cond_signal 関数

        int pthread_cond_signal(pthread_cond_t *cond);

        役割: 1 つ以上の待機中のスレッドをウェイクアップする

pthread_cond_broadcast 関数

        int pthread_cond_broadcast(pthread_cond_t *cond);

        機能: ブロードキャストによるすべてのスレッドの起床

    

たとえば、消費者-生産者モデルを参照してください。   

生産者消費者モデル

プロデューサー モデルのオブジェクト: プロデューサー、コンシューマー、コンテナー

一方の端は商品の生産に専念し、もう一方の端は商品の消費に専念します。コンテナがいっぱいになると、生産者はブロックして消費者が商品を消費するのを待って、コンテナがアイテムを保管できるようにします。コンテナーが空の場合、プロデューサーがアイテムを生成してコンテナーに格納するまでブロックして待機する必要があります。 

一度にコンテナーに入るスレッドは 1 つだけです (相互排他関係が形成され、プロデューサーとコンシューマーが同期関係を維持します)。

実際のプロジェクト開発では、このモデルが多くの場所で使用されるため、生産者モデルと消費者モデルについて話す理由

Producer Consumer モデルの実装 (条件変数版)

スレッド同期の典型的なケースはプロデューサー/コンシューマー モデルであり、条件変数を使用してこのモデルを実装する方が便利です。2 つのスレッドがあり、1 つはプロデューサーの動作をシミュレートし、もう 1 つはコンシューマーの動作をシミュレートするとします。2 つのスレッドが共有リソース (一般にプールと呼ばれる) で同時に動作し、そこから生産者が項目を追加し、消費者が製品を消費します。

プログラムでの挿入と削除の操作がより頻繁に行われるため (連結リストを使用してコンテナーを実装します)

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

struct msg
{
    struct msg *next;
    int num;
};

struct msg *head;

//静态方法初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//消费者
void* consumer(void *p)
{
    struct msg *mp;
    while(1)
    {
        //取产品
        pthread_mutex_lock(&mutex);
        while(head==NULL)   //不能用 if  防止虚假唤醒
        {
            pthread_cond_wait(&cond,&mutex);
        }
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&mutex);

        //消费产品
        printf("消费:%d\n",mp->num);
        free(mp);
        sleep(rand()%5);
    }
    return NULL;
}
//生产者
void *product(void *arg)
{
    
    struct msg *mp;
    while(1)
    {
        //生产产品
        mp = malloc(sizeof(struct msg));
        mp->num = rand()%200;
        printf("生产:%d\n",mp->num);

        //防止产品
        pthread_mutex_lock(&mutex);
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&mutex);

        pthread_cond_signal(&cond);
        sleep(rand()%5); 
    }

    return NULL;
}

int main(void)
{

    pthread_t pid,cid;
    srand(time(NULL));

    pthread_create(&pid,NULL,product,NULL);
    pthread_create(&cid,NULL,consumer,NULL);

    pthread_join(pid,NULL);
    pthread_join(cid,NULL);

    return 0;
}

 条件変数の利点:

        条件変数は、ミューテックスと比較して競合を減らすことができます。

        ミューテックスをそのまま使うと、プロデューサーとコンシューマーの競合に加えて、コンシューマーもミューテックスを競う必要がありますが、アグリゲーション(連結リスト)にデータがなければ意味のあるコンシューマー間の競合はありません。条件変数の仕組みにより、生産者が生産を完了した場合にのみ、消費者間の競争が発生し、プログラムの効率が向上します。

信号量

ミューテックスの粒度が比較的大きいため、オブジェクトのデータの一部を        複数のスレッド間で共有したい場合、ミューテックスを使用してそれを実現する方法はなく、データ オブジェクト全体をロックすることしかできません。これにより、マルチスレッド操作でデータを共有するときにデータの正確性を確保するという目的は達成されますが、事実上、スレッドの同時実行性が低下します。スレッドは、同時実行から逐次実行に変わります。単一のプロセスを直接使用するのと同じです。

        セマフォは、同期を確実にするだけでなく、データが混乱しないだけでなく、スレッドの同時実行性を向上させる、比較的妥協した処理方法です。

        (工程間でも使用可能)

セマフォは PV 操作のようなものです

関連機能

        セマフォのタイプ: sem_t

        セマフォの初期値は、セマフォを占有するスレッドの数を決定します

        

        sem_init 関数

                int sem_init(sem_t *sem,int pshared,unsigned int value);

                パラメータ:

                        sem_t セマフォ

                        pshared 0 はスレッド、非ゼロ (通常は 1) はプロセスに使用されます

                        value は、セマフォの初期値を指定します

        sem_destroy 関数

               int sem_destroy(sem_t *sem);

                機能: セマフォを破棄する

        sem_wait 関数

                int sem_wait(sem_t *sem);

                関数: 最初に sem==0 のブロックを判断し、値 -1

                           セマフォの値から1を引き、0ならブロックする

       sem_trywait 関数

                int sem_trywait(sem_t *sem);

                効果: セマフォから 1 を減算しようとします (ノンブロッキング)

        sem_timedwait 関数

                sem_timedwait(sem_t *sem,const struct timespec *abs_timeout);

                機能: 制限時間内にセマフォのロックを試みます--

                パラメータ: abs_timeout は絶対時間です

                1 秒間のタイミング:

                        time_t cur = time(NULL); //現在時刻を取得

                        struct timespec t; //timespec 構造体変数 t を定義

                        t.tv_sec = オン + 1;

                        //t.tv_nsec = t.tv_sec + 100;

                        sem_timedwait(&sem,&t);

        sem_post 関数

                int sem_post(sem_t *sem);

                機能: セマフォに 1 を加える

        sem_getvalue 関数

                sem_getvalue(sem_t *sem,int *sval);

                機能: 現在のセマフォの値を取得する

たとえば、生産者消費者モデルを参照してください

Producer Consumer モデルの実装 (セマフォ版)

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#define NUM 5

int queue[NUM];     //缓冲区
sem_t produc_number,blank_number;

void *product(void *arg)
{
    int i = 0;
    while(1)
    {
        sem_wait(&blank_number);    //缓冲区是否已满
        queue[i] = rand()%100 + 1;   //生产产品
        printf("放缓冲区:%d\n",queue[i]);
        sem_post(&produc_number);   //产品数 ++

        i = (i+1)%NUM;
        sleep(rand()%3);
    }

    return NULL;
}
void *consumer(void *arg)
{
    int i = 0;

    while(1)
    {
        sem_wait(&produc_number); //产品数量--
        printf("取走缓冲区:%d\n",queue[i]);
        queue[i] = 0;
        sem_post(&blank_number);    //格子数目++

        i = (i+1)%NUM;  //循环队列,下一个位置
        sleep(rand()%3);
    }

    return NULL;
}

int main(void)
{

    pthread_t pid,cid;
    
    sem_init(&blank_number,0,NUM);      //初始时缓冲区空格子为5(或者说初始时生产者为5)
    sem_init(&produc_number,0,0);       //初始时产品数为0(或者说消费者为0)        

    pthread_create(&pid,NULL,product,NULL);
    pthread_create(&cid,NULL,consumer,NULL);

    pthread_join(pid,NULL);
    pthread_join(cid,NULL);

    //线程销毁
    sem_destroy(&blank_number);
    sem_destroy(&produc_number);

    return 0;
}

ファイルロック

fcntl 関数を使用してロック メカニズムを実装します. ファイルを操作するプロセスがロックを取得しない場合, ファイルを開くことはできますが, 読み取りおよび書き込み操作を実行することはできません. fcntl 関数: ファイルのアクセス制御パーミッションを取得および設定します.

int fcntl(int fd,int cmd,.../*arg*/);

参考2:

      F_SETLK(struct flock*) ファイルロックの設定 (trylock)

      F_SETLKW(struct flock*) ファイルロックを設定 (lock) W-->wait

      F_GETLK(struct flock*); ファイルロックを取得

参考3:

       構造体の群れ{

                ...

                short l_type; ロックタイプ: F_RDLCK F_WRLCK F_UNLCK (ロック解除)

                short l_whence; オフセット位置: SEEK_SET SEEK_CUR SEEK_END

                short l_start; 開始位置: 1000 

                short l_len; length: 0 は、提示価格全体がロックされていることを意味します

                short l_pid; ロックを保持しているプロセス ID: F_GETLK

                ...

        }

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

void sys_err(char *str)
{
    perror(str);
    exit(-1);
}

int main(int arg,char *argv[])
{

    int fd;
    struct flock f_lock;

    if(arg < 2)
    {
        printf("./a.txt filename\n");
        exit(1);
    }

    if(fd == open(argv[1],O_RDWR) < 0)
    {
        sys_err("open");
    }

    // f_lock.l_type = F_WRLCK;    //设置写锁
    f_lock.l_type = F_RDLCK;    //设置读锁

    f_lock.l_whence = SEEK_SET; //文件头部
    f_lock.l_start = 0;
    f_lock.l_len = 0;

    fcntl(fd,F_SETLKW,&f_lock);     //上锁
    printf("get flock\n");
    sleep(10);

    f_lock.l_type = F_UNLCK;
    fcntl(fd,F_SETLK,&f_lock);      //解锁
    printf("un flock\n");

    close(fd);


    return 0;
}

「読み取り共有、書き込み排他」に従ってください。ただし、プロセスはファイルをロックせずにファイルを直接操作でき、ファイルに正常にアクセスできますが、データが混乱することは避けられません。

ファイルディスクリプタは複数のスレッドで共有されており、ファイルロックはファイルディスクリプタが指すファイル構造体のメンバー変数を変更することで実現されるため、複数のスレッドでファイルロックを使用することはできません。

おすすめ

転載: blog.csdn.net/weixin_46120107/article/details/126376674