Linux でのマルチスレッドプログラミング例の分析

Linux でのマルチスレッドプログラミング例の分析

1 はじめに

スレッド技術は 1960 年代にすでに提案されていましたが、マルチスレッドがオペレーティング システムに本格的に適用されたのは 1980 年代半ばになってからであり、この点では Solaris が先駆者でした。従来の Unix もスレッドの概念をサポートしていますが、プロセス内で許可されるスレッドは 1 つだけなので、マルチスレッドはマルチプロセスを意味します。現在、マルチスレッド テクノロジは、Windows/NT、そしてもちろん Linux を含む多くのオペレーティング システムでサポートされています。
プロセスの概念を理解した後で、なぜスレッドを導入する必要があるのでしょうか?マルチスレッドを使用する利点は何ですか?どのようなシステムでマルチスレッドを選択すべきでしょうか?まずこれらの質問に答えなければなりません。
マルチスレッドを使用する理由の 1 つは、プロセスに比べてマルチタスクを非常に「簡素に」実行できることです。 Linux システムでは、新しいプロセスを開始するには、独立したアドレス空間を割り当て、コード セグメント、スタック セグメント、およびデータ セグメントを維持するための多数のデータ テーブルを確立する必要があることはわかっています。これは、複数のタスクを処理する「コストのかかる」方法です。プロセス内で実行される複数のスレッドは同じアドレス空間を使用し、ほとんどのデータを共有します。スレッドの開始に費やされる空間は、プロセスの開始に費やされる空間よりもはるかに小さくなります。さらに、スレッド間の切り替えに必要な時間は、プロセス間の切り替えに必要な時間よりもはるかに短くなります。統計によると、一般的にプロセスのオーバーヘッドはスレッドの約 30 倍です。もちろん、このデータは特定のシステムによって大きく異なる場合があります。
マルチスレッドを使用する 2 番目の理由は、スレッド間の便利な通信メカニズムです。異なるプロセスには独立したデータ空間があり、データは通信を通じてのみ転送できるため、時間がかかるだけでなく不便です。スレッドの場合はそうではありません。同じプロセス内のスレッドはデータ空間を共有するため、あるスレッドのデータを他のスレッドが直接使用することができ、高速であるだけでなく便利です。もちろん、データ共有には他の問題も伴います。一部の変数は、2 つのスレッドで同時に変更できません。サブルーチンで静的として宣言された一部のデータは、マルチスレッド プログラムに壊滅的な打撃を与える可能性が高くなります。これらは、マルチスレッド プログラムを作成するときに最も注意が必要な領域です。
上記の利点に加えて、マルチタスクおよび同時作業方式としてのマルチスレッド プログラムには、プロセスと比較して次の利点があります。
1) アプリケーションの応答性を向上させます。これは、グラフィカル インターフェイス プログラムにとって特に意味があります。操作に長い時間がかかる場合、システム全体がこの操作を待機します。このとき、プログラムはキーボード、マウス、またはメニュー操作に応答しません。マルチスレッド テクノロジを使用して、時間のかかる操作を新しいスレッドに配置すると、この厄介な状況を回避できます。
2) マルチCPUシステムをより効率的にします。オペレーティング システムは、スレッド数が CPU 数より大きくない場合、異なるスレッドが異なる CPU 上で実行されるようにします。
3) プログラム構造を改善する。長くて複雑なプロセスは複数のスレッドに分割することができ、複数の独立した、または半独立した実行部分にすることができます。このようなプログラムは理解しやすく、変更も容易になります。
簡単なマルチスレッドプログラムを書いてみましょう。

2 シンプルなマルチスレッドプログラミング

Linux システムでのマルチスレッドは、pthread と呼ばれる POSIX スレッド インターフェイスに従います。 Linux でマルチスレッド プログラムを作成するには、リンク時にヘッダー ファイル pthread.h とライブラリ libpthread.a を使用する必要があります。ちなみに、Linux での pthread の実装は、システム コール clone() を通じて実現されます。 clone() は Linux 固有のシステムコールです。使い方は fork と似ています。clone() の詳細については、関連するドキュメントを参照してください。以下に、最も単純なマルチスレッド プログラム example1.c を示します。

/* 例.c */
#include <stdio.h>
#include <pthread.h>
空スレッド(void)
{
整数 i;
(i=0;i<3;i++) の場合
printf("これはpthreadです。/n");
}

int メイン(void)
{
pthread_t ID;
int i,ret;
ret = pthread_create(&id,NULL,(void *) スレッド,NULL);


if(ret!=0)

{
printf ("pthread 作成エラー!/n");
出口(1)
}
(i=0;i<3;i++) の場合
printf("これがメインプロセスです。/n");
pthread_join(id,NULL);
戻り値 (0);
}

