Linux マルチスレッド プログラミングの学習体験

  目次



序文

なぜスレッドがあるのでしょうか?
        あなたが現在工場の所有者であれば、工場には生産ラインがあります。現在、供給が需要を上回っており、生産規模を拡大する必要があります。プロセスの観点から見ると、別の工場を作成して以前の生産ラインをコピーするだけですが、元の工場にいくつかの生産ラインを直接追加するとどうなるでしょうか? では、この拡張の規模はもっと小さくすべきでしょうか? この方法はスレッド方式です。




1. Linux スレッドの概要

1. プロセスとスレッド

        一般的な UNIX/Linux プロセスは、制御スレッドが 1 つだけあると見なすことができます。つまり、プロセスは同時に 1 つのことだけを実行します。複数の制御スレッドを使用すると、プログラム設計中に同時に複数のことを実行するようにプロセスを設計でき、各スレッドが独立したタスクを処理します。プロセスは実行中のプログラムのインスタンスであり、システム リソース (CPU 時間、メモリなど) の割り当てを担当する基本単位です。スレッド用に設計されたシステムでは、プロセス自体は基本的な実行単位ではなく、スレッドのコンテナーです。プログラム自体は、命令、データ、およびそれらの構成の単なる記述であり、プロセスはプログラムの実際の実行インスタンス (これらの命令とデータ) です。

        スレッドは、オペレーティング システムが操作のスケジューリングを実行できる最小単位です。これはプロセスに含まれており、プロセス内の実際の操作単位となります。スレッドとは、プロセス内の単一の順次制御フローを指します。プロセス内で複数のスレッドを同時に実行でき、各スレッドは異なるタスクを並行して実行します。スレッドには、プロセス内のスレッドを表すスレッド ID、レジスタ値のセット、スタック、スケジューリングの優先順位と戦略、シグナル マスク ワード、errno 定数、スレッドのプライベート データなど、プロセス内の実行環境を表すために必要な情報が含まれています。 。プロセスに関するすべての情報は、実行可能プログラムのテキスト、プログラムのグローバル メモリとヒープ メモリ、スタック、ファイル記述子など、プロセスのすべてのスレッドで共有されます。

「プロセス - リソース割り当ての最小単位、スレッド - プログラム実行の最小単位」

        プロセスには独立したアドレス空間があり、プロセスがクラッシュしても保護モードの他のプロセスには影響せず、スレッドはプロセス内の異なる実行パスにすぎません。スレッドには独自のスタックとローカル変数がありますが、スレッドには個別のアドレス空間はありません。1 つのスレッドの終了はプロセス全体の終了と同等です。したがって、マルチプロセス プログラムはマルチスレッド プログラムよりも堅牢ですが、プロセスを切り替えるときにより多くのリソースを消費し、大規模になり、効率が低下します。ただし、同時操作と特定の変数の共有を必要とする一部の同時操作では、プロセスではなくスレッドのみを使用できます。

2. スレッドを使用する理由

        プロセスとスレッドの違い: 実際、これらの違いがスレッドを使用する理由です。一般に、プロセスには独立したアドレス空間がありますが、スレッドには独立したアドレス空間がありません (同じプロセス内のスレッドはプロセスのアドレス空間を共有します)。

        マルチスレッドを使用する理由の 1 つは、マルチスレッドがプロセスに比べて非常に「倹約的な」マルチタスクの方法であるということです。Linux システムでは、新しいプロセスを開始するときに、コード セグメント、スタック セグメント、およびデータ セグメントを維持するために、独立したアドレス空間を割り当て、多数のデータ テーブルを確立する必要があることがわかっていますが、これは「高価な」マルチタスク プロセスです。作業の方法。プロセス内で実行されている複数のスレッドは、互いに同じアドレス空間を使用し、ほとんどのデータを共有します。スレッドの開始に必要な空間は、プロセスの開始に必要な空間よりもはるかに小さくなります。さらに、スレッドは相互に切り替わります。時間は、また、プロセス間の切り替えに必要な時間よりもはるかに短くなります。

        マルチスレッドを使用する2 番目の理由は、スレッド間の通信メカニズムが便利であることです。プロセスごとに独立したデータ空間を持ち、通信でしかデータの受け渡しができないため、時間がかかるだけでなく非常に不便です。スレッドの場合はそうではなく、同一プロセス内のスレッド間でデータ空間を共有するため、あるスレッドのデータを他のスレッドで直接利用することができ、速いだけでなく便利です。もちろん、データ共有は他の問題も引き起こします。一部の変数は 2 つのスレッドで同時に変更できません。サブルーチンで静的として宣言された一部のデータは、マルチスレッド プログラムに壊滅的な打撃を与える可能性が高くなります。これらは正しいです。これは正しいです。マルチスレッド プログラムを作成するときに注意すべき最も重要な点です。

