Linux 上のスレッド (スレッドの同期と相互排他)

目次

Linux でのスレッド作成関数 pthread_create()

スレッド待機関数 pthread_join()

スレッドが終了しました

関数 pthread_exit() 

関数 pthread_cancel()

スレッドを切り離す pthread_detach()

スレッド間の同期

信号量

スレッド間の相互排他

ミューテックス 

読み書きロック

デッドロック


プロセスとスレッド
スレッドとプロセスは意味のある 1 対の概念であり、主な違いと関連性は次のとおりです。

  • プロセスはオペレーティング システムによるリソース割り当ての基本単位であり、プロセスには完全な仮想空間があります。システムリソースを割り当てる場合、CPU リソースに加えて、スレッドには独立したリソースが割り当てられず、スレッドが必要とするリソースを共有する必要があります。
  • スレッドはプロセスの一部です。明示的なスレッド割り当てがない場合、プロセスはシングルスレッドとみなされます。プロセス内にスレッドが確立されている場合、システムはマルチスレッドとみなされます。
  • マルチスレッドとマルチプロセッシングは 2 つの異なる概念ですが、どちらも機能を並行して実行します。ただし、メモリや変数などのリソースは複数のスレッド間で簡単に共有できますが、マルチプロセスとは異なり、プロセス間の共有方法は限られています。
  • プロセスにはプロセス制御テーブル PCB があり、システムは PCB を通じてプロセスをスケジュールし、スレッドにはスレッド制御テーブル TCB がありますが、TCB によって表される状態は PCB の状態よりもはるかに小さいです。

スレッドはプロセス データを共有しますが、独自のデータの一部も所有します。

  • スレッドID
  • レジスタのセット
  • スタック
  • エラーノ
  • シグナルマスクワード
  • スケジュールの優先順位 

プロセスの複数のスレッドは同じアドレス空間を共有するため、テキスト セグメントとデータ セグメントが共有されます。関数を定義すると、各スレッドで呼び出すことができます。グローバル変数を定義すると、各スレッドでアクセスできます。さらに、各スレッドは次のプロセス リソースと環境を共有します。

  • ファイル記述子テーブル
  • 各信号の処理方法(SIG_IGN、SIG_DFLまたはカスタム信号処理関数)
  • 現在の作業ディレクトリ
  • ユーザーIDとグループID

Linux でのスレッド作成関数 pthread_create()

関数 pthread_create() はスレッドの作成に使用されます。

pthread_create() 関数が呼び出されるとき、渡されるパラメータには、スレッド属性、スレッド関数、およびスレッド関数変数が含まれます。これらは、特定の特性を持つスレッドを生成するために使用され、スレッド関数はスレッド内で実行されます。関数 pthread_create() を使用してスレッドを作成します。そのプロトタイプは次のとおりです。

 #include <pthread.h>

       int pthread_create( pthread_t *スレッド,

                                        const pthread_attr_t *attr,
                                         void *(*start_routine) (void *),//関数ポインタ

                                         void *arg );

  • thread: スレッドを識別するために使用されます。これは、ヘッダー ファイルで定義された pthread_ t 型の変数です。 pthreadtypes.h: typedef unsigned long int pthread t;
  • attr: このパラメータは、スレッドの属性を設定し、空に設定し、デフォルトの属性を採用するために使用されます。
  • start_routine: スレッドのリソースが正常に割り当てられると、スレッド内で実行されるユニットは通常、自分で作成した関数 start_routine() に設定されます。
  • arg: スレッド関数の実行時に渡されるパラメータ。実行パラメータはスレッドの終了を制御するために渡されます。

スレッドが正常に作成された場合、関数は 0 を返します。0 でない場合は、スレッドの作成が失敗したことを意味し、一般的なエラー戻りコードは EAGAIN および EINVAL です。エラー コード EAGAIN はシステム内のスレッド数が上限に達したことを示し、エラー コード EINVAL はスレッドの属性が不正であることを示します。
スレッドが正常に作成されると、新しく作成されたスレッドはパラメータ 3 および 4 に従って実行関数を決定し、スレッド作成関数が戻った後、元のスレッドはコードの次の行の実行を続けます。 

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

using namespace std;