このプログラムをコンパイルします:
gcc example1.c -lpthread -o example1
example1 を実行すると、次の結果が得られます。

これがメインのプロセスです。
これは pthread です。
これがメインのプロセスです。
これがメインのプロセスです。
これは pthread です。
これは pthread です。

再度実行すると、次の結果が得られます。

これは pthread です。
これがメインのプロセスです。
これは pthread です。
これがメインのプロセスです。
これは pthread です。
これがメインのプロセスです。

2 つの結果は異なりますが、これは 2 つのスレッドが CPU リソースを競合した結果です。上記の例では、pthread_create と pthread_join の 2 つの関数を使用し、pthread_t 型の変数を宣言しました。
pthread_t はヘッダー ファイル /usr/include/bits/pthreadtypes.h で定義されています。
typedef 符号なし long int pthread_t;
スレッドの識別子です。関数 pthread_create はスレッドを作成するために使用されます。そのプロトタイプは次のとおりです。
外部 int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));
最初のパラメーターはスレッド識別子へのポインター、2 番目のパラメーターはスレッド属性の設定に使用され、3 番目のパラメーターはスレッド実行関数の開始アドレス、最後のパラメーターは実行関数のパラメーターです。ここでは、関数スレッドはパラメータを必要としないため、最後のパラメータは null ポインタに設定されます。また、2 番目のパラメータを null ポインタに設定して、デフォルトのプロパティを持つスレッドを生成します。次のセクションでは、スレッド属性を設定および変更する方法について説明します。スレッドが正常に作成されると、関数は 0 を返します。0 以外の場合は、スレッドの作成に失敗したことを意味します。一般的なエラー戻りコードは EAGAIN と EINVAL です。前者は、スレッド数が多すぎるなど、システムが新しいスレッドの作成を制限していることを意味します。後者は、2 番目のパラメータによって表されるスレッド属性値が不正であることを意味します。スレッドが正常に作成された後、新しく作成されたスレッドはパラメータ 3 と 4 によって決定された関数を実行し、元のスレッドは次のコード行の実行を続行します。
関数 pthread_join は、スレッドの終了を待機するために使用されます。関数のプロトタイプは次のとおりです。
外部 int pthread_join __P ((pthread_t __th, void **__thread_return));
最初のパラメーターは待機対象のスレッドの識別子であり、2 番目のパラメーターは待機対象のスレッドの戻り値を格納するために使用できるユーザー定義のポインターです。この関数はスレッド ブロッキング関数です。この関数を呼び出す関数は、待機中のスレッドが終了するまで待機します。関数が戻ると、待機中のスレッドのリソースが再利用されます。スレッドを終了する方法は 2 つあります。1 つは上記の例のように、関数が終了すると、それを呼び出したスレッドも終了する方法です。もう 1 つは、関数 pthread_exit を通じて実装する方法です。関数のプロトタイプは次のとおりです。
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一の引数は関数の戻りコードであり、pthread_join の 2 番目の引数である thread_return が NULL でない限り、thread_return に渡されます。最後に、スレッドは複数のスレッドによって待機できないことに注意してください。そうでない場合、シグナルを受信した最初のスレッドが正常に戻り、pthread_join を呼び出す残りのスレッドはエラー コード ESRCH を返します。
このセクションでは、最も単純なスレッドを作成し、最もよく使用される 3 つの関数 pthread_create、pthread_join、pthread_exit を習得しました。次に、スレッドの一般的なプロパティとその設定方法について学習します。

3 スレッドプロパティを変更する