プロセスと比較して、マルチタスクおよび同時作業方法としてのマルチスレッド プログラムには、次のような利点があります。

  • アプリケーションの応答性を向上させます。これは、グラフィカル インターフェイス プログラムで特に重要です。操作に時間がかかると、システム全体が操作を待ちます。このとき、プログラムはキーボード、マウス、メニュー操作に応答しません。マルチスレッド テクノロジを使用すると、 (時間のかかる) 操作を新しいスレッドに配置すると、この厄介な状況を回避できます。
  • マルチ CPU システムをより効率的にします。オペレーティング システムは、スレッドの数が CPU の数を超えない場合、異なるスレッドが異なる CPU で実行されるようにします。
  • プログラム構造を改善します。長く複雑なプロセスを複数のスレッドに分割し、いくつかの独立または半独立した実行部分にすることができ、そのようなプログラムは理解と変更が容易になります。





2. Linuxにおけるスレッド開発APIの概要

 マルチスレッド開発は、Linux プラットフォーム上の成熟した pthread ライブラリによってすでにサポートされています。マルチスレッド開発に関わる最も基本的な概念は主に、スレッド、ミューテックス ロック、条件の 3 点です。このうち、スレッドの操作はスレッドの作成、終了、待機の 3 種類に分けられます。ミューテックス ロックには、作成、破棄、ロック、ロック解除の 4 つの操作が含まれます。条件付き操作には、作成、破棄、トリガー、ブロードキャスト、待機の 5 種類があります。

詳細については、以下の表を参照してください。

1. スレッド自体に関連する API 

        1. スレッドの作成       

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
// 返回:若成功返回0,否则返回错误编号

pthread_create が正常に返される        と、tidp が指すメモリ ユニットは、新しく作成されたスレッドのスレッド ID に設定されます。attr パラメータは、さまざまなスレッド属性をカスタマイズするために使用されます。一時的に NULL に設定して、デフォルトの属性を持つスレッドを作成できます。

        新しく作成されたスレッドは、start_rtn 関数のアドレスから実行を開始します。この関数には、型なしのポインター パラメーター arg が 1 つだけあります。start_rtn 関数に複数のパラメータを渡す必要がある場合は、これらのパラメータを構造体に配置し、この構造体のアドレスを arg パラメータとして渡す必要があります。arg パラメータにより、子スレッドが動作できるようになります。

void *func1(void *arg)
int param = 100;
pthread_t t1;
ret = pthread_create(&t1,NULL,func1,(void *)&param);

        2. スレッドの終了

        単一スレッドは 3 つの方法で終了でき、プロセス全体を終了せずに制御フローを停止できます。

  1) スレッドはスタートアップ ルーチンから戻ったばかりで、戻り値はスレッドの終了コードです。

  2) スレッドは、同じプロセス内の他のスレッドによってキャンセルされる可能性があります。

  3) スレッドは pthread_exit を呼び出します。

#include <pthread.h>
int pthread_exit(void *rval_ptr);
//rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。
//进程中的其他线程可以通过调用pthread_join函数访问到这个指针。

        3. スレッド待機中

