技術共有

Linux マルチプロセスとマルチスレッド (8) マルチスレッド

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

マルチスレッド化

スレッドの定義

スレッドはプロセス内の実行単位です。

現在のプロセス内のプログラムの実行を担当します。

プロセス内に少なくとも 1 つのスレッドが存在する

プロセス内に複数のスレッドが存在する可能性があります

複数のスレッドが同じプロセスのすべてのリソースを共有し、各スレッドがオペレーティング システムの統合スケジューリングに参加します。

単純にプロセス = メモリ リソース + メイン スレッド + 子スレッド +… として理解できます。

スレッドとプロセス

密接に関連するタスクの場合は、同時実行中にマルチスレッドが優先されます。密接に関連せず、比較的独立したタスクの場合は、マルチプロセスを選択することをお勧めします。

  • プロセス: オペレーティング システムによるリソース割り当ての基本単位。リソース割り当ての最小単位、プログラムの実行およびスケジュール単位、およびプログラムの実行インスタンスです。
  • スレッド: CPU のスケジューリングとディスパッチの基本単位、CPU 実行の最小単位、プログラム実行フローの最小単位、およびプログラム実行の最小単位です。

スレッドとプロセスの違いは次のとおりです。

  • メモリ空間
    • プロセス内の複数のスレッドが同じメモリ空間を共有する
    • 複数のプロセスが独立したメモリ空間を持つ
  • プロセス/スレッド間の通信
    • スレッド間の単純な通信
    • プロセス間通信は複雑です

スレッドリソース

  • 共有プロセスリソース
    • 同じアドレス空間
    • ファイル記述子テーブル
    • 各信号がどのように処理されるか
    • 現在の作業ディレクトリ
    • ユーザーIDとグループID
  • ユニークなリソース
    • スレッドスタック
    • 各スレッドにはプライベートなコンテキスト情報があります
    • スレッドID
    • レジスタ値
    • エラー番号値
    • シグナルマスクワードとスケジュールの優先順位

スレッド関連のコマンド

Linux システムには、pidstat、top、ps など、プロセスを表示するためのコマンドが多数あり、プロセスを表示したり、
処理中のスレッド

pidstat コマンド

sysstat ツールを ubuntu にインストールする必要があると、pidstat をサポートできるようになります。

sudo apt インストール sysstat

オプション

-t: 指定したプロセスに関連付けられたスレッドを表示します。

-p: プロセス PID を指定します

プロセス 12345 に関連付けられたスレッドを表示する

sudo pidstat -t -p 12345

すべてのプロセスに関連付けられたスレッドを表示する

sudo pidstat -t

プロセス 12345 に関連付けられたスレッドを表示し、1 秒ごとに出力します

sudo pidstat -t -p 12345 1

すべてのプロセスに関連付けられたスレッドを表示し、1 秒ごとに出力します

sudo pidstat -t 1

トップコマンド

特定のプロセスのスレッドを表示するには、top コマンドを使用します。pid を指定するには、-H オプションを -p と組み合わせて使用​​する必要があります。

オプション

-H: スレッド情報を表示します。

-p: プロセス PID を指定します

プロセス 12345 に関連付けられたスレッドを表示する

sudoトップ-H -p 12345

すべてのプロセスに関連付けられたスレッドを表示する

sudoトップ-H

psコマンド

ps コマンドと -T オプションを組み合わせて使用​​し、プロセス内のすべてのスレッドを表示します

オプション

-T: スレッド情報を表示します

-p: プロセス PID を指定します

プロセス 12345 に関連付けられたスレッドを表示する

sudo ps -T -p 12345

すべてのプロセスに関連付けられたスレッドを表示する

sudo ps -T

一般的な同時実行シナリオ

1. マルチプロセスモード

マルチプロセス モードでは、各プロセスは互いに干渉することなく、異なるタスクを担当します。各プロセスは、互いに影響を与えることなく、異なるメモリ空間で実行されます。

  • アドバンテージ:
    • プロセスのアドレス空間は独立しています。プロセス内で例外が発生すると、他のプロセスには影響しません。
  • 欠点:
    • 各プロセスは独立したメモリ領域を割り当てる必要があります。プロセスの作成にはコストがかかり、より多くのメモリを消費します。
    • プロセス間コラボレーションとプロセス間通信はより複雑です
  • 該当するシーン:
    • 複数のタスクはあまり密接に関連していないため、マルチプロセス モードを使用できます。
    • タスク間に依存関係がなく、マルチプロセスモードが使用可能