前のセクションの例では、pthread_create 関数を使用してスレッドを作成しました。このスレッドでは、デフォルトのパラメータを使用しました。つまり、関数の 2 番目のパラメータは NULL に設定されました。実際、ほとんどのプログラムではデフォルトのプロパティを使用するだけで十分ですが、それでもスレッドの関連するプロパティを理解することは必要です。
属性構造は pthread_attr_t であり、ヘッダー ファイル /usr/include/pthread.h でも定義されています。詳細を知りたい方は、自分で調べてみてください。属性値は直接設定することはできず、関連する関数を使用して操作する必要があります。初期化関数は pthread_attr_init であり、pthread_create 関数の前に呼び出す必要があります。属性オブジェクトには、主に、バインドするかどうか、デタッチするかどうか、スタック アドレス、スタック サイズ、優先度が含まれます。デフォルトの属性は、バインドなし、非デタッチ、デフォルトの 1M スタック、および親プロセスと同じ優先度レベルです。
スレッドバインディングに関しては、Light Weight Process (LWP) という別の概念が関係します。ライトプロセスは、ユーザー層とシステム層の間に位置するカーネル スレッドとして理解できます。システムは、軽量プロセスを通じてスレッド リソースを割り当て、スレッドを制御します。軽量プロセスは、1 つ以上のスレッドを制御できます。デフォルトでは、システムは、開始される軽量プロセスの数と、どの軽量プロセスがどのスレッドを制御するかを制御します。この状況は、アンバウンドと呼ばれます。バインディング状態では、名前が示すように、スレッドはライト プロセスに固定的に「バインド」されます。バインドされたスレッドは、CPU タイム スライスのスケジュールが軽量プロセス向けに設定されているため、応答速度が速くなります。バインドされたスレッドでは、必要なときに軽量プロセスが常に利用可能であることが保証されます。バインドされた軽量プロセスの優先度とスケジュール レベルを設定することで、バインドされたスレッドはリアルタイム応答などの要件を満たすことができます。
スレッドのバインディング状態を設定する関数は pthread_attr_setscope で、2 つのパラメータがあります。1 つ目は属性構造体へのポインタで、2 つ目はバインディング タイプで、PTHREAD_SCOPE_SYSTEM (バインド) と PTHREAD_SCOPE_PROCESS (バインド解除) の 2 つの値があります。次のコードはバインドされたスレッドを作成します。

#include <pthread.h>
pthread_attr_t 属性;
pthread_t の tid です。

/* プロパティ値を初期化し、すべてデフォルト値に設定します */
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

pthread_create(&tid, &attr, (void *) my_function, NULL);

スレッドの分離状態によって、スレッド自体が終了する方法が決まります。上記の例では、スレッドのデフォルト プロパティ (非デタッチ状態) を使用しています。この場合、元のスレッドは作成されたスレッドが終了するまで待機します。 pthread_join() 関数が返された場合にのみ、作成されたスレッドは終了し、占有していたシステム リソースを解放できます。デタッチされたスレッドの場合はそうではありません。他のスレッドによって待機されることはありません。自身の実行が完了すると、スレッドは終了し、システム リソースは直ちに解放されます。プログラマーは、ニーズに応じて適切な分離状態を選択する必要があります。スレッドのデタッチ状態を設定する関数は、pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate) です。 2 番目のパラメータは、PTHREAD_CREATE_DETACHED (デタッチされたスレッド) と PTHREAD _CREATE_JOINABLE (デタッチされていないスレッド) になります。ここで注意すべき点は、スレッドが別のスレッドとして設定され、このスレッドが非常に高速に実行されている場合、pthread_create 関数が戻る前に終了する可能性があるということです。終了した後、スレッド番号とシステム リソースが他のスレッドに転送されて使用される可能性があります。このようにして、pthread_create を呼び出すスレッドは間違ったスレッド番号を取得します。この状況を回避するには、特定の同期対策を講じることができます。最も簡単な方法の 1 つは、作成されたスレッドで pthread_cond_timewait 関数を呼び出して、スレッドをしばらく待機させ、pthread_create 関数が戻るのに十分な時間を残すことです。待機時間を設定することは、マルチスレッド プログラミングでよく使用される方法です。ただし、プロセス全体をスリープ状態にし、スレッド同期の問題を解決できない wait() などの関数は使用しないように注意してください。
よく使用されるもう 1 つの属性は、構造体 sched_pa​​ram に格納されるスレッド優先度です。保存するには、関数 pthread_attr_getschedparam と pthread_attr_setschedparam を使用します。一般的には、常に最初に優先順位を取得し、取得した値を変更してから、それを元に戻します。以下は簡単な例です。

/* 例.c */
#include <stdio.h>
#include <pthread.h>
空スレッド(void)
{
整数 i;
(i=0;i<3;i++) の場合
printf("これはpthreadです。/n");
}

int メイン(void)
{
pthread_t ID;
int i,ret;
ret = pthread_create(&id,NULL,(void *) スレッド,NULL);


if(ret!=0)

{
printf ("pthread 作成エラー!/n");
出口(1)
}
(i=0;i<3;i++) の場合
printf("これがメインプロセスです。/n");
pthread_join(id,NULL);
戻り値 (0);
}

4スレッドデータ処理