void *threadRun(void *args)
{
    while (true)
    {
        sleep(1);
        cout << "我是子线程..." << endl;
    }
}

int main()
{
    pthread_t t1;
    pthread_create(&t1, nullptr, threadRun, nullptr);

    while (true)
    {
        sleep(1);
        cout << "我是主线程..." << endl;
    }
}

コンパイル時に、スレッド ライブラリ libpthread をリンクする必要があります。

g++ -o myphread myphread .cc -lpthread 

 実行結果から、結果が不規則であることがわかります。これは主に 2 つのスレッドが CPU リソースを獲得するために競合していることが原因です。

スレッド待機関数 pthread_join()

なぜスレッドは待機する必要があるのでしょうか?

  • 終了したスレッド。その領域は解放されておらず、まだプロセスのアドレス空間内にあります。
  • 新しいスレッドを作成しても、終了したばかりのスレッドのアドレス空間は再利用されません。

関数 pthread_join() は、スレッドの実行が終了するのを待つために使用されます。この関数はブロッキング関数であり、待機スレッドが終了するまで待機してから関数が戻り、待機スレッドのリソースを再利用します。関数のプロトタイプは次のとおりです。

 #include <pthread.h>

       int pthread_join(pthread_t thread, void **retval);

  •  thread: スレッドの識別子、つまり、pthread_create() 関数によって正常に作成された値。
  • retval: スレッドの戻り値。待機中のスレッドの戻り値を格納するために使用できるポインターです。

この関数を呼び出したスレッドはハングし、ID が thread であるスレッドが終了するまで待機します。スレッド thread はさまざまな方法で終了され、pthread_join によって取得される終了ステータスも異なります。

1. スレッド thread が return で復帰した場合、retval が指すユニットにはスレッド thread 関数の戻り値が格納されます。

2. スレッド thread が pthread_cancel を呼び出す別のスレッドによって異常終了された場合、定数 PTHREAD_CANCELED は retval が指すユニットに格納されます。

3. pthread_exit を単独で呼び出してスレッド thread を終了した場合、retval が指すユニットには pthread_exit に渡されたパラメータが格納されます。

4. スレッド thread の終了ステータスに興味がない場合は、retval パラメータに NULL を渡すことができます。 

スレッドが終了しました

プロセス全体を終了せずに、特定のスレッドのみを終了する必要がある場合は、次の 3 つの方法があります。

1. スレッド関数から戻ります。このメソッドはメイン スレッドには適用できません。メイン関数からの戻りは exit の呼び出しと同じです。

2. スレッドは、pthread_exit を呼び出すことでそれ自体を終了できます。

3. スレッドは pthread_cancel を呼び出して、同じプロセス内の別のスレッドを終了できます。

関数 pthread_exit() 

void pthread_exit(void *retval);

 スレッド終了関数には、プロセスと同様に戻り値がなく、スレッドが終了すると呼び出し元 (自分自身) に戻ることができません。

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

関数 pthread_cancel()

実行中のスレッドをキャンセルする

 int pthread_cancel(pthread_t スレッド);

戻り値: 成功した場合は 0 を返し、失敗した場合はエラー コードを返します。

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

using namespace std;
static int retvalue;//线程返回值