#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回:若成功返回0,否则返回错误编号

        この関数を呼び出すスレッドは、指定されたスレッドが pthread_exit を呼び出して起動ルーチンから戻るまでブロックされます。ルーチンが起動ルーチンから単に戻った場合、rval_ptr には戻りコードが含まれます。

        pthread_join を呼び出すことで、スレッドを自動的にデタッチ状態にできるため、リソースを復元できます。スレッドがすでにデタッチ状態にある場合、pthread_join 呼び出しは失敗し、EINVAL を返します。

        スレッドの戻り値に興味がない場合は、rval_ptr を NULL に設定できます。この場合、pthread_join 関数を呼び出すと、指定されたスレッドが終了するまで待機しますが、スレッドの終了ステータスは取得されません。

  4. 糸の外れ

        スレッドは参加可能 (デフォルト) または切り離されます。結合可能なスレッドが終了すると、そのスレッド ID と終了ステータスは、別のスレッドがそのスレッドに対して pthread_join を呼び出すまで保持されます。切り離されたスレッドはデーモンのようなもので、スレッドが終了すると関連するリソースがすべて解放されるため、スレッドの終了を待つことができません。あるスレッドが別のスレッドがいつ終了するかを知る必要がある場合は、2 番目のスレッドのランデブーを維持するのが最善です。

pthread_detach 関数は、指定されたスレッドをデタッチ状態に変更します。

#include <pthread.h>
int pthread_detach(pthread_t thread);
// 返回:若成功返回0,否则返回错误编号

この関数は通常、次のステートメントのように、スレッド自体を切り離したいスレッドによって使用されます。

pthread_detach(pthread_self());

        5. スレッドIDの取得と比較

#include <pthread.h>
pthread_t pthread_self(void);
// 返回:调用线程的ID

 スレッド ID の比較やポータブル操作では、システムによってスレッド ID の定義が異なる可能性があるため、スレッド ID を単純に整数として扱うことはできません。次の関数を使用する必要があります。

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
// 返回:若相等则返回非0值,否则返回0

  マルチスレッド プログラムの場合、多くの場合、これらの複数のスレッドを同期する必要があります。同期とは、一定期間内に 1 つのスレッドのみが特定のリソースにアクセスできるようにすることを指します。この間、他のスレッドはリソースにアクセスできません。ミューテックス、条件変数、リーダー/ライター ロックを通じてリソースを同期できます。

2. ミューテックスロックに関するAPI

        ミューテックスは本質的にはロックであり、共有リソースにアクセスする前にミューテックスはロックされ、アクセスが完了するとミューテックスのロックが解放されます。ミューテックスがロックされた後、そのミューテックスを再度ロックしようとする他のスレッドは、現在のスレッドがミューテックス ロックを解放するまでブロックされます。ミューテックスが解放されたときに複数のスレッドがブロックされている場合、ミューテックス上でブロックされているすべてのスレッドが実行可能になります。実行可能になった最初のスレッドがミューテックスをロックでき、他のスレッドはミューテックスがまだロックされていることがわかります。戻って、再び利用可能になるまで待ちます。このように、一度に 1 つのスレッドのみが順方向に実行できます。

        ミューテックス変数は、pthread_mutex_t データ型で表されます。ミューテックス変数は、使用する前に初期化する必要があります。定数 PTHREAD_MUTEX_INITIALIZER に設定するか (静的に割り当てられたミューテックスの場合のみ)、または pthread_mutex_init 関数を呼び出して初期化できます。ミューテックスが動的に割り当てられる場合 (たとえば、malloc 関数の呼び出しによって)、メモリが解放される前に pthread_mutex_destroy を呼び出す必要があります。     

 pthread_mutex_init(&mutex,NULL);
 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

  1.ミューテックスロックの作成と破棄


#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t mutex);
// 返回:若成功返回0,否则返回错误编号

        デフォルトの属性でミューテックスを初期化するには、単に attr を NULL に設定します。

        2. ロックとロック解除

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t mutex);
int pthread_mutex_unlock(pthread_mutex_t mutex);
int pthread_mutex_trylock(pthread_mutex_t mutex);
// 返回:若成功返回0,否则返回错误编号

        スレッドがブロックされたくない場合は、pthread_mutex_trylock を使用してミューテックスのロックを試みることができます。pthread_mutex_trylock が呼び出されたときにミューテックスがロック解除状態にある場合、pthread_mutex_trylock はブロックせずにミューテックスをロックし、0 を返します。それ以外の場合、pthread_mutex_trylock は失敗し、ミューテックスをロックできず EBUSY を返します。

        ミューテックスロックを使用するとロックアップが発生します。ロックが2つの場合のみロックアップ状態になります。ロックが1つの場合はロックアップは発生しません。ロックされるとプログラムは継続的に実行できなくなります。ロックアップのプロセスは大まかに次のとおりです。スレッド A はロック 1 を取得し、1 秒間スリープし、ロック 2 を取得します。スレッド B はロック 2 を取得し、1 秒間スリープし、ロック 1 を取得します。   