プロセスと比較した場合、スレッドの最大の利点の 1 つはデータの共有です。各プロセスは親プロセスから継承したデータ セグメントを共有し、データを簡単に取得および変更できます。しかし、これによってマルチスレッド プログラミングに多くの問題も生じます。複数の異なるプロセスが同じ変数にアクセスすることに注意する必要があります。多くの関数は再入可能ではありません。つまり、関数の複数のコピーを同時に実行することはできません (異なるデータ セグメントが使用されない限り)。関数内で宣言された静的変数は問題を引き起こすことが多く、関数の戻り値も問題を引き起こす可能性があります。関数内で静的に宣言された領域のアドレスが返される場合、スレッドが関数を呼び出してアドレスを取得し、そのアドレスが指すデータを使用すると、別のスレッドがこの関数を呼び出してこのデータを変更する可能性があるからです。プロセス内で共有される変数は、最適化中にコンパイラが変数の使用方法を変更するのを防ぐために、キーワード volatile を使用して定義する必要があります (gcc で -OX パラメータを使用するなど)。変数を保護するには、セマフォ、ミューテックス、その他の方法を使用して、変数が正しく使用されるようにする必要があります。次に、スレッドデータの処理に関する関連知識を徐々に紹介します。

4.1 スレッドデータ

シングルスレッド プログラムには、グローバル変数とローカル変数という 2 つの基本的なデータ タイプがあります。しかし、マルチスレッド プログラムには、スレッド データ (TSD: スレッド固有データ) という 3 番目のデータ型があります。これはグローバル変数と非常によく似ています。スレッド内では、各関数はグローバル変数のようにこれを呼び出すことができますが、スレッド外の他のスレッドからは見えません。このようなデータの必要性は明らかです。たとえば、共通変数 errno は標準エラー情報を返します。ほとんどすべての関数がこれを呼び出すことができるはずなので、これは明らかにローカル変数にはできません。しかし、これはグローバル変数にもできません。そうしないと、スレッド A で出力されるエラー メッセージがスレッド B のエラー メッセージと同じになる可能性が高くなります。このような変数を実装するには、スレッド データを使用する必要があります。各スレッドデータにキーを作成し、このキーに関連付けます。各スレッドでは、このキーを使用してスレッドデータを参照しますが、異なるスレッドでは、このキーで表されるデータは異なります。同じスレッドでは、同じデータ内容を表します。
スレッド データに関連する主な機能は、キーの作成、キーへのスレッド データの割り当て、キーからのスレッド データの読み取り、キーの削除の 4 つです。
キーを作成するための関数プロトタイプは次のとおりです。
外部 int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
最初のパラメータはキー値へのポインタであり、2 番目のパラメータはデストラクタ関数を指定します。このパラメータが空でない場合、システムは各スレッドの終了時にこのキーにバインドされたメモリ ブロックを解放するためにこの関数を呼び出します。この関数は、キーが 1 回だけ作成されるようにするために、関数 pthread_once ((pthread_once_t*once_control, void (*initroutine) (void))) と一緒に使用されることがよくあります。関数 pthread_once は初期化関数を宣言します。pthread_once が初めて呼び出されると、この関数が実行され、それ以降の呼び出しは無視されます。

次の例では、キーを作成し、それをいくつかのデータに関連付けます。グラフィック ウィンドウを定義する関数 createWindow を定義する必要があります (データ型は Fl_Window * で、これはグラフィカル インターフェイス開発ツール FLTK のデータ型です)。各スレッドがこの関数を呼び出すため、スレッド データを使用します。

/* 例.c */
#include <stdio.h>
#include <pthread.h>
空スレッド(void)
{
整数 i;
(i=0;i<3;i++) の場合
printf("これはpthreadです。/n");
}

int メイン(void)
{
pthread_t ID;
int i,ret;
ret = pthread_create(&id,NULL,(void *) スレッド,NULL);


if(ret!=0)

{
printf ("pthread 作成エラー!/n");
出口(1)
}
(i=0;i<3;i++) の場合
printf("これがメインプロセスです。/n");
pthread_join(id,NULL);
戻り値 (0);
}

このように、異なるスレッドで createMyWin 関数を呼び出すと、スレッド内で表示されるウィンドウ変数を取得できます。この変数は、pthread_getspecific 関数を通じて取得されます。上記の例では、関数 pthread_setspecific を使用してスレッド データをキーにバインドしています。これら 2 つの関数のプロトタイプは次のとおりです。
外部 int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
外部 void *pthread_getspecific __P ((pthread_key_t __key));
これら 2 つの関数のパラメータの意味と使用法は明らかです。 pthread_setspecific を使用してキーの新しいスレッド データを指定する場合は、スペースを再利用するために元のスレッド データを自分で解放する必要があることに注意してください。このプロセス関数 pthread_key_delete は、キーを削除するために使用されます。このキーが占有していたメモリは解放されますが、キーが占有していたメモリのみが解放され、キーに関連付けられたスレッド データが占有していたメモリ リソースは解放されず、関数 pthread_key_create で定義されたデストラクタ関数はトリガーされないことにも注意してください。キーを解放する前に、スレッド データの解放を完了する必要があります。