2. マルチスレッドモード

マルチスレッド モードでは、プロセス内に複数のスレッドが存在し、同じメモリ空間を共有することができ、スレッドは直接通信できます。

  • アドバンテージ:
    • スレッド間の通信は簡単です
    • 同じプロセスの複数のスレッドがリソースを共有し、リソースの使用率を向上させることができます。
  • 欠点:
    • スレッドには独立したプロセス アドレス空間がありません。メイン スレッドが終了すると、他のスレッドも終了します。
    • スレッドの切り替えとスケジュールにはリソースが必要です。スレッドが多すぎるとシステム リソースが消費されます。
    • スレッド間の同期は複雑であり、スレッドの安全性の問題を考慮する必要がある
  • 該当するシーン:
    • タスク間に依存関係があり、マルチスレッド モードが使用可能
    • タスク間の通信は比較的頻繁に行われ、マルチスレッドモードが使用可能です。

スレッドを作成する

1. pthread_create()

pthread_create() はスレッドの作成に使用され、作成が成功すると、スレッドの実行が開始されます。
pthread_create() が正常に呼び出されると、0 が返され、それ以外の場合はエラー コードが返されます。

関数ヘッダー ファイル:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • 1
  • 2
  • 3
  • 4

パラメータの説明:

  • thread: pthread_t 型へのポインタ。スレッド ID を格納するために使用されます。
  • attr: スレッド属性。NULL にすることもでき、デフォルトの属性が使用されることを示します。
  • start_routine: スレッドのエントリ関数。
  • arg: スレッドエントリ関数に渡されるパラメータ。

戻り値:

  • 0: 正常に作成されました。
  • EAGAIN: リソースが不足しているため、スレッドの作成に失敗しました。
  • EINVAL: パラメータが無効です。
  • ENOMEM: メモリが不足しているため、スレッドの作成に失敗しました。

知らせ:

  • 子スレッドが正常に作成されると、そのスレッドの実行が個別にスケジュールされ、他のスレッドと同時に実行されます。
  • コンパイル時に -lpthread ライブラリをリンクする必要があります。

例: スレッドを作成する

// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    printf("%sn",(char *)arg);
}