3. 条件変数に関するAPI

        

        条件変数は、スレッドで使用できるもう 1 つの同期メカニズムです。条件変数は、複数のスレッドが集まる場所を提供します。条件変数をミューテックスとともに使用すると、スレッドは競合のない方法で特定の条件が発生するのを待つことができます。

  条件自体はミューテックスによって保護されています。スレッドは、条件の状態を変更する前に、まずミューテックスをロックする必要があります。条件を評価する前にミューテックスをロックする必要があるため、他のスレッドはミューテックスを取得するまでこの変更に気づきません。

  条件変数は使用前に初期化する必要があります。pthread_cond_t データ型で表される条件変数は 2 つの方法で初期化できます。定数 PTHREAD_COND_INITIALIZER は静的に割り当てられた条件変数に割り当てることができますが、条件変数が動的に割り当てられている場合は、 pthread_cond_destroy 関数を使用して条件変数を条件付けします。変数は初期化されません。

        1. 生成・破壊条件の変更

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t cond);
// 返回:若成功返回0,否则返回错误编号

デフォルト以外の属性を使用して条件変数を作成する必要がない限り、pthread_cont_init 関数の attr パラメーターを NULL に設定できます。

        2.待ちます

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, cond struct timespec *restrict timeout);
// 返回:若成功返回0,否则返回错误编号

  pthread_cond_wait は、条件が true になるまで待機します。指定された時間内に条件が満たされない場合、エラー コードを表す戻り変数が生成されます。pthread_cond_wait に渡されるミューテックスは条件を保護し、呼び出し元はロックされたミューテックスを関数に渡します。この関数は、呼び出し元のスレッドを条件を待機しているスレッドのリストに追加し、ミューテックスのロックを解除します。どちらの操作もアトミック操作です。これにより、条件チェックとスレッドが条件の変化を待つためにスリープ状態になる間の時間チャネルが閉じられ、スレッドが条件の変化を見逃すことがなくなります。pthread_cond_wait が返されると、ミューテックスは再びロックされます。

  pthread_cond_timedwait 関数は、タイムアウトが 1 つあることを除いて、pthread_cond_wait 関数と同様に機能します。timeout は待機時間を指定します。これは timespec 構造体を通じて指定されます。

        3. トリガー

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t cond);
int pthread_cond_broadcast(pthread_cond_t cond);
// 返回:若成功返回0,否则返回错误编号

        これら 2 つの関数を使用して、条件が満たされたことをスレッドに通知できます。pthread_cond_signal 関数は条件を待機しているスレッドを起動し、pthread_cond_broadcast 関数は条件を待機しているすべてのプロセスを起動します。

  条件付き状態を変更した後は、スレッドにシグナルを送信する必要があることに注意してください。

2. 例

例 1: 単純なスレッドの作成:次のプログラムは、スレッド内の変数 param を出力する単純なスレッド t1 を作成します。

#include <stdio.h>

#include <pthread.h>

*func1(void *arg)
{
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));


}

int main()
{

        int ret;
        int param = 100;
        pthread_t t1;
        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());

        return 0;

}

プログラムの実行結果:

明らかに、このプログラムのプロセスはスレッド t1 を実行しません。

  具体的な理由は、メイン スレッドと新しいスレッドの間に競合があるためです。メイン スレッドは、メイン スレッドが終了する前に、新しいスレッドの実行が完了するまで待機する必要があります。メイン スレッドが待機しない場合、メイン スレッドが終了する可能性があるため、新しいスレッドが実行される前にプロセス全体が終了します。この動作は、オペレーティング システムのスレッド実装とスケジューリング方法によって異なります。