void *threadRun1(void *args)
{
    cout << "我是子线程1..." << endl;
    retvalue = 1;
    return (void *)&retvalue;
}
void *threadRun2(void *args)
{
    cout << "我是子线程2..." << endl;
    retvalue = 2;
    pthread_exit((void *)&retvalue);
}
void *threadRun3(void *arg)
{
    while (1)
    { 
        cout << "我是子线程3..." << endl;
        sleep(1);
    }
    return NULL;
}
int main()
{
    pthread_t tid;
    void *ret;

    // threadRun1 return
    pthread_create(&tid, NULL, threadRun1, NULL);
    pthread_join(tid, &ret);
    cout << "子线程1返回... 返回值:" << *(int *)ret << endl;

    // threadRun2 exit
    pthread_create(&tid, NULL, threadRun2, NULL);
    pthread_join(tid, &ret);
    cout << "子线程2返回... 返回值:" << *(int *)ret << endl;

    // threadRun3 cancel by other
    pthread_create(&tid, NULL, threadRun3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if (ret == PTHREAD_CANCELED)
        cout << "子线程3返回... 返回值:PTHREAD_CANCELED" << endl;
    else
        cout << "子线程3返回... 返回值:NULL" << endl;

}

スレッドを切り離す pthread_detach()

int pthread_detach(pthread_t スレッド);

スレッドの切り離された状態によって、スレッドがどのように終了するかが決まります。切り離されたスレッドには、切り離されたスレッドと切り離されていないスレッドの 2 種類があります。
上記の例では、スレッド作成時に属性が設定されておらず、デフォルトの終了方法は非デタッチ状態です。この場合、作成スレッドが終了するまで待つ必要があります。pthread_join() 関数が戻った場合にのみ、スレッドは終了したとみなされ、スレッドの作成時にシステムによって割り当てられたリソースが解放されます。
分離スレッドは他のスレッドを待機させる必要がなく、現在のスレッドの実行が終了するとスレッドは終了し、リソースはすぐに解放されます。糸分離方式はニーズに応じて適切な分離状態を選択できます。
スレッドを切り離されたスレッドとして設定する場合、スレッドが非常に高速に実行されている場合、pthread_create() 関数が戻る前にスレッドが終了する可能性があります。スレッドは終了後にスレッド番号とシステムリソースを他のスレッドに引き継ぐことができるため、pthread_create()関数で取得したスレッド番号を使用して動作するとエラーが発生します。

スレッド ライブラリとスレッド ID について

スレッド ライブラリは、スレッドを作成するためのインターフェイスをユーザーに提供します。スレッド ライブラリはプロセスによって共有領域にマップされ、プロセス内のスレッドはいつでもライブラリ内のコードとデータにアクセスできます。ライブラリにはスレッドを記述する関連構造があります。

 

#include<iostream>
#include<string>
#include<unistd.h>

using namespace std;

std::string hexAddr(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);

    return buffer;
}

void *threadRoutine(void* args)
{
    string name = static_cast<const char*>(args);
    int cnt = 5;
    while(cnt)
    {
        cout << name << " : " << cnt-- << " : " << hexAddr(pthread_self()) << " &cnt: " << &cnt << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1"); // 线程被创建的时候,谁先执行不确定!
    pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2"); // 线程被创建的时候,谁先执行不确定!
    pthread_create(&t3, nullptr, threadRoutine, (void*)"thread 3"); // 线程被创建的时候,谁先执行不确定!

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);

    return 0;
}

 

 

スレッド間の同期

データのセキュリティを確保するという前提の下で、スレッドが特定の順序で重要なリソースにアクセスできるようにして、枯渇の問題を効果的に回避することを同期と呼びます。POSIX セマフォは、共有リソースへの競合のないアクセスを実現するための同期操作に使用されます。POSIX はスレッド間の同期に使用できます。

信号量

セマフォは、オペレーティング システムで最も一般的に使用される同期プリミティブの 1 つです。スピン ロックはビジー待機を実装するロックであり、セマフォはプロセスがスリープ状態になることを可能にします。簡単に言えば、セマフォは 2 つの演算プリミティブ、P 演算と V 演算をサポートするカウンターです。P と V はもともとオランダ語でそれぞれ減少と増加を表す 2 つの単語を指しましたが、後にアメリカ人がそれを down と up に変更し、現在では Linux カーネルでもこの​​ 2 つの名前が呼ばれています。
セマフォの典型的な例は、プロデューサーとコンシューマーの問題です。これは、オペレーティング システム開発の歴史における古典的なプロセス同期問題であり、ダイクストラによって最初に提案されました。生産者が商品を生産し、消費者が商品を購入すると仮定すると、通常、消費者は商品を購入するために実店舗やオンラインショッピングモールに行く必要があります。コンピューターを使用してこのシナリオをシミュレートします。1 つのスレッドがプロデューサーを表し、別のスレッドがコンシューマーを表し、バッファーがストアを表します。プロデューサによって生産された商品は、消費のためにコンシューマ スレッドに供給されるためにバッファに配置され、コンシューマ スレッドはバッファからアイテムを取得してバッファを解放します。商品の生産時に使用可能な空きバッファがないことをプロデューサー スレッドが検出した場合、プロデューサーはコンシューマ スレッドが空きバッファを解放するまで待つ必要があります。消費者スレッドが商品の購入時に店舗の在庫が切れていることを発見した場合、消費者は新しい商品が生産されるまで待たなければなりません。スピンロック機構が使用されている場合、消費者は商品が在庫切れであることに気付いた場合、スツールを移動して店の入り口に座り、配達員が商品を届けるのを待ちますが、セマフォ機構が使用されている場合は、店員は消費者の電話番号を記録し、商品の到着を待って消費者に購入を通知します。もちろん、現実の生活でも、パンのようにすぐに準備できる商品であれば、人は喜んで店内で待ちますが、家電などの商品であれば、絶対に店内で待つことはありません。