int main() {
    pthread_t tid; //? typedef unsigned long int pthread_t;
    // 创建线程
    //@param tid 线程ID
    //@param attr 线程属性
    //@param start_routine 线程函数
    //@param arg 线程函数参数
    int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
    if (ret!= 0){
        printf("pthread_create error!n");
        return 1;
    }
    sleep(1); // 等待线程执行完毕
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

2. pthread_exit() はスレッドを終了します

pthread_exit() はスレッドを終了するために使用されます。スレッドの実行が完了すると、pthread_exit() が自動的に呼び出されて終了します。

関数ヘッダー ファイル:

#include <pthread.h>

void pthread_exit(void *retval);
  • 1
  • 2
  • 3

パラメータの説明:

  • retval: スレッドが終了したときに返される値。
  • スレッド関数が実行されると、 pthread_exit() が自動的に呼び出されて終了します。

3. pthread_join() はスレッドが終了するのを待ちます

pthread_join() はスレッドが終了するのを待つために使用されます。
pthread_join() を呼び出した後、現在のスレッドはスレッドが終了するまでブロックされます。

関数ヘッダー ファイル:

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
  • 1
  • 2
  • 3

パラメータの説明:

  • スレッド: スレッドID。
  • retval: スレッドの戻り値へのポインタ。スレッドの終了時に返される値を格納するために使用されます。 (二次ポインタ)

戻り値:

  • 0: 成功を待ちます。
  • EINVAL: パラメータが無効です。
  • ESRCH: スレッド ID が存在しません。
  • EDEADLK: スレッドはデッドロック状態にあります。

例:

// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    sleep(1); // 休眠1秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}

int main() {
    pthread_t tid; //? typedef unsigned long int pthread_t;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
    if (ret!= 0){
        printf("pthread_create error!n");
        return 1;
    }

    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid, NULL);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
等待线程结束...
Hello, World!
  • 1
  • 2

糸離れ

糸は結合可能と取り外し可能に分かれています

  • 組み合わせ可能
    • 結合可能なスレッドは、そのリソースを再利用して他のスレッドによって強制終了される可能性があり、そのメモリ リソース (スタックなど) は、他のスレッドによって再利用されるまで解放されません。
    • スレッド作成のデフォルト状態は結合可能です。pthread_join 関数は、子スレッドが終了して関連リソースが解放されるまで待機するために、他のスレッドから呼び出すことができます。
  • 分離可能な
    • 他のスレッドによってリサイクルまたは強制終了することはできません。スレッドのリソースは、終了時にシステムによって解放されます。
// todo : 创建一个线程,并在线程中打印出“Hello, World!”
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    sleep(1); // 休眠1秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}

int main() {
    pthread_t tid; //? typedef unsigned long int pthread_t;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int ret = pthread_create(&tid, NULL,print_hello, "Hello, World!");
    if (ret!= 0){
        printf("pthread_create error!n");
        return 1;
    }

    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    //pthread_join(tid, NULL);//! 阻塞等待线程结束,直到线程结束后才继续往下执行

    //线程分离
    pthread_detach(tid); //! 分离线程,不用等待线程结束后才退出程序,该线程的资源在它终止时由系统来释放。

    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

複数のスレッドを作成する

例 1: 複数のスレッドを作成してさまざまなタスクを実行する

// todo : 创建多个线程,执行不同的任务
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello_A(void *arg) {
    sleep(1); // 休眠1秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}
// 线程函数
//@param arg 线程函数参数
void * print_hello_B(void *arg) {
    sleep(2); // 休眠2秒
    printf("%sn",(char *)arg);
    pthread_exit(NULL); // 线程退出
}


int main() {
    pthread_t tidA; //? 存储线程ID  typedef unsigned long int pthread_t;
    pthread_t tidB;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int retA = pthread_create(&tidA, NULL,print_hello_A, "A_ Hello, World!");
    if (retA!= 0){
        printf("pthread_create error!n");
        return 1;
    }

    int retB = pthread_create(&tidB, NULL,print_hello_B, "B_ Hello, World!");
    if (retB!= 0){
        printf("pthread_create error!n");
        return 1;
    }
    
    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tidA, NULL);//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tidB, NULL);
    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

例 2: 同じタスクを実行する複数のスレッドを作成する

// todo : 创建多个线程,执行相同任务
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
//? 两个线程执行相同任务,对函数中的值修改了,会不会影响到其他线程的执行?
//! 在多线程编程中,如果多个线程执行相同的任务并且对共享资源进行修改,可能会影响到其他线程的执行。
//! 这是因为多个线程共享相同的内存空间,对共享资源的修改可能会导致竞态条件(race condition),
//! 从而导致不可预测的行为。
//! print_hello函数中的变量i是局部变量,每个线程都会有自己的i副本,因此对i的修改不会影响到其他线程。
//! 但是,如果涉及到共享资源(例如全局变量或静态变量),就需要考虑线程同步的问题,以避免竞态条件。


//*局部变量:每个线程都有自己的栈空间,因此局部变量是线程私有的,不会影响到其他线程。
//*共享资源:如果多个线程访问和修改同一个全局变量或静态变量,就需要使用同步机制(如互斥锁、信号量等)来确保线程安全。
//Linux:在Linux系统中,默认的线程栈大小通常是8MB。可以使用ulimit -s命令查看和修改当前用户的线程栈大小。例如,ulimit -s 1024将线程栈大小设置为1MB。
//Windows:在Windows系统中,默认的线程栈大小是1MB。可以通过编译器选项或在创建线程时指定栈大小来修改。

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {

    for (char i = 'a'; i < 'z' ; ++i) {
        printf("%cn", i);
        sleep(1); // 休眠1秒
    }
    pthread_exit(NULL); // 线程退出
}

int main() {
    pthread_t tid[2]={0}; //? 存储线程ID的数组  typedef unsigned long int pthread_t;


    for (int i = 0; i < 2; ++i) {
        // 创建线程
        //* @param tid 线程ID
        //* @param attr 线程属性
        //* @param start_routine 线程函数
        //* @param arg 线程函数参数
        int retA = pthread_create(&tid[i], NULL,print_hello, NULL);
        if (retA!= 0){
            printf("pthread_create error!n");
            return 1;
        }
    }

    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid[0], NULL);//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tid[1], NULL);


    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

スレッド間の通信

プロセス間のその他の通信は、スレッド間の通信にも当てはまります。

メインスレッドはパラメータを子スレッドに渡します

pthread_create() 関数を通じて子スレッドを作成する場合、pthread_create() の 4 番目のパラメーターは子スレッドの関数に渡されるパラメーターです。

サブスレッドはメインスレッドにパラメータを渡します

pthread_exit() 関数を通じて子スレッドを終了するときに、メインスレッドにパラメータを渡すことができます。

void pth_exit(void *retval);
  • 1

pthread_join()関数でサブスレッドの終了を待つ場合、サブスレッドの戻りパラメータを取得します。

int pthread_join (pthread_t __th, void **__thread_return);
//二级指针获取到了pthread_exit()函数参数指针的指向地址,通过该地址可以获取到子线程的返回参数。
  • 1
  • 2

例:

// todo : 线程直接通讯,子线程向父线程传参
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    printf("子线程开始,结束之时传递参数100的地址n");

    sleep(1); // 休眠1秒
    //! int num=100;//局部变量,函数结束释放内存
    static int num=100;//* 静态局部变量,函数结束不释放内存,延长生命周期
    pthread_exit(&num); // 线程退出
}



int main() {
    pthread_t tid; //? 存储线程ID  typedef unsigned long int pthread_t;
    // 创建线程
    //* @param tid 线程ID
    //* @param attr 线程属性
    //* @param start_routine 线程函数
    //* @param arg 线程函数参数
    int retA = pthread_create(&tid, NULL,print_hello, NULL);
    if (retA!= 0){
        printf("pthread_create error!n");
        return 1;
    }



    printf("等待线程结束...n");
    void* num;//获取子进程传递的参数,num指向了子进程传递的参数
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid, (void **)&num);//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    printf("子线程结束,传递的参数为%dn",*(int*)num);
    printf("主线程结束n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

スレッドのミューテックス

スレッドのミューテックス

Mutex は、共有リソースへのアクセスを制御するために使用される同期メカニズムです。

スレッドの主な利点は、グローバル変数を通じて情報を共有できることですが、この便利な共有には代償が伴います。

複数のスレッドが同じ変数を同時に変更しないようにする必要があります。

実際、2 つのスレッドがクリティカル セクションに同時にアクセスすることはできません。

ミューテックスロックの原理

ミューテックス ロックの原理は、スレッドがミューテックス領域に入ろうとしたときに、そのミューテックス領域がすでに他のスレッドによって占有されている場合、そのスレッドはミューテックス領域が解放されるまでブロックされるというものです。

これは本質的に pthread_mutex_t 型の変数であり、ミューテックス領域のステータスを表す整数値が含まれます。
値が 1 の場合、現在のクリティカル リソースがアクセスを競合する可能性があり、ミューテックス ロックを取得したスレッドがクリティカル セクションに入ることができることを意味します。現時点では、値は 0 であり、他のスレッドは待機することしかできません。
値が 0 の場合、現在のクリティカル リソースが他のスレッドによって占有されており、クリティカル セクションに入ることができず、待機することしかできないことを意味します。

ミューテックスロックの特徴

typedef union
{
  struct __pthread_mutex_s __data; // 互斥锁的结构体
  char __size[__SIZEOF_PTHREAD_MUTEX_T];// 互斥锁的大小
  long int __align;// 互斥锁的对齐
} pthread_mutex_t;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • ミューテックス ロックは、ミューテックス ロックを表す pthread_mutex_t 型の変数です。
  • 2 つのスレッドが同じ pthread_mutex_t 変数にアクセスする場合、それらは同じミューテックス ロックにアクセスします。
  • 対応する変数は、構造体を含む共通の本体である pthreadtypes.h ヘッダー ファイルで定義されます。

ミューテックスロックの使用

スレッドのミューテックス ロックを初期化するには、主に 2 つの方法があります。

静的初期化

  • pthread_mutex_t 型の変数を定義し、それを PTHREAD_MUTEX_INITIALIZER に初期化します。
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
  • 1

動的初期化

動的初期化 動的初期化には、主に pthread_mutex_init 関数と pthread_mutex_destroy 関数の 2 つの関数が含まれます。

pthread_mutex_init() 関数

ミューテックス ロックを初期化するために使用され、ミューテックス ロックのアドレスとミューテックス ロックの属性という 2 つのパラメーターを受け入れます。

関数ヘッダー ファイル:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • 1
  • 2
  • 3

パラメータの説明:

  • mutex: pthread_mutex_t タイプへのポインタ。ミューテックス ロックのアドレスを格納するために使用されます。
  • attr: ミューテックス ロックの属性。NULL にすることができ、デフォルトの属性が使用されることを示します。

戻り値:

  • 0: 初期化が成功しました。
  • 失敗するとエラー コードが返されます。
pthread_mutex_destroy() 関数

ミューテックス ロックを破棄するために使用され、1 つのパラメータ (ミューテックス ロックのアドレス) を受け入れます。

関数ヘッダー ファイル:

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 1
  • 2
  • 3

パラメータの説明:

  • mutex: pthread_mutex_t タイプへのポインタ。ミューテックス ロックのアドレスを格納するために使用されます。

戻り値:

  • 0: 破壊成功。
  • 失敗するとエラー コードが返されます。

例:

// todo :  互斥锁;创建两个线程,分别对全局变量进⾏ +1 操作
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>


static int global = 0;// 全局变量

//静态初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 互斥锁
//动态初始化互斥锁
pthread_mutex_t mut;// 互斥锁

// 线程函数
//@param arg 线程函数参数
void * print_hello(void *arg) {
    printf("子线程开始n");

    int loops = *(int *)arg;
    int i,tmp = 0;
    for (i = 0;i < loops;i++){
        pthread_mutex_lock(&mut);// 加锁
        printf("子线程%d,global=%dn",i,global);
        tmp = global;
        tmp++;
        global = tmp;
        pthread_mutex_unlock(&mut);// 解锁
    }
    printf("子线程结束n");
    pthread_exit(NULL); // 线程退出
}



int main() {

    // 动态初始化互斥锁
    //* @param mutex 互斥锁
    //* @param attr 互斥锁属性 NULL 是默认属性
    int r= pthread_mutex_init(&mut,NULL);
    if (r!= 0){
        printf("pthread_mutex_init error!n");
        return 1;
    }

    pthread_t tid[2]={0}; //? 存储线程ID  typedef unsigned long int pthread_t;
    int arg=20;
    for (int i = 0; i < 2; i++){
        // 创建线程
        //* @param tid 线程ID
        //* @param attr 线程属性
        //* @param start_routine 线程函数
        //* @param arg 线程函数参数
        int retA = pthread_create(&tid[i], NULL,print_hello, &arg);
        if (retA!= 0){
            printf("pthread_create error!n");
            return 1;
        }
    }




    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid[0],NULL );//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tid[1],NULL );

    printf("%dn",global);
    printf("主线程结束n");

    // 销毁动态创建的互斥锁
    //* @param mutex 互斥锁
    pthread_mutex_destroy(&mut);// 销毁互斥锁

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79

スレッドの同期

スレッド同期: (ほとんどの場合) 相互排除に基づいて、訪問者による他のメカニズムを介したリソースへの秩序あるアクセスを指します。

条件変数: スレッド同期のためにスレッド ライブラリによって提供されるメカニズム。

スレッド同期の一般的なアプリケーション シナリオは、プロデューサーとコンシューマーの間で行われます。

生産者と消費者の問題

このモデルでは、プロデューサー スレッドとコンシューマー スレッドに分割され、複数のスレッドの同期プロセスがこのスレッドを通じてシミュレートされます。

このモデルでは、次のコンポーネントが必要です。

  • 倉庫: 通常、共有リソースとして製品を保管するために使用されます。
  • プロデューサー スレッド: 製品の生産に使用されます
  • コンシューマスレッド: 製品の消費に使用されます

原理:

倉庫に製品がない場合、コンシューマ スレッドは製品を消費できるようになるまで待つ必要があります。

倉庫が製品でいっぱいになると、プロデューサ スレッドはコンシューマ スレッドが製品を消費するまで待つ必要があります。

ミューテックス ロックに基づいたプロデューサー モデルとコンシューマー モデルの実装例

メインスレッドは消費者です

プロデューサとしての n 個のサブスレッド

// todo :  基于互斥锁实现⽣产者与消费者模型主线程为消费者,n 个⼦线程作为⽣产者
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static int n = 0; // 产品数量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 互斥锁

//生产者执行函数
void * dofunc(void *arg) {
    int arg1 = *(int*)arg;
    for (int i = 0; i <arg1; i++) {
        //获取互斥锁
        pthread_mutex_lock(&mutex);
        //生产产品
        printf("生产者%ld生产了%d个产品n",pthread_self(),++n);//! pthread_self()返回当前线程ID
        //释放互斥锁
        pthread_mutex_unlock(&mutex);
        //休眠1秒
        sleep(1);
    }
    pthread_exit(NULL);
}


int main() {
    pthread_t tid[4]={0}; //? 存储线程ID  typedef unsigned long int pthread_t;
    int arr[4]={1,2,3,4};
    for (int i = 0; i < 4; i++) {
        // 创建线程
        //* @param tid 线程ID
        //* @param attr 线程属性
        //* @param start_routine 线程函数
        //* @param arg 线程函数参数
        int retA = pthread_create(&tid[i], NULL,dofunc,&arr[i] );
        if (retA!= 0){
            printf("pthread_create error!n");
            return 1;
        }
    }
    //消费者执行

    for (int i = 0;i<10;i++) {
        //获取互斥锁
        pthread_mutex_lock(&mutex);
        while (n > 0){
            //消费产品
            printf("消费者%ld消费了1个产品:%dn",pthread_self(),n--);
        }
        //释放互斥锁
        pthread_mutex_unlock(&mutex);
        //休眠1秒
        sleep(1);
    }


    printf("等待线程结束...n");
    // 等待线程结束
    //* @param thread 线程ID
    //* @param status 线程退出状态
    pthread_join(tid[0],NULL );//! 阻塞等待线程结束,直到线程结束后才继续往下执行
    pthread_join(tid[1],NULL );
    pthread_join(tid[2],NULL );
    pthread_join(tid[3],NULL );

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

条件変数

条件変数は、スレッドが実行を続行する前に特定の条件が満たされるまで待機できるようにする同期メカニズムです。

条件変数の原理は、条件変数にミューテックス ロックと待機キューが含まれていることです。

ミューテックス ロックは、待機キューと条件変数を保護するために使用されます。

ここに画像の説明を挿入します

条件変数の初期化

条件変数の性質は pthread_cond_t 型です

其他线程可以阻塞在这个条件变量上, 或者唤
醒阻塞在这个条件变量上的线程
typedef union
{
  struct __pthread_cond_s __data;
  char __size[__SIZEOF_PTHREAD_COND_T];
  __extension__ long long int __align;
} pthread_cond_t;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

条件変数の初期化は、静的初期化と動的初期化に分けられます。

静的初期化

静的に初期化された条件変数の場合は、まず pthread_cond_t 型の変数を定義し、それから PTHREAD_COND_INITIALIZER に初期化する必要があります。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 1

動的初期化 pthread_cond_init()

条件変数を動的に初期化するには、まず pthread_cond_t 型の変数を定義し、次に pthread_cond_init 関数を呼び出して初期化する必要があります。

関数ヘッダー ファイル:

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • 1
  • 2
  • 3

パラメータの説明:

  • cond: pthread_cond_t 型へのポインタ。条件変数のアドレスを格納するために使用されます。
  • attr: 条件変数の属性。デフォルトの属性を使用するには NULL にすることができます。

戻り値:

  • 0: 初期化が成功しました。
  • 失敗するとエラー コードが返されます。

pthread_cond_destroy()

条件変数を破棄するために使用され、条件変数のアドレスという 1 つのパラメーターを受け取ります。

関数ヘッダー ファイル:

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
  • 1
  • 2
  • 3

パラメータの説明:

  • cond: pthread_cond_t 型へのポインタ。条件変数のアドレスを格納するために使用されます。

戻り値:

  • 0: 破壊成功。
  • 失敗するとエラー コードが返されます。

条件変数の使用

条件変数の使用は待機と通知に分けられます

pthread_cond_wait() を待つ

待機関数 pthread_cond_wait() は、条件変数のアドレス、ミューテックス ロックのアドレス、待機時間の 3 つのパラメータを受け取ります。

関数ヘッダー ファイル:

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
  • 1
  • 2
  • 3

パラメータの説明:

  • cond: pthread_cond_t 型へのポインタ。条件変数のアドレスを格納するために使用されます。
  • mutex: pthread_mutex_t タイプへのポインタ。ミューテックス ロックのアドレスを格納するために使用されます。
  • abstime: timeout。タイムアウトがないことを示す NULL にすることもできます。

戻り値:

  • 0: 成功を待ちます。
  • 失敗するとエラー コードが返されます。

pthread_cond_signal() を通知する

通知機能
pthread_cond_signal() は、条件変数のアドレスという 1 つのパラメーターを受け入れます。

関数ヘッダー ファイル:

#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
  • 1
  • 2
  • 3

パラメータの説明:

  • cond: pthread_cond_t 型へのポインタ。条件変数のアドレスを格納するために使用されます。

戻り値:

  • 0: 通知が成功しました。
  • 失敗するとエラー コードが返されます。

すべてに通知 pthread_cond_broadcast()

すべての機能に通知
pthread_cond_broadcast() は、条件変数のアドレスという 1 つのパラメーターを受け入れます。

関数ヘッダー ファイル:

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
  • 1
  • 2
  • 3

パラメータの説明:

  • cond: pthread_cond_t 型へのポインタ。条件変数のアドレスを格納するために使用されます。

戻り値:

  • 0: 通知が成功しました。
  • 失敗するとエラー コードが返されます。

条件変数に基づいた生産者モデルと消費者モデルの実装例

ここに画像の説明を挿入します

step 1 : 消费者线程判断消费条件是否满足 (仓库是否有产品),如果有产品可以消费,则可以正
常消费产品,然后解锁
step 2 : 当条件不能满足时 (仓库产品数量为 0),则调用 pthread_cond_wait 函数, 这个函数
            具体做的事情如下:
            在线程睡眠之前,对互斥锁解锁
            让线程进⼊到睡眠状态
            等待条件变量收到信号时 唤醒,该函数重新竞争锁,并获取锁后,函数返回 
step 3 :重新判断条件是否满足, 如果不满足,则继续调用 pthread_cond_wait 函数
step 4 : 唤醒后,从 pthread_cond_wait 返回,消费条件满足,则正常消费产品
step 5 : 释放锁,整个过程结束
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

条件変数をミューテックス ロックと組み合わせて使用​​する必要があるのはなぜですか?

共有データを保護します。

ミューテックス ロックは、共有データを保護し、1 つのスレッドだけが同時にデータにアクセスして変更できるようにするために使用されます。

これにより、データの競合や不整合が回避されます。

条件変数は、スレッド間の通信に使用され、特定の条件が満たされたことを他のスレッドに通知します。

ただし、条件変数の操作自体は共有データの保護を提供しないため、ミューテックス ロックと組み合わせて使用​​する必要があります。

誤ったウェイクアップを回避します。

条件変数の特性の 1 つは、偽のウェイクアップが発生する可能性があることです。

つまり、スレッドは明示的な通知なしに起動されます。このような事態による誤操作を避けるため、

スレッドは、起動後に条件が実際に満たされているかどうかを再確認する必要があります。

ミューテックスを使用すると、条件のチェック中に共有データが他のスレッドによって変更されないことが保証されるため、偽のウェイクアップによって引き起こされるエラーが回避されます。

通知が正しいことを確認してください。

スレッドが条件変数を通じて他のスレッドに通知する場合、通知する前に共有データが更新されていることを確認する必要があります。

ミューテックスはこれを保証し、ロックを解放する前にすべてのデータ更新操作が完了していることを保証します。

同様に、通知を受信するスレッドは、条件をチェックするときにデータが安定していることを確認するために、条件をチェックする前にミューテックスを保持する必要があります。

複雑な同期パターンを実装します。
ミューテックス ロックと条件変数を組み合わせると、プロデューサーとコンシューマーの問題、リーダーとライターの問題など、より複雑な同期パターンを実装できます。ミューテックスは共有データを保護し、条件変数はスレッド間の調整と通信に使用されます。

// todo :  条件变量
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdlib.h>


static int number = 0;// 产品数量
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;// 互斥锁
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;// 条件变量

// 线程函数
//@param arg 线程函数参数
void * thread_handler(void *arg) {
    int cnt = atoi((char *)arg);// 获取线程参数
    int i,tmp;// 临时变量
    for(i = 0;i < cnt;i++){// 生产产品
        pthread_mutex_lock(&mtx);// 上锁
        printf("线程 [%ld] ⽣产⼀个产品,产品数量为:%dn",pthread_self(),++number);
        pthread_mutex_unlock(&mtx);// 解锁

        //! 唤醒cond阻塞的线程
        //! @param cond 条件变量
        //pthread_cond_signal(&cond);//! 只能唤醒一个线程,如果有多个线程在等待,则只有一个线程会被唤醒
        //唤醒所有线程
        pthread_cond_broadcast(&cond);
    }
    pthread_exit((void *)0);// 线程退出
}



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

    pthread_t tid[argc-1];// 线程ID
    int i;
    int err;
    int total_of_produce = 0;// 总共生产的产品数量
    int total_of_consume = 0;// 总共消费的产品数量
    bool done = false;// 是否完成生产
    //循环创建线程
    for (i = 1;i < argc;i++){
        total_of_produce += atoi(argv[i]);// 计算总共需要生产的产品数量
        // 创建线程
        err = pthread_create(&tid[i-1],NULL,thread_handler,(void *)argv[i]);
        if (err != 0){
            perror("[ERROR] pthread_create(): ");
            exit(EXIT_FAILURE);
        }
    }
    //消费者
    for (;;){
        //*先获取锁,再进行条件变量的等待
        pthread_mutex_lock(&mtx);// 上锁

        //*while循环来判断条件,避免虚假唤醒
        while(number == 0) {// 等待生产者生产产品
            //! 等待条件变量
            //! @param cond 条件变量
            //! @param mtx 互斥锁
            //! 函数中会释放互斥锁,并阻塞线程,
            //! 直到条件变量被唤醒,再重新竞争互斥锁,获取互斥锁并继续执行
            pthread_cond_wait(&cond, &mtx);
        }
        while(number > 0){
            total_of_consume++;// 总共消费的产品数量
            printf("消费⼀个产品,产品数量为:%dn",--number);// 消费产品
            done = total_of_consume >= total_of_produce;// 是否完成生产
        }
        pthread_mutex_unlock(&mtx);// 解锁

        if (done)// 是否完成生产
            break;
    }

    // 等待线程退出
    for(i = 0;i < argc-1;i++){
        pthread_join(tid[i],NULL);
    }

    return 0;

}
    //循环创建线程
    for (i = 1;i < argc;i++){
        total_of_produce += atoi(argv[i]);// 计算总共需要生产的产品数量
        // 创建线程
        err = pthread_create(&tid[i-1],NULL,thread_handler,(void *)argv[i]);
        if (err != 0){
            perror("[ERROR] pthread_create(): ");
            exit(EXIT_FAILURE);
        }
    }
    //消费者
    for (;;){
        //*先获取锁,再进行条件变量的等待
        pthread_mutex_lock(&mtx);// 上锁

        //*while循环来判断条件,避免虚假唤醒
        while(number == 0) {// 等待生产者生产产品
            //! 等待条件变量
            //! @param cond 条件变量
            //! @param mtx 互斥锁
            //! 函数中会释放互斥锁,并阻塞线程,
            //! 直到条件变量被唤醒,再重新竞争互斥锁,获取互斥锁并继续执行
            pthread_cond_wait(&cond, &mtx);
        }
        while(number > 0){
            total_of_consume++;// 总共消费的产品数量
            printf("消费⼀个产品,产品数量为:%dn",--number);// 消费产品
            done = total_of_consume >= total_of_produce;// 是否完成生产
        }
        pthread_mutex_unlock(&mtx);// 解锁

        if (done)// 是否完成生产
            break;
    }

    // 等待线程退出
    for(i = 0;i < argc-1;i++){
        pthread_join(tid[i],NULL);
    }

    return 0;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127