4.2 ミューテックスロック

ミューテックス ロックは、一度に 1 つのスレッドだけがコードを実行するようにするために使用されます。その必要性は明らかです。各スレッドが同じファイルにデータを順番に書き込むと、最終結果は悲惨なものになります。
まず次のコードを見てみましょう。これは共通バッファを使用する読み取り/書き込みプログラムであり、バッファには 1 つの情報しか保持できないと想定しています。つまり、バッファには情報ありと情報なしの 2 つの状態しかありません。

void リーダー関数 ( void );
void ライター関数 ( void );

char バッファ;
int buffer_has_item = 0;
pthread_mutex_t ミューテックス;
構造体timespec遅延;
void main ( void ) {
pthread_t リーダー;
/* 遅延時間を定義する */
遅延.tv_sec = 2;
遅延.tv_nec = 0;
/* デフォルトのプロパティでミューテックスオブジェクトを初期化します */
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
ライター関数();
}

void ライター関数 (void){
(1){
/* ミューテックスをロックする */
pthread_mutex_lock (&​​mutex);
(バッファにアイテムが0ある場合){
バッファ = make_new_item();
バッファにはアイテムが 1 つあります。
}
/* ミューテックスを開く */
pthread_mutex_unlock(&mutex);
pthread_delay_np(&遅延);
}
}

void リーダー関数(void){
(1){
pthread_mutex_lock(&mutex);
if(buffer_has_item==1){
バッファ内のアイテムを消費します。
バッファにアイテムがあります=0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&遅延);
}
}

ミューテックス変数 mutex はここで宣言されます。構造体 pthread_mutex_t は、システムによって割り当てられた属性オブジェクトを含むプライベート データ型です。関数 pthread_mutex_init は、ミューテックス ロックを生成するために使用されます。 NULL パラメータは、デフォルトのプロパティが使用されることを示します。特定の属性を持つミューテックスを宣言する必要がある場合は、関数 pthread_mutexattr_init を呼び出す必要があります。関数 pthread_mutexattr_setpshared および pthread_mutexattr_settype は、ミューテックス属性を設定するために使用されます。前の関数は、PTHREAD_PROCESS_PRIVATE と PTHREAD_PROCESS_SHARED の 2 つの値を持つプロパティ pshared を設定します。前者は異なるプロセス内のスレッドを同期するために使用され、後者は同じプロセス内の異なるスレッドを同期するために使用されます。上記の例では、デフォルトのプロパティ PTHREAD_PROCESS_PRIVATE を使用しました。後者は、ミューテックス ロック タイプを設定するために使用されます。オプションのタイプは、PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE、および PTHREAD _MUTEX_DEFAULT です。これらはそれぞれ異なるリストおよびロック解除メカニズムを定義します。通常は、最後のデフォルト属性が選択されます。
pthread_mutex_lock ステートメントはミューテックスのロックを開始し、pthread_mutex_unlock が呼び出されるまで後続のすべてのコードはロックされます。つまり、一度に 1 つのスレッドによってのみ呼び出され、実行されます。スレッドが pthread_mutex_lock を実行するときに、その時点でロックが別のスレッドによって使用されている場合、スレッドはブロックされます。つまり、プログラムは別のスレッドがミューテックス ロックを解放するまで待機します。上記の例では、スレッドが常にこの関数を占有するのを防ぐために、 pthread_delay_np 関数を使用してスレッドをしばらくスリープさせます。
上記の例は非常に単純なので、ここでは紹介しません。指摘する必要があるのは、ミューテックス ロックを使用するプロセスでデッドロックが発生する可能性が非常に高いということです。2 つのスレッドが同時に 2 つのリソースを占有し、対応するミューテックス ロックを異なる順序でロックしようとします。たとえば、2 つのスレッドがミューテックス 1 とミューテックス 2 をロックする必要があります。スレッド a が最初にミューテックス 1 をロックし、スレッド b が最初にミューテックス 2 をロックします。このとき、デッドロックが発生します。このとき、関数 pthread_mutex_lock の非ブロッキング バージョンである関数 pthread_mutex_trylock を使用できます。デッドロックが避けられないことが判明すると、対応する情報が返され、プログラマーはデッドロックに対して対応する処理を行うことができます。さらに、異なるミューテックス ロック タイプはデッドロックを異なる方法で処理しますが、最も重要なことは、プログラマー自身がプログラム設計においてこの点に注意を払う必要があるということです。