スレッド セマフォはプロセス セマフォに似ていますが、スレッド セマフォを使用すると、スレッドベースのリソースのカウントを効率的に実行できます。セマフォは実際には、パブリック リソースを制御するために使用される非負の整数カウンターです。パブリック リソースが増加すると、セマフォの値が増加します。パブリック リソースが消費されると、セマフォの値は減少します。セマフォが 0 より大きい場合にのみ、セマフォによって表されるパブリック リソースにアクセスできます。
セマフォの主な関数は、セマフォ初期化関数 sem_init()、セマフォ破壊関数 sem_destroy()、
セマフォ増加関数 sem_pom()、セマフォ減少関数 sem_wait() など 関数 sem_trywait() もあり、その意味は相互に排他的な関数 pthread_mutex_trylock() と一致しており、最初にリソースが利用可能かどうかを判断します。関数のプロトタイプはヘッダー ファイル semaphore.h で定義されます。
1. スレッドセマフォ初期化関数 sem_int(
) セマフォを初期化する関数です。そのプロトタイプは次のとおりです。

#include <セマフォ.h>

int sem_init(sem_t *sem、int pshared、unsigned int 値);

  • sem はセマフォ構造体のポインタを指します。セマフォの初期化が完了すると、このポインタを使用してセマフォを増減できます。
  • パラメータ pshared は、セマフォの共有タイプを示すために使用されます。0 以外の場合、セマフォはプロセス間で共有できます。それ以外の場合、セマフォは現在のプロセス内の複数のスレッド間でのみ共有できます。
  • パラメータ値は、セマフォの初期化時にセマフォの値を設定するために使用されます。