例 2: 上記の問題を解決するには、メイン スレッドに pthread_join(t1,NULL) を導入します。その目的は、メイン スレッドがここでの実行後にブロック状態に入り、新しいスレッドの実行が完了するのを待つことです。

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

void *func1(void *arg)
{
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));


}

int main()
{

        int ret;
        int param = 100;
        pthread_t t1;
        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());


        pthread_join(t1,NULL);

        return 0;

}
 





プログラムを実行した結果は次のようになります。 

 例 3: スレッドを作成し、戻り値を指定して pthread_exit((void *)&ret); を呼び出します。

#include <stdio.h>

#include <pthread.h>

void *func1(void *arg)
{
        static int ret = 10;
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));

        pthread_exit((void *)&ret);
}

int main()
{
        int *pret;

        int ret;
        int param = 100;
        pthread_t t1;
        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());


        pthread_join(t1,(void **)&pret);
        printf("main:ti quit:%d\n",*pret);
        return 0;

}
   

操作結果:

 戻り値の文字列も使用できます。

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

#include <stdio.h>

#include <pthread.h>

void *func1(void *arg)
{
        static int ret = 10;
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));

        pthread_exit((void *)&ret);
}

void *func2(void *arg)
{
        static int ret = 10;
        printf("t2:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t2:paaram is %d\n",*((int *)arg));

        pthread_exit((void *)&ret);

}
int main()
{
        int *pret;

        int ret;
        int param = 100;
        pthread_t t1;
        pthread_t t2;

        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        ret = pthread_create(&t2,NULL,func2,(void *)&param);
        if(ret == 0){

                printf("main:creat t2 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());


        pthread_join(t1,(void **)&pret);
        pthread_join(t2,(void **)&pret);
        printf("main:ti quit:%d\n",*pret);
        return 0;

}

操作結果:

 実行結果から判断すると、2 つの実行結果が異なっており、2 つのスレッド間で競合があることがわかります。これにより、どちらのスレッドを先に実行させるかが不便になります。この現象を解決する方法は、ミューテックス ロックを導入することです。 。

例 5: スレッド 1 を実行し、3 までカウントした後に終了します。ミューテックス ロックを条件と組み合わせて使用​​することをお勧めします。次の例はあまり適切ではありません。

#include <stdio.h>

#include <pthread.h>

pthread_mutex_t mutex;
int g_data = 0;

void *func1(void *arg)
{
        printf("t1:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t1:paaram is %d\n",*((int *)arg));


        pthread_mutex_lock(&mutex);
        while(1){
                printf("t1:%d\n",g_data++);
                sleep(1);
                if(g_data == 3){
                        pthread_mutex_unlock(&mutex);
                        pthread_exit(NULL);
                }
        }
}
void *func2(void *arg)
{

        printf("t2:%ld ptread  creat success\n",(unsigned long )pthread_self());
        printf("t2:paaram is %d\n",*((int *)arg));

        while(1){
                printf("t2:%d\n",g_data);
                pthread_mutex_lock(&mutex);
                g_data++;
                pthread_mutex_unlock(&mutex);
               sleep(1);
        }
}

int main()
{
      
        int ret;
        int param = 100;
        pthread_t t1;
        pthread_t t2;
        pthread_mutex_init(&mutex,NULL);
        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0){

                printf("main:creat t1 success\n");
        }
        ret = pthread_create(&t2,NULL,func2,(void *)&param);
        if(ret == 0){

                printf("main:creat t2 success\n");
        }
        printf("main:%ld\n",(unsigned long)pthread_self());

        while(1){

                printf("t2:%d\n",g_data);

                sleep(1);
        }
        pthread_join(t1,NULL);
        pthread_join(t2,NULL);
        pthread_mutex_destroy(&mutex);
        return 0;
}                                                                        





要約する

参考書籍『UNIX環境での応用プログラミング』

参考:Linuxマルチスレッドプログラミングに関する予備研究 - Fengzi_太陽の光を仰ぐ - Blog Garden (cnblogs.com)

上記は、Linux スレッドに関する私の学習経験です。初心者である場合はご容赦ください。お役に立ちましたら、いいね! やサポートをお願いします。

おすすめ

転載: blog.csdn.net/qq_44848795/article/details/122012253