4.3 条件変数

前のセクションでは、ミューテックス ロックを使用してスレッド間のデータ共有と通信を実装する方法について説明しました。ミューテックス ロックの明らかな欠点は、ロック状態とロック解除状態の 2 つの状態しかないことです。条件変数は、スレッドをブロックして別のスレッドが信号を送信するのを待つことで、ミューテックス ロックの欠点を補います。ミューテックス ロックと一緒に使用されることがよくあります。条件変数は、スレッドをブロックするために使用されます。条件が満たされない場合、スレッドは対応するミューテックスのロックを解除し、条件が変化するのを待機することがよくあります。別のスレッドが条件変数を変更すると、対応する条件変数に通知され、この条件変数によってブロックされている 1 つ以上のスレッドが起動されます。これらのスレッドはミューテックスを再ロックし、条件が満たされているかどうかを再テストします。一般的に、条件変数はスレッドを同期するために使用されます。
条件変数の構造体は pthread_cond_t であり、関数 pthread_cond_init() を使用して条件変数を初期化します。そのプロトタイプは次のとおりです。
外部 int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
ここで、cond は pthread_cond_t 構造体へのポインターであり、cond_attr は pthread_condattr_t 構造体へのポインターです。 pthread_condattr_t 構造体は、条件変数の属性構造体です。ミューテックス ロックと同様に、これを使用して、条件変数をプロセス内で使用できるか、プロセス間で使用できるかを設定できます。デフォルト値は PTHREAD_PROCESS_PRIVATE です。これは、この条件変数が同じプロセス内の各スレッドによって使用されることを意味します。初期化された条件変数は、使用されていない場合にのみ再初期化または解放できることに注意してください。条件変数を解放する関数は、pthread_cond_destroy (pthread_cond_t cond) です。
関数 pthread_cond_wait() は、条件変数でスレッドをブロックします。関数のプロトタイプは次のとおりです。
外部 int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
スレッドは mutex が指すロックを解除し、条件変数 cond でブロックします。スレッドは関数 pthread_cond_signal と関数 pthread_cond_broadcast によって起動できますが、条件変数はスレッドをブロックして起動するだけであることに注意してください。変数が 0 かどうかなど、具体的な判断条件はユーザーが指定する必要があります。次の例からこれを確認できます。スレッドが起動された後、判定条件が満たされているかどうかが再確認されます。満たされていない場合、一般的には、スレッドはここでブロックされたまま、次回起動されるまで待機する必要があります。このプロセスは通常、while ステートメントを使用して実装されます。
スレッドをブロックするために使用される別の関数は pthread_cond_timedwait() です。そのプロトタイプは次のとおりです。
外部 int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex、__const構造体timespec *__abstime));
この関数には、pthread_cond_wait() 関数よりも 1 つ多い時間パラメータがあります。abstime 期間が経過すると、条件変数が満たされなくてもブロックが解除されます。
関数 pthread_cond_signal() のプロトタイプは次のとおりです。
外部 int pthread_cond_signal __P ((pthread_cond_t *__cond));
条件変数 cond でブロックされているスレッドを解放するために使用されます。この条件変数で複数のスレッドがブロックされている場合、どのスレッドが起動されるかはスレッド スケジューリング ポリシーによって決定されます。この関数は、条件変数を保護するミューテックス ロックによって保護される必要があることに注意してください。そうしないと、条件のテストと pthread_cond_wait 関数の呼び出しの間に条件満足信号が送信され、無制限に待機することになります。以下は、pthread_cond_wait() および pthread_cond_signal() 関数を使用する簡単な例です。

pthread_mutex_t count_lock;
pthread_cond_t カウントがゼロでない;
符号なしカウント;
減算カウント(){
pthread_mutex_lock (&​​count_lock);
while(カウント==0)
pthread_cond_wait(&count_nonzero, &count_lock);
カウント=カウント -1;
pthread_mutex_unlock (&​​count_lock);
}

増分カウント(){
pthread_mutex_lock(&count_lock);
カウント==0の場合
pthread_cond_signal(&count_nonzero);
カウント=カウント+1;
pthread_mutex_unlock(&count_lock);
}

カウント値が 0 の場合、減分関数は pthread_cond_wait でブロックされ、ミューテックス count_lock が開かれます。このとき、increment_count 関数が呼び出されると、pthread_cond_signal() 関数は条件変数を変更し、decrement_count() にブロックを停止するように指示します。読者は、2 つのスレッドでこれら 2 つの関数を別々に実行して、どのような結果が表示されるかを確認できます。
関数 pthread_cond_broadcast (pthread_cond_t *cond) は、条件変数 cond でブロックされているすべてのスレッドを起動するために使用されます。これらのスレッドは起動されると、対応するミューテックス ロックを再び競合するため、この関数は注意して使用する必要があります。