2. スレッドセマフォ増加関数 sem_post()
sem_post (この関数の機能はセマフォの値を増加させることであり、増加するたびの値は 1 です。このセマフォを待っているスレッドがある場合、待っていたスレッドは戻ります。関数のプロトタイプは次のとおりです。

#include <semaphore.h>
int sem_post (sem_t *sem) ;

3. スレッドセマフォ待機関数 sem_wait()
sem_wait() 関数の機能は、セマフォの値を減らすことであり、セマフォの値が 0 の場合、セマフォの値が 0 より大きくなるまでスレッドはブロックされます。sem_wait() 関数は、セマフォの値を毎回 1 ずつ減らし、セマフォの値が 0 になると減らなくなります。関数のプロトタイプは次のとおりです。

#include <セマフォ。h>
int sem_wait (sem_t *sem) ;

4. スレッド セマフォ破棄関数 sem_destroy()
sem_destroy() 関数は、セマフォ sem を解放するために使用されます。関数プロトタイプは次のとおりです。

#include <セマフォ.h>

int sem_destroy(sem_t *sem) ;

5. スレッドセマフォの例
セマフォの使用例を見てみましょう。ミューテックスの場合、カウントするためにグローバル変数が使用されます。この場合、同じジョブを実行するためにセマフォが使用されます。一方のスレッドはプロデューサを模倣するためにセマフォをインクリメントし、もう一方のスレッドはコンシューマを模倣するためにセマフォを取得します。

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

sem_t sem;
int running = 1;//线程运行控制
int cnt = 5;

//生产者
void *producter_f (void *arg)
{
    int semval = 0;
    while (running)
    {
        usleep(1);
        sem_post(&sem);//信号量增加
        sem_getvalue(&sem,&semval);//获取信号量的值
        printf("生产,总数量:%d\n",semval); 
    }
}

//消费者
void *consumer_f (void *arg)
{
    int semval = 0;
    while (running)
    {
        usleep(1);
        sem_wait(&sem);//信号量减少
        sem_getvalue(&sem,&semval);//获取信号量的值
        printf("消费,总数量:%d\n",semval); 
    }
}
int main ()
{
    pthread_t consumer_t;//消费者线程参数
    pthread_t producter_t;//生产者线程参数
    sem_init(&sem , 0, 16);//信号量初始化
    pthread_create(&producter_t, NULL, producter_f, NULL);//建立生产者线程
    pthread_create(&consumer_t, NULL,consumer_f, NULL) ;//建立消费者线程
    usleep(1) ;//等待
    running =0;//设置线程退出值
    pthread_join(consumer_t, NULL);//等待消费者线程退出
    pthread_join (producter_t, NULL) ;//等待生产者线程退出
    sem_destroy(&sem);//信号量销毁
    return 0;

}

 実行結果から、さまざまなスレッド間に競合関係があることがわかります。ただし、値は 1 つ生成して 1 つ消費するという順序ではなく、交差して表示され、場合によっては複数生成されて消費される競争の結果です。

セマフォには、同時に任意の数のロック所有者を許可できるという興味深い特性があります。セマフォ初期化関数は sem_init(struct semaphore *sem, int count) で、count の値は 1 以上にすることができます。count が 1 より大きい場合、同時に最大 count 個のロック ホルダーが存在することを意味します。このセマフォはカウンティング セマフォと呼ばれます。count が 1 に等しい場合、同時に 1 つの CPU のみがロックを保持できます。このセマフォはミューテックス セマフォまたはバイナリ セマフォと呼ばれます。Linux カーネルでは、カウント値 1 のほとんどのセマフォが使用されます。スピン ロックと比較すると、セマフォはスリープを許可するロックです。セマフォは、カーネルとユーザー空間の間の複雑な相互作用など、複雑な状況と比較的長いロック時間を伴う一部のアプリケーション シナリオに適しています。

スレッド間の相互排他

プロセススレッド間のミューテックス関連の背景概念

クリティカル リソース: マルチスレッド実行ストリームによって共有されるリソースはクリティカル リソースと呼ばれます。

クリティカル セクション: 各スレッド内で、クリティカルな自己娯楽にアクセスするコードはクリティカル セクションと呼ばれます。

相互排他: 相互排他により、常に 1 つだけの実行フローがクリティカル セクションに入り、クリティカル リソースにアクセスし、通常はクリティカル リソースが保護されることが保証されます。

アトミック性: どのスケジューリング メカニズムによっても中断されない操作。この操作には、完了または未完了の 2 つの状態しかありません。

ミューテックス

多くの場合、スレッドが使用するデータはローカル変数であり、その変数のアドレス空間はスレッドのスタック空間にありますが、この場合、変数は単一のスレッドに属し、他のスレッドはこの変数を取得できません。

ただし、多くの変数をスレッド間で共有する必要がある場合があります。このような変数は共有変数と呼ばれ、スレッド間の対話はデータ共有を通じて完了します。

複数のスレッドが共有変数を同時に操作すると、いくつかの問題が発生します。

このコードは、チケット取得プロセスをシミュレートします。 

#include <iostream>

#include <unistd.h>

#include <pthread.h>

名前空間 std を使用します。

int チケット = 100;

void *threadRoutine(void *name)

{

    文字列 tname = static_cast<const char*>(名前);

    その間(真)

    {

        if(チケット > 0)

        {

            usleep(2000); // チケット取得のシミュレーションに費やした時間

            cout << tname << " チケットを取得します: " << ticket-- << endl;

        }

        それ以外

        {

            壊す;

        }

    }

    nullptr を返します。

}

int main()

{

    pthread_t t[4];

    int n = sizeof(t)/sizeof(t[0]);

    for(int i = 0; i < n; i++)

    {

        char *data = 新しいchar[64];

        snprintf(データ, 64, "スレッド-%d", i+1);

        pthread_create(t+i, nullptr, threadRoutine, data);

    }

    for(int i = 0; i < n; i++)

    {

        pthread_join(t[i], nullptr);

    }

    0を返します。

}

 

 

マルチスレッドの同時アクセスチケットにより、現実には無理なマイナス値が直接取得されてしまうことが分かりました。チケットが 1 つだけ残っている場合、この重要なリソースにアクセスするスレッドが複数あるため、重要なリソースをロックする必要があります。

ミューテックス 

初期カウントのサイズに応じて、セマフォはカウント セマフォと相互排他セマフォに分割できます。有名なトイレ理論によると、セマフォは同時に N 人が収容できるトイレに相当し、トイレが満杯でない限り他の人が入ることができ、満杯の場合は外で待つことになります。相互排除ロックは、街路にある携帯トイレのようなもので、一度に一人しか入れず、列に並んでいる人が出てからでないと次の人は入れません。ミューテックスはカウント値が 1 のセマフォに似ているのに、なぜカーネル コミュニティはセマフォを多重化するメカニズムではなくミューテックスを再開発するのでしょうか? 設計の初めに、Linux カーネルでのセマフォの実装は次のとおりです

。問題ありませんが、ミューテックスはセマフォよりもシンプルで軽量です。激しいロック競合が発生するテスト シナリオでは、ミューテックスはセマフォよりも高速でスケーラブルです。また、ミューテックス データ構造はセマフォほど定義されていません。これらは、ミューテックス設計の開始時点での利点です。ミューテックスの一部の最適化スキーム (スピン待機など) は、読み取りおよび書き込みセマフォに移植されています。


スレッド相互排他関数の概要
スレッド相互排他に関連する関数プロトタイプと初期化定数は次のとおりです。主に相互排他初期化メソッドのマクロ定義、相互排他初期化関数 pthread_mutx_init()、相互排他ロック関数 pthread_mutex_lock()、相互排他プレロック関数 pthread_mutx_trylock()、ミューテックスロック解除関数 pthread_mutx_unlock()、およびミューテックス破棄関数 pthread_mutex_destroy() です。

pthread_mutx_init()関数はミューテックス変数を初期化し、構造体 pthread_mutex_t はシステム内部のプライベート データ型ですが、システムがその実装を変更する可能性があるため、これを使用する場合は pthread_mutex_t を直接使用するだけで十分です。属性は NULL で、デフォルトの属性が使用されることを示します。
pthread_mutex_lock()関数宣言によりミューテックスによるロックが開始され、その後コードはpthread_mutx_unlock()関数が呼び出されるまで保護領域内のコードを実行できません。つまり、同時に実行できるスレッドは 1 つだけです。スレッドが pthread_mutex_lock() 関数を実行するときに、その時点でロックが別のスレッドによって使用されている場合、スレッドはブロックされます。つまり、プログラムは別のスレッドがミューテックスを解放するのを待ちます。ミューテックスの使用後に必ずリソースを解放し、pthread_mutex_destroy()関数を呼び出してリソースを解放してください。

ミューテックスはクリティカル セクションを保護するために使用されます。これにより、特定の期間内に 1 つのスレッドだけがコードを実行したり、特定のリソースにアクセスしたりすることが保証されます。次のコードは、プロデューサ/コンシューマのサンプル プログラムです。プロデューサはデータを生成し、コンシューマはデータを消費します。これらは変数を共有し、一度に 1 つのスレッドだけがこのパブリック変数にアクセスします。 

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

using namespace std;

int tickets = 100; 
class TData
{
public:
    TData(const string &name, pthread_mutex_t *mutex):_name(name), _pmutex(mutex)
    {}
    ~TData()
    {}
public:
    string _name;
    pthread_mutex_t *_pmutex;
};

void *threadRoutine(void *args)
{
    TData *td = static_cast<TData*>(args);

    while(true)
    {
        pthread_mutex_lock(td->_pmutex);
        if(tickets > 0) 
        {
            usleep(2000); // 模拟抢票花费的时间
            cout << td->_name << " get a ticket: " << tickets-- << endl;
            pthread_mutex_unlock(td->_pmutex);
        }
        else
        {
            pthread_mutex_unlock(td->_pmutex);
            break;
        }
    }

    return nullptr;
}

int main()
{

   pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);

    pthread_t tids[4];
    int n = sizeof(tids)/sizeof(tids[0]);
    for(int i = 0; i < n; i++)
    {
        char name[64];
        snprintf(name, 64, "thread-%d", i+1);
        TData *td = new TData(name, &mutex);
        pthread_create(tids+i, nullptr, threadRoutine, td);
    }

    for(int i = 0; i < n; i++)
    {
        pthread_join(tids[i], nullptr);
    }

    pthread_mutex_destroy(&mutex);
    return 0;
}

 

 

 ミューテックス ロックは、セマフォの実装よりもはるかに効率的です。

  • ミューテックスは、スピン待機メカニズムを初めて実装したものです。
  • ミューテックスはスリープ前にロックの取得を試みます。
  • ミューテックスは MCS ロックを実装し、複数の CPU によるロックの競合によって引き起こされる CPU キャッシュ ラインのスラッシングを回避します。