4.4 セマフォ

セマフォは、本質的には共通リソースへのアクセスを制御するために使用される非負の整数カウンターです。共通リソースが増加すると、関数 sem_post() が呼び出され、セマフォが増加します。パブリック リソースは、セマフォ値が 0 より大きい場合にのみ使用できます。使用後は、関数 sem_wait() によってセマフォが削減されます。関数 sem_trywait() は関数 pthread_mutex_trylock() と同じ機能を持ち、関数 sem_wait() の非ブロッキング バージョンです。以下では、セマフォに関連する関数を 1 つずつ紹介します。これらはすべて、ヘッダー ファイル /usr/include/semaphore.h で定義されています。
セマフォのデータ型は構造体 sem_t であり、基本的には長整数です。関数 sem_init() はセマフォを初期化するために使用されます。そのプロトタイプは次のとおりです。
外部 int sem_init __P ((sem_t *__sem、int __pshared、unsigned int __value));
sem はセマフォ構造体へのポインタです。pshared が 0 でない場合、セマフォはプロセス間で共有されます。それ以外の場合は、現在のプロセスのすべてのスレッドでのみ共有できます。value はセマフォの初期値を示します。
関数 sem_post( sem_t *sem ) は、セマフォの値を増やすために使用されます。このセマフォでスレッドがブロックされている場合、この関数を呼び出すと、スレッドの 1 つがブロック解除されます。選択メカニズムも、スレッド スケジューリング ポリシーによって決定されます。
関数 sem_wait( sem_t *sem ) は、セマフォ sem の値が 0 より大きくなるまで現在のスレッドをブロックするために使用されます。ブロックが解除されると、 sem の値は 1 減少し、使用後にパブリック リソースが減少したことを示します。関数 sem_trywait ( sem_t *sem ) は、関数 sem_wait() の非ブロッキング バージョンであり、セマフォ sem の値を 1 だけ直接減らします。
関数 sem_destroy(sem_t *sem) は、セマフォ sem を解放するために使用されます。
セマフォの使用例を見てみましょう。この例では、合計 4 つのスレッドがあり、そのうち 2 つはファイルから共通バッファーにデータを読み込む役割を担い、他の 2 つのスレッドはバッファーからデータを読み取り、異なる処理 (加算および乗算演算) を実行します。

/* ファイル sem.c */
#include <stdio.h>
#include <pthread.h>
#include <セマフォ.h>
#定義 MAXSTACK 100
整数スタック[MAXSTACK][2];
整数サイズ=0;
sem_t sem;
/* ファイル 1.dat からデータを読み取ります。データが読み取られるたびに、セマフォが 1 つ増加します */
void ReadData1(void){
ファイル *fp=fopen("1.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[サイズ][0],&stack[サイズ][1]);
sem_post(&sem);
++サイズ;
}
fp を閉じる
}
/*ファイル2.datからデータを読み込む*/
void ReadData2(void){
ファイル *fp=fopen("2.dat","r");
while(!feof(fp)){
fscanf(fp,"%d %d",&stack[サイズ][0],&stack[サイズ][1]);
sem_post(&sem);
++サイズ;
}
fp を閉じる
}
/*バッファ内のデータをブロックして待機します。データを読み取った後、スペースを解放して待機を続けます*/
void ハンドルデータ1(void){
(1){
sem_wait(&sem);
printf("プラス:%d+%d=%d/n",stack[サイズ][0],stack[サイズ][1],
スタック[サイズ][0]+スタック[サイズ][1]);
 - サイズ;
}
}

void ハンドルデータ2(void){
(1){
sem_wait(&sem);
printf("乗算:%d*%d=%d/n",スタック[サイズ][0],スタック[サイズ][1],
スタック[サイズ][0]*スタック[サイズ][1]);
 - サイズ;
}
}
int main(void){
スレッドt1、t2、t3、t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* プログラムが途中で終了するのを防ぎ、ここで無期限に待機させます */
pthread_join(t1,NULL);
}

Linux では、gcc -lpthread sem.c -o sem コマンドを使用して実行可能ファイル sem を生成します。 事前にデータファイル 1.dat と 2.dat を編集してあります。その内容がそれぞれ 1 2 3 4 5 6 7 8 9 10 と -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 であると仮定して、 sem を実行すると次の結果が得られます。