ミューテックスの簡易性と効率性ゆえに、セマフォに比べてミューテックスの使用シナリオは厳しく、ミューテックスを使用する際に注意すべき制約は以下のとおりです。

  • 一度に 1 つのスレッドだけがミューテックスを保持できます。
  • ロックホルダーのみがロックを解除できます。あるプロセスでミューテックスを保持し、別のプロセスで解放することはできません。したがって、ミューテックスはカーネルとユーザー空間の間の複雑な同期シナリオには適しておらず、セマフォと読み書きセマフォの方が適しています。
  • 再帰的なロックとロック解除は許可されません。
  • プロセスがミューテックスを保持している場合、プロセスは終了できません。
  • ミューテックスは、公式インターフェイス関数を使用して初期化する必要があります。
  • ミューテックスはスリープできるため、割り込みハンドラーや割り込みの下位部分 (タスクレット、タイマーなど) で使用することはできません。
     

実際のプロジェクトでは、スピン ロック、セマフォ、ミューテックスをどのように選択すればよいですか?
スピン ロックは割り込みコンテキストで躊躇なく使用できますが、クリティカル セクションにスリープ、暗黙的なスリープ アクション、およびカーネル インターフェイス関数がある場合、スピン ロックは避けるべきです。セマフォとミューテックスのどちらを選択するか? コード シナリオが上記のミューテックスの制約のいずれかを満たさない限り、ミューテックスを最初に使用できます。

C++ でスレッドをカプセル化する

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

class Thread
{
public:
    typedef enum
    {
        NEW = 0,
        RUNNING,
        EXITED
    } ThreadStatus;
    typedef void (*func_t)(void *);

public:
    Thread(int num, func_t func, void *args) : _tid(0), _status(NEW), _func(func), _args(args)
    {
        char name[128];
        snprintf(name, sizeof(name), "thread-%d", num);
        _name = name;
    }
    int status() { return _status; }
    std::string threadname() { return _name; }
    pthread_t threadid()
    {
        if (_status == RUNNING)
            return _tid;
        else
        {
            return 0;
        }
    }
    // 类的成员函数,具有默认参数this,需要static
    // 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数,将this指针传过来
    static void *runHelper(void *args)
    {
        Thread *ts = (Thread*)args; // 拿到了当前对象
        // _func(_args);
        (*ts)();
        return nullptr;
    }
    void operator ()() //仿函数
    {
        if(_func != nullptr) _func(_args);
    }
    void run()
    {
        int n = pthread_create(&_tid, nullptr, runHelper, this);
        if(n != 0) exit(1);
        _status = RUNNING;
    }
    void join()
    {
        int n = pthread_join(_tid, nullptr);
        if( n != 0)
        {
            std::cerr << "main thread join thread " << _name << " error" << std::endl;
            return;
        }
        _status = EXITED;
    }
    ~Thread()
    {}

private:
    pthread_t _tid;
    std::string _name;
    func_t _func; // 线程未来要执行的回调
    void *_args;
    ThreadStatus _status;
};

C++ を使用してミューテックス ロックをカプセル化する

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

class Mutex // 自己不维护锁,有外部传入
{
public:
    Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *_pmutex;
};

class LockGuard // 自己不维护锁,由外部传入
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        _mutex.lock();
    }
    ~LockGuard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};

 使用

#include <iostream>
#include <string>
#include <unistd.h>
#include "Thread.hpp"
#include "lockGuard.hpp"

using namespace std;

int tickets = 100;                                // 全局变量,共享对象
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 在外边定义的锁
void threadRoutine(void *args)
{
    std::string message = static_cast<const char *>(args);
    while (true)
    {
        {
            LockGuard lockguard(&mutex); // RAII 风格的锁
            if (tickets > 0)
            {
                usleep(2000);
                cout << message << " get a ticket: " << tickets-- << endl; // 临界区
            }
            else
            {
                break;
            }
        }
    }
}