掛け算:-1*-2=2
プラス: -1+-2=-3
掛け算:9*10=90
プラス: -9+-10=-19
掛け算:-7*-8=56
プラス: -5+-6=-11
掛け算:-3*-4=12
プラス:9+10=19
プラス:7+8=15
プラス: 5+6=11

ここから、スレッド間の競合関係がわかります。サイズの値は各スレッドによって任意に変更されるため、値が当初意図した順序で表示されません。これは、マルチスレッド プログラミングで注意が必要な問題でもあります。

5 まとめ

マルチスレッド プログラミングは非常に興味深く、便利な技術です。マルチスレッド技術を使用する Network Ants は、最もよく使用されるダウンロード ツールの 1 つです。マルチスレッド技術を使用する Grep は、シングルスレッドの grep よりも数倍高速です。同様の例はたくさんあります。誰もがマルチスレッド技術を活用して、効率的で実用的なプログラムを作成できるようになることを願っています。

これは、Linuxの下でのマルチスレッドプログラミングの例の分析に関するこの記事の終わりです。

以下もご興味があるかもしれません:
  • Linuxマルチスレッドプログラミングクイックスタート
  • Linuxの下でのC言語のマルチスレッドプログラミング
  • Linuxでのマルチスレッドの詳細な説明と簡単な例
  • Linuxの下でのC \ C ++マルチプロセスおよびマルチスレッドプログラミングの例の詳細な説明
  • Linuxマルチスレッドプログラミングの詳細な説明(Linuxに限定されない)
  • Linuxマルチスレッドプログラミング(V)
  • Linuxマルチスレッドプログラミング(IV)
  • Linuxの下でのマルチスレッドプログラミング(パート3)
  • Linuxマルチスレッドプログラミング(パート2)
  • Linuxマルチスレッドプログラミング(i)
  • Linux マルチスレッド プログラミングの詳細なチュートリアル (スレッドはセマフォを使用して通信コードを実装します)

<<:  Vue el-date-picker 動的制限時間範囲ケースの詳細な説明

>>:  MySQL でのデータベース間クエリの例

推薦する

SQL文のパフォーマンスを分析するための標準的な要約

この記事では、explain を使用して SQL ステートメントを分析する方法を紹介します。実際、イ...

Vue+Springbootでインターフェースシグネチャを実装するためのサンプルコード

1. 実装のアイデアインターフェース署名の目的は、リクエストパラメータが改ざんされていないか、リクエ...

CSS を使用して同じ親タグの左側と右側に 2 つのボタンを配置する方法

この記事では、主に同じ親タグの左側と右側にある 2 つのボタンの CSS レイアウト方法を紹介し、皆...

Navicat を使用してリモート Linux MySQL データベースに接続するときに発生する 10061 不明エラーの詳細な説明

Navicat を使用してリモート Linux MySQL データベースに接続すると、不明なエラー ...

HTML フレーム、Iframe、フレームセットの違い

10.4.1 フレームセットとフレームの違い まず、フレームセットとフレームの違いについて説明します...

MySQL 無料インストール版 (zip) のインストールと設定の詳細なチュートリアル

この記事では、MySQL無料インストール版(zip)のインストールと設定のチュートリアルを参考までに...

携帯電話に GreasyFork js スクリプトをインストールするチュートリアル

目次序文1. Iceraven ブラウザ (Firefox) (Android) 2. (アンドロイ...

React+tsは二次リンク効果を実現します

この記事では、二次リンク効果を実現するためのReact+tsの具体的なコードを参考までに共有します。...

Mybatis ページングプラグイン pageHelper の詳細な説明と簡単な例

Mybatis ページングプラグイン pageHelper の詳細な説明と簡単な例動作フレームワーク...

ブラウザ内でHTMLタグを中央に配置するCSSスタイル

CSS スタイル:コードをコピーコードは次のとおりです。 <スタイル タイプ="te...

Vue でのスロット配置と使用状況分析

このチュートリアルの動作環境: Windows 7 システム、vue 2.9.6 バージョン、DEL...

Navicat が MySQL に接続するときに発生する 1045 エラーの解決方法

ローカル データベースに接続すると、Navicat for MySQL は以下のように 1045 エ...

VSCode の Remote-SSH を使用して Linux に接続し、リモート開発を行う

Remote-SSHをインストールして設定するまず VSCode を開き、拡張機能を見つけて、Rem...

CSS ペイント API: CSS のような描画ボード

1. Canvas画像をCSS背景画像として使用するCSS ペイント API は、Canvas キャ...

LinuxはRsync+Inotifyを使用してローカルとリモートのデータのリアルタイム同期を実現します。

0x0 テスト環境本社本番サーバーと支社バックアップサーバーはリモートデータバックアップが必要です...