int main()
{
    Thread t1(1, threadRoutine, (void *)"hello world");
    Thread t2(2, threadRoutine, (void *)"hello leiyaling");
    t1.run();
    t2.run();

    t1.join();
    t2.join();
    return 0;
}

ミューテックス ロックは、ロックとロック解除のみがアトミックであるため、スレッドの安全性を保証できます。 

スレッド セーフ: 複数のスレッドが同じコードを同時に実行しても、異なる結果は表示されません。この問題は、グローバル変数または静的変数に対する操作が一般的で、ロック保護がない場合に発生します。

リエントランシー: 同じ関数が異なる実行フローから呼び出され、現在のプロセスが実行される前に、他の実行フローが再度入ることをリエントランシーと呼びます。リエントラントの場合でも実行結果に差異や問題がない場合はリエントラント関数と呼ばれ、そうでない場合は非リエントラント関数と呼ばれます。 

一般的なスレッドの安全でない状況

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

一般的なスレッドの安全性の状況

  • 各スレッドには、グローバル変数または静的変数への読み取りアクセスのみがあり、書き込みアクセスはありません。一般に、これらのスレッドは安全です。
  • クラスまたはインターフェイスはスレッドのアトミック操作です
  • 複数のスレッド間の切り替えによって、このインターフェイスの実行結果があいまいになることはありません

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

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

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

  • グローバル変数や静的変数は使用しないでください
  • malloc または new によって開かれたスペースは使用しないでください。
  • 非リエントラント関数は呼び出されず、静的データまたはグローバル データを返しません。すべてのデータは関数の呼び出し元によって提供されます。
  • ローカル データを使用するか、グローバル データのローカル コピーを作成してグローバル データを保護します

再入可能性とスレッドの安全性

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

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

  • リエントラント関数は、スレッドセーフ関数の一種です。
  • スレッド セーフは必ずしもリエントラントである必要はありませんが、リエントラント関数はスレッド セーフである必要があります。
  • 重要なリソースへのアクセスがロックされている場合、この関数はスレッドセーフですが、リエントラント関数がロックを解放していない場合はデッドロックが発生するため、リエントラントではありません。

読み書きロック
 

上記で紹介したセマフォには明らかな欠点があります。クリティカル セクションの読み取りプロパティと書き込みプロパティの区別がないということです。読み取り/書き込みロックでは、通常、複数のスレッドがクリティカル セクションを同時に読み取ってアクセスできるようになりますが、書き込みアクセスは 1 つのスレッドのみに制限されます。読み取り/書き込みロックは同時実行性を効果的に向上させることができます。マルチプロセッサ システムでは、複数のリーダーが共有リソースに同時にアクセスできますが、ライターは排他的です。読み取り/書き込みロックには次の特性があります。

  • クリティカルセクションには複数の読者が同時に入ることができますが、ライターは同時に入ることはできません。
  • クリティカル セクションに入ることができるのは、一度に 1 人のライターだけです。
  • リーダーとライターは同時に重要なセクションに入ることができません。

 

デッドロック

デッドロックとは、プロセス群内の各プロセスが解放されないリソースを占有しているにもかかわらず、他のプロセスが使用しており解放されないリソースを相互に占有しているため、永続的に待ち状態になっている状態を指します。

デッドロックに必要な 4 つの条件

  • 相互に排他的な条件: リソースは一度に 1 つの実行フローのみで使用できます。
  • リクエストおよびホールド条件: リソースのリクエストによって実行フローがブロックされた場合、取得したリソースをホールドします。
  • 非剥奪条件: 実行フローによって取得されたリソースは、使い果たされる前に強制的に剥奪することはできません。
  • 循環待機条件: 複数の実行フロー間で、先頭から末尾までの循環待機リソース関係が形成されます。

デッドロックを回避する

  • 行き詰まりを打破するために必要な4つの条件
  • ロックする順番は同じです
  • ロックが解放されないシナリオを回避する
  • 1 回限りのリソース割り当て 

おすすめ

転載: blog.csdn.net/m0_55752775/article/details/130688298