マルチスレッドプログラミングの概要

マルチスレッドプログラミングの概要

1. プロセスとスレッドの概要

1.1.両者の歴史

元のオペレーティング システムにはプロセスやスレッドの概念がありませんでした。タスクが開始されると、そのタスクがマシン全体を制御します。タスクが完了するまでは、他のタスクを実行する方法はありませんでした。
タスク A が実行中に大量のデータ入力 (I/O 操作) を読み取る必要があるとします。このとき、CPU はタスク A がデータの読み取りを完了するまで静かに待つことしかできず、実行を続行できませんが、これは無駄です。 CPU リソース。

マルチタスク オペレーティング システムのサポート: マルチタスクの概念は、後のオペレーティング システムに追加されました。タスクとは、目的を達成するために 1 つ以上のプロセスによって実行される操作を指します。各プロセスは特定のメモリ アドレス空間に対応し、独自のメモリ空間のみを使用でき、プロセスは相互に干渉しません。これにより、プロセスの切り替えが可能になります。
プロセスが一時停止されると、オペレーティング システムは現在のプロセスの状態 (プロセス ID、プロセスが使用するリソースなど) を保存し、次回スイッチバックするときに、以前に保存された状態に基づいて復元します。状態になり、実行を続行します。
また、CPU 時間の割り当てはオペレーティング システムによって管理されます。オペレーティング システムは、現在の CPU ステータスとプロセスの優先順位に基づいて時間を割り当て、それによって各プロセスに適切な CPU 時間を確実に割り当てることができます。
アプリケーション開発者にとって、CPU 時間の割り当てにエネルギーを費やす必要はなくなり、プログラムが常に CPU を占有し、アプリケーションが達成したいビジネスだけに集中できると考えられます。実際、プロセスの実行中は常に CPU 時間を占有するわけではなく、オペレーティング システムはプロセス属性に基づいて他のプロセスに CPU 時間を割り当て、現在のプロセスの CPU 時間をプリエンプトします。

マルチスレッドをサポートするオペレーティング システム:
プロセスの出現後、オペレーティング システムのパフォーマンスは大幅に向上しましたが、人々はまだ満足しておらず、リアルタイム インタラクションに対する要求が徐々に高まっています。プロセスは一度に 1 つのことしか実行できないため、プロセスに複数のサブタスクがある場合、それらのサブタスクを 1 つずつしか実行できません。
例えば監視システムの場合、画像データを画面に表示するだけでなく、サーバーと通信して画像データを取得したり、人の対話操作を処理したりする必要があります。ある時点でシステムが画像データを取得するためにサーバーと通信しており、ユーザーが監視システム上のボタンをクリックした場合、システムはユーザーの操作を処理する前に画像データが取得されるまで待つ必要があります。画像データの取得に10秒ほど時間がかかると、ユーザーはただ待ち続けることになります。人々がそのようなシステムに満足できないのは明らかです。
そこでスレッドの概念が導入されました。プロセスには複数のスレッドが含まれます。プロセスはリソース割り当ての最小単位であり、スレッドは CPU スケジューリングの最小単位です。プロセス内のスレッドは、プロセス内のアドレスやファイル記述子などの情報を共有できます。CPU 時間は、各スレッドの属性に応じてオペレーティング システムによって割り当てられるため、各スレッドに適切な CPU 時間を割り当てることができるため、マルチスレッド システムでは、上記の監視システムの例を複数のスレッドで実装できます
。 1 つのスレッドはサーバーからデータを取得するために使用され、ユーザーの操作に応答するためにスレッドが使用されます。
もちろん、この例は、サーバーとの通信に 1 つのプロセスを使用し、ユーザー操作への応答に 1 つのプロセスを使用するマルチプロセス モードを使用して解決できます。

リアルタイム システムと非リアルタイム システム:
リアルタイム システムとは、計算の正確さがプログラムの論理的な正確さだけでなく、結果が生成される時間にも依存することを意味します。システムの制約が満たされていない場合、システム エラーが発生します。
汎用 Linux でのリアルタイム スケジューリング:
汎用 Linux では、スレッドの優先順位を設定することでリアルタイム スケジューリングを実現できます。詳細については、「スレッドの優先順位」を参照してください。ただし、汎用 Linux でのリアルタイム スケジューリングには
、 1. Linux システム スケジューリング単位は 10msであるため、正確な
タイミングを提供できない
2. プロセスがカーネル状態に入るためにシステム コールを呼び出すとき、プリエンプトできない
3. Linux カーネル実装では多数のマスクされた割り込み操作が使用されており、割り込みの損失が発生します

リアルタイム オペレーティング システムでのスケジューリング システム:
RTAI (Real-Time Application Interface) は、リアルタイム オペレーティング システムです。その基本的な考え方は、Linux システムでハード リアルタイム サポートを提供するために、マイクロカーネルを備えた小さなリアルタイム オペレーティング システム (RT-Linux のリアルタイム サブシステムとも呼ばれます) を実装し、通常の Linux システムをオペレーティング システム内で優先度の低いタスクとして実行されます。
また、Linux システムの一般的なタイミング精度は 10 ミリ秒、つまりクロック サイクルが 10 ミリ秒であり、RT-Linux は、システムのリアルタイム クロックを単一のトリガー状態に設定することで、十数マイクロ秒レベルのスケジューリング粒度を提供できます。

1.2. 開発中の選択肢

マルチスレッドとマルチプロセスの比較を複数の異なる次元で見てみましょう
次元 マルチプロセス マルチスレッドの概要
データ共有の同期 データ共有は複雑で IPC が必要です; データは分離されており、プロセスデータのため同期は簡単です共有されておりデータ共有が簡単ですが、だからこそ複雑な同期には利点もあります
メモリ
CPUが多くのメモリを占有し、切り替えが
複雑 CPU使用率が低い 占有メモリが少なく、切り替えが簡単CPU 使用率が高く、スレッドが優勢です。
作成、
破棄
、切り替え、作成、破棄、スイッチが複雑で遅いです。作成。破棄と切り替えが簡単で、速度が速いです。スレッドが優勢です。プログラミング: デバッグとプログラミングが簡単
です
。 、デバッグは単純です、プログラミングは複雑です、デバッグは複雑です。プロセスは優勢です。
信頼性。プロセスは相互に影響しません。1 つのスレッドがハングアップすると、プロセス全体がハングアップします。プロセスは優勢です。

1. 大量のリソース共有を必要とする優先スレッド
2. 頻繁に作成および破棄する必要がある優先スレッド

2. 簡単なマルチスレッドの例

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

void* Func(void* pParam);

int main()
{

int iData = 3;

pthread_t ThreadId;
pthread_create(&ThreadId, NULL, Func, &iData);

for(int i=0; i<3; i++)
{
	printf("this is main thread\n");
	sleep(1);
}

pthread_join(ThreadId, NULL);

return 1;

}

void* Func(void* pParam)
{

Fopen

int* pData = (int *)pParam;

for(int i=0; i<*pData; i++)
{
	printf("this is Func thread\n");
	sleep(2);
}
fclose
return NULL;	

}

次のようにコンパイルして実行します:
[root@localhost thread_linuxprj]# g++ -g -o thread_test thread_test.cpp –lpthread

[root@localhost thread_linuxprj]# ./thread_test
これはメインスレッドです
これは Func スレッドです
これはメインスレッド
です これは Func スレッドです
これはメインスレッドです
これは Func スレッドです

このプログラムにはメインスレッドと関数 Func を実行するスレッドの合計 2 つのスレッドがあり、出力結果を見ると、これら 2 つのスレッドが同時に出力操作を行っていることがわかります。

2.1.スレッドの起動

int pthread_create( pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);

パラメータの説明:
pthread_t *thread: 作成されたスレッドに対応する ID、スレッドの一意の識別子。
const pthread_attr_t *attr: スレッド属性。スレッドのスケジューリング モードと優先度、DETACH/JOIN モードを設定できます。通常は NULL に設定され、デフォルトの属性が使用されることを示します。
void *( start_routine) (void ): スレッドによって実行される関数ポインタ この関数には void 型パラメータが必要で、戻り値は void型でなければなりません。
void *arg: スレッドによって実行される関数のパラメータ。NULL に設定できます。

戻り値:
0 は成功を示し、-1 は失敗を示します。errno を使用して失敗の理由を取得できます。

2.2. スレッドの停止

2.2.1. スレッドが自動的に停止する

スレッド関数の実行が終了すると、スレッドは自動的に停止します。上記の例では、Fun 関数が戻った後、対応するスレッドが自動的に停止し、その戻り値は他のスレッドで取得できます。

pthread_exit 関数を明示的に呼び出してスレッドを停止することもできます。

通常の状況では、直接戻ってください。

2.2.2. 停止する外部通知スレッド

pthread_cancel 関数を使用して、スレッドに停止を通知するシグナルを送信できます。スレッドはシグナルを受信した後、独自の exit 属性に従って終了するか、他の操作を実行します。

宛先スレッドはデフォルトでこの操作に対して直接終了するため、通常は使用することはお勧めできません。これにより、スレッド内に適用されているリソースを解放できなくなり、リソース リークが発生します。スレッド自動終了モードを使用することをお勧めします。

ただし、実際のシナリオでは、次の例のように、あるスレッドが別のスレッドに終了を通知することが必要になることがよくあります。

void* Func(void* pParam)
{ while(true) { 関連するロジック処理を実行する}



int main()
{

pthread_t Thread1;
pthread_create(&Thread1, NULL, Func, NULL);

等待退出信号
请求子线程退出

メインスレッドはサブスレッド Thread1 を作成して終了を待ち、外部からの終了信号を受信するとサブスレッドに終了を通知します。

サブスレッドは開始された後、メインスレッドから終了信号を受信するまで独自の論理処理を実行し続けてから終了します。

pthread_cancel の使用は推奨されないため、次のモードを使用して解決できます

bool g_ThreadExitFlag = false;

void* Func(void* pParam)
{ while(!g_ThreadExitFlag) { 関連するロジック処理を実行する}



	清除本线程所申请的资源
return NULL;

int main()
{ pthread_t Thread1; pthread_create(&Thread1, NULL, Func, NULL);

等待退出信号

g_ThreadExitFlag = true;

pthread_join(Thread1, NULL);

メインスレッドと Thread1 の間では共有変数 g_ThreadExitFlag が使用され、Thread1 が実行するスレッド関数では、各論理ループが g_ThreadExitFlag が true かどうかを判定し、自身で申請したリソースをクリアしてリターンし、Thread1 スレッドは自動的に終了します。

2.2.3. pthread_join について

man 関数を実行しているメインスレッドが終了すると、このプロセス内の他のスレッドは、どこで実行されたかに関係なく、すぐに終了します。このように、サブスレッドのロジックが完成していないため、期待と矛盾する事態が発生します。
したがって、上記の例では、メイン スレッドが pthread_join 関数の呼び出しをブロックし、コンパイルおよび実行後の出力は次のようになります。
これはメイン スレッドです。
これは Func スレッドです。
これはメインスレッドです
。 これは Func スレッドです。
これはメイン スレッドです。

コード上では Func を 3 回出力する必要がありますが、実際の実行時は Fun が戻る前に man 関数が戻ったため、すべてのスレッドが終了しているため、Func は 2 回しか出力されません。

pthread_join函数原型如下:

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

pthread_t thread_id: リソースのリサイクルを待機しているスレッド ID
void **retval: 対応するスレッド関数の戻り値 注意しない場合は、NULL を設定しても問題ありません。

pthread_join は、thread_id スレッドの終了を待ち、該当スレッドのリソースを再利用するために使用され、該当スレッドが終了しない場合は常に待機状態になります。

スレッドを作成するとき、pthread_attr_t パラメーターを使用して、現在のスレッドが結合モードであるか分離モードであるかを指定できます。スレッドが結合モードの場合は、pthread_join 関数を使用してスレッド リソースをリサイクルする必要があります。スレッドの終了を意識する必要がない場合は、スレッドをデタッチ モードに設定できます。このとき、スレッド リソースをリサイクルするために pthread_join 関数を呼び出す必要はありません。当然のことながら、pthread_join はスレッドの待機を行うことができません。デタッチモード。

3. スレッド間の競合と同期

マルチスレッド プログラムを設計する場合、複数のスレッドが同時にメモリ領域の読み取りと書き込みを行うことがよくあります。これにより、マルチスレッドの競合が発生し、期待と矛盾する結果になります。以下の例:

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

int g_iData = 0;
void* Func1(void* pParam);
void* Func2(void* pParam);

int main()
{ pthread_t スレッド 1、スレッド 2; pthread_create(&Thread1, NULL, Func1, NULL); pthread_create(&Thread2, NULL, Func2, NULL);


pthread_join(Thread1, NULL);
pthread_join(Thread2, NULL);

return 1;

}

void* Func1(void* pParam)
{ for (int i=0; i<3; i++) { g_iData = 1;


    sleep(1);

    printf("Func1 print g_iData:%d\n", g_iData);
}

return NULL;	

}

void* Func2(void* pParam)
{

for (int i=0; i<3; i++)
{
    g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);
}

return NULL;		

}

メイン スレッドに加えて、Func1 と Func2 をそれぞれ実行する 2 つのワーカー スレッドがあり、Func1 は「Func1 print 1」を 3 回出力し、Fun2 は「Func2 print 2」を 3 回出力することが予想されます。
実際の実行結果は次のとおりです。
[root@localhost thread_linuxprj]# ./thread_test
Func2 print g_iData:1
Func1 print g_iData:
1
Func1
print g_iData:1 Func2 print g_iData:1 Func1 print g_iData:2
Func2 print g_iData:2

[root@localhost thread_linuxprj]# ./thread_test
Func2 print g_iData:1
Func1 print g_iData:1
Func1 print g_iData:1
Func2 print g_iData:1
Func1 print g_iData:2
Func2 print g_iData:2

[root@localhost thread_linuxprj]# ./thread_test
Func1 print g_iData:1
Func2 print g_iData:1
Func2 print g_iData:2
Func1 print g_iData:2
Func1 print g_iData:1
Func2 print g_iData:1

実行結果からわかるように、予期しない内容が多数印刷され、実行するたびに印刷内容が異なります。
これは、マルチスレッド プログラムの特性を反映しています。複数のスレッドがプロセス データを共有するため、同期がより複雑になり、スレッドのスケジューリングがオペレーティング システムに依存し、スケジューリングの順序が異なるため、問題が発生すると、外部パフォーマンスが低下します。プログラムも異なり、簡単です。 多くの不要な問題が発生し、位置決めに多くの困難を引き起こします。

したがって、プログラミングおよびコーディング時にマルチスレッドの同期を設計するように努める必要があります。

3.1. 関数が同じリソースにアクセスする必要がない場合は、関数を避けるようにしてください。

スレッドによって呼び出される関数がパブリック リソース (メモリ、ファイル記述子) にアクセスしない場合、マルチスレッドの競合の問題はまったく発生しません。したがって、マルチスレッドプログラムを作成する場合は、必要な場合以外は同じリソースにアクセスしないでください。

たとえば、上記のマルチスレッド競合の例では、2 つのスレッド関数が同じメモリ (グローバル変数 g_iData) にアクセスするため、アクセスされるものがグローバル変数ではなく、独自のスタック内の変数である場合、この問題は発生しません。
void* Func1(void* pParam)
{ for (int i=0; i<3; i++) { int iData = 1;


    sleep(1);

    printf("Func1 print g_iData:%d\n",  iData);
}

return NULL;	

}

void* Func2(void* pParam)
{

for (int i=0; i<3; i++)
{
    int iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n",  iData);
}

return NULL;		

}

ただし、マルチスレッド プログラミングが採用される理由は、一般に、複数のタスクが同じリソースにアクセスする必要があるためであり、同じリソースにマルチスレッドがアクセスすることは避けられない状況です。これは次の技術を使用して解決する必要があります。

3.2. ミューテックスロック

次のコードに示すように、g_mutex を使用すると、同時に 1 つのスレッドのみが同じリソースにアクセスするように制御できます。(ロックでスリープ操作を使用することは推奨されないことに注意してください。次の例では、マルチスレッドの競合をわかりやすく示すためにロックにスリープを追加するだけです)

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

int g_iData = 0;
void* Func1(void* pParam);
void* Func2(void* pParam);

pthread_mutex_t g_mutex;

int main()
{ pthread_mutex_init(&g_mutex, NULL);

pthread_t Thread1, Thread2;
pthread_create(&Thread1, NULL, Func1, NULL);
pthread_create(&Thread2, NULL, Func2, NULL);

pthread_join(Thread1, NULL);
pthread_join(Thread2, NULL);

pthread_mutex_destroy(&g_mutex);

return 1;

}

void* Func1(void* pParam)
{ for (int i=0; i<3; i++) { pthread_mutex_lock(&g_mutex);


    g_iData = 1;

    sleep(1);

    printf("Func1 print g_iData:%d\n", g_iData);

    pthread_mutex_unlock(&g_mutex);
}

return NULL;	

}

void* Func2(void* pParam)
{

for (int i=0; i<3; i++)
{
    pthread_mutex_lock(&g_mutex);

    g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);

    pthread_mutex_unlock(&g_mutex);
}

return NULL;		

}

スレッドが pthread_mutex_lock(&g_mutex) を実行して g_mutex を占有します。他のスレッドが pthread_mutex_lock(&g_mutex) コードを実行すると、待機状態になります。g_mutex を占有しているスレッドが pthread_mutex_unlock (&​​g_mutex) を呼び出して g_mutex を解放するまで、オペレーティング システムは、pthread_mutex_lock (&​​g_mutex) を実行してロックを占有し、その後の論理処理を実行できる待機中の他のスレッドからスレッドを割り当てます。待ちます。

ミューテックスを使用すると、メモリの一部はロックされませんが、操作がロックされます。

ロックは使用前に pthread_mutex_init 関数を使用して初期化する必要があり、ロックが使用されなくなった場合は、破棄するために pthread_mutex_destroy を呼び出す必要があります。

3.2.1. 注意事項

ロックした後は必ずロックを解除することを忘れないでください。この概念はわかりやすいですが、使用中に問題が発生することがよくあります。ロックを使用するたびに、 void Func() { pthread_mutex_lock(&g_mutex) if(
など、ロックが解除されている箇所をすべて確認する必要があります。…) { pthread_mutex_unlock (&​​g_mutex) return; } else if(…) { If(…) { pthread_mutex_lock(&g_mutex) XXX を呼び出すpthread_mutex_unlock (&​​g_mutex) return } else { if(…) { If() { //ここで終了なし ロック解除中デッドロックを引き起こします! return; } … … pthread_mutex_unlock (&​​g_mutex) return; } } pthread_mutex_unlock (&​​g_mutex)
































戻る;
}

pthread_mutex_unlock (&​​g_mutex)
}

ロック内で長時間の操作を行わないようにしてください。
ロックの範囲内で長時間のアルゴリズムが実行されると、スレッドがこの操作を実行すると、このロックを占有する必要がある他のスレッドがロック状態になります。長時間状態: 時間のかかる操作が完了するまで待機状態になると、プログラムの効率が大幅に低下します。

一般的な時間のかかる操作:
CPU で長時間を消費するアルゴリズム、IO での読み取りおよび書き込み操作、およびスリープ。

特定の書き込み状況で、複数のスレッドが同じ IO の読み取りと書き込みを行う場合 (たとえば、複数のスレッドが同じファイルを操作する場合)、現時点では IO をロックすることはお勧めできませんが、プログラム設計を A スレッドのみに変更する必要があります。 IO上で動作します。

たとえば、スレッド A とスレッド B が同時にファイルに書き込みたい場合、スレッド A とスレッド B が同時にメモリに書き込み、リフレッシュするために新しいスレッド C が追加されるように変更できます。このメモリ内のデータをファイルに書き込みます。この方法では、1 つのスレッドのみがファイルにアクセスし、IO へのマルチスレッド アクセスを必要とせず、共有メモリをロックするだけで済みます。

ロック範囲内で別のロックをロックしないようにしてください。
ロック A のロック範囲内で、ロック B をロックします。このように、ロック A のロックを解除したい場合は、現在のスレッドがロックをロックするまで待つ必要があります。B はロックされています。成功した場合、つまり、ロック A はロック B に依存します。
このようなコードは循環依存関係が発生しやすく、あるコードではロック A がロック B に依存し、別のコードではロック B がロック A に依存します。このように、2 つのスレッドがこれら 2 つのコードを実行すると、デッドロックが発生します。例えば:

void Func1(void* pParam)
{ pthread_mutex_lock(&g_mutexA);

    … …
    pthread_mutex_lock(&g_mutexB);

    … …
		
		pthread_mutex_unlock(&g_mutexB);

   pthread_mutex_unlock(&g_mutexA);

}

void Func2(void* pParam)
{

    pthread_mutex_lock(&g_mutexB);

    … …
    pthread_mutex_lock(&g_mutexA);

    … …
		
		pthread_mutex_unlock(&g_mutexA);

   pthread_mutex_unlock(&g_mutexB);

}

スレッド 1 が Func1 で pthread_mutex_lock(&g_mutexB); を実行すると、スレッド 2 は pthread_mutex_lock(&g_mutexA); を実行しますが、このとき、スレッド 2 はすでに pthread_mutex_lock(&g_mutexB); 操作を実行しているため、スレッド 1 は g_mutexB をロックするのを待機しています。 、スレッド 1 が g_mutexA をロックしているため、スレッド 2 は g_mutexA の待機状態になります。
このように、2 つのスレッドが常に待機することになり、デッドロックが発生します。

ロック範囲は
できるだけ小さくする必要があり、ロック範囲が大きいほど上記の問題が発生する可能性が高く、その後のコード更新やメンテナンスに支障をきたすため、ロック範囲はできるだけ小さくする必要があります。

自動ロックを使用してデッドロックを回避します。C
++ を使用してオペレーティング システム API をカプセル化し、自動ロック モードを実装できます。自動ロックのライフ サイクル中、コードは自動的にロックされ、自動ロックのライフ サイクルが終了すると、自動的にロックが解除されます。ほとんどのデッドロックの問題。上記の例は、次のように自動ロックを使用して実装されます。

CLock_CS lock_cs;
void Func()
{ AUTO_CRITICAL_SECTION(lock_cs) if(…) { //自動ロックのライフサイクルが終了し、自動的にロックが解除されます。return; } else if(…) { If(…) { //自動ロックのライフサイクルが終了し、自動的にロックが解除されます。return } else { if(...) { If() { //自動ロックのライフサイクルが終了し、自動的にロックが解除されます。Return; } … … //自動ロックのライフサイクルが終了し、自動的にロックが解除されます。return; } } //自動ロックのライフサイクルが終了し、自動的にロックが解除されます。戻ります; }





























//自動ロックのライフサイクルが終了し、自動的にロックが解除されます。
自動ロックの実装については、
次の svn コードを参照してください
https://192.168.20.6:8443/svn/hnc8/trunk/apidev/net/comm/src/criticalsection.h

読み取り/書き込みロックを使用して効率を向上させる
適用シナリオ: 複数のスレッドが読み取りおよび書き込み操作を実行します。書き込み時は片方のみ書き込み可能で、他の読み書き動作は待機状態となります。読み込みの際、公開リソースに変更を加えないため、同時に読み込むことができ効率が向上します。

ただし、ミューテックス ロックが使用されている場合、1 つのスレッドがロックを占有すると、他のスレッドはそのロックを占有することができず、ロック待ち状態になります。したがって、同時読み取りの要求に応えることができない。

これは、読み取り/書き込みロックを使用して実現できます。

int pthread_rwlock_init(pthread_rwlock_t *rwlock を制限,
const pthread_rwlockattr_t *属性を制限);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

3.3.条件変数

ミューテックス ロックを使用すると、同時に 1 つのスレッドだけがロック範囲内のコードを実行できることが保証されますが、各スレッドの実行順序は保証されません。開発で、特定のスレッドが実行された後にのみ後続のスレッドが実行できるようにする必要がある場合は、条件変数を使用することをお勧めします。
次の例のように、スレッド 1、2、3 が同時に開始され、スレッド 1 の実行が完了すると、スレッド 2 または 3 のいずれかが後続の操作のために起動されます。

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

int g_iData = 0;
bool g_bNewThreadRun = false;
void* Func1(void* pParam);
void* Func2(void* pParam);
void* Func3(void* pParam);

pthread_mutex_t g_mutex;
pthread_cond_t g_cond;

int main()
{ pthread_mutex_init(&g_mutex, NULL); pthread_cond_init(&g_cond, NULL);

pthread_t Thread1, Thread2, Thread3;
pthread_create(&Thread1, NULL, Func1, NULL);
pthread_create(&Thread2, NULL, Func2, NULL);
 //pthread_create(&Thread3, NULL, Func3, NULL);

pthread_join(Thread1, NULL);
pthread_join(Thread2, NULL);
 pthread_join(Thread3, NULL);

pthread_cond_destroy(&g_cond);
pthread_mutex_destroy(&g_mutex);

return 1;

}

void* Func1(void* pParam)
{

for (int i=0; i<3; i++)
{

    g_iData = 1;

    sleep(1);

    printf("Func1 print g_iData:%d\n", g_iData);   
}

g_bNewThreadRun = true;
 pthread_cond_signal(&g_cond);


return NULL;	

}

void* Func2(void* pParam)
{ pthread_mutex_lock(&g_mutex); while(!g_bNewThreadRun) { pthread_cond_wait(&g_cond, &g_mutex); g_bNewThreadRun = false ; for (int i=0; i<3; i++) { g_iData = 2;








    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);

}

pthread_mutex_unlock(&g_mutex);
return NULL;		

}

void* Func2(void* pParam)
{ pthread_mutex_lock(&g_mutex); while(!g_ bNewThreadRun) { pthread_cond_wait(&g_cond, &g_mutex); }




    g_bNewThreadRun = false;

for (int i=0; i<3; i++)
{
    g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);
}

pthread_mutex_unlock(&g_mutex);

return NULL;		

}

スレッド 1、2、および 3 が開始されます。スレッド 1 はロックがないためすぐに実行され、g_bNewThreadRun=true を設定し、条件変数 pthread_cond_signal(&g_cond) をトリガーします。スレッド 2 は最初に pthread_mutex_lock(&g_mutex); ロック操作を実行します
。次に、pthread_cond_wait(&g_cond, &g_mutex) 関数を呼び出します。この関数を入力すると、まずロックが解除され、次に g_cond が有効になるまで待機します。
同様に、スレッド 3 は pthread_mutex_lock(&g_mutex) を実行してから pthread_cond_wait(&g_cond, &g_mutex) 関数を呼び出し、g_cond が有効になるのを待ちます。
スレッド 1 が条件変数 g_cond をトリガーすると、スレッド 2 または 3 のいずれかが起動されます。pthread_cond_wait(&g_cond, &g_mutex) 関数は戻り、g_mutex は戻る前にロックされ、1 つのスレッドのみが後続の操作を実行できるようになります。実行が完了したら、pthread_mutex_unlock(&g_mutex) を呼び出してロックを解除します。

3.3.1. 注意事項


設定条件が有効な場合、スレッドの実行順序はオペレーティングシステムによってスケジュールされるため、pthread_cond_signal をトリガーする前である必要があります。上記の例では、スレッド 1 が pthread_cond_signal を呼び出した後、g_ bNewThreadRun を true に設定するとこの 2 つのステップの後、スレッド 1 は g_ bNewThreadRun を true に設定します。この間にスレッド 2 または 3 の pthread_cond_wait が目覚めました。その結果、g_ bNewThreadRun がまだ false ではないと判断され、pthread_cond_wait が呼び出され続けました。待機状態になり、条件変数の有効性信号が失われます。

 pthread_cond_wait を呼び出す前に、受信ロックをロックする必要があります。
上記の例によれば、pthread_cond_wait 関数に入ると、システムは最初に受信ロックを解除します。条件変数が有効になるのを待った後、受信ロックをロックしてから戻ります。これにより、後続のロックのみが保証されます。 1 つのスレッドがパブリック リソースに対する操作を実行できます。
したがって、pthread_cond_wait を呼び出す前に、受信ロックをロックする必要があります。ロックがない場合、実行結果は予測できません。


p​​thread_cond_wait を呼び出す場合、スレッドの実行順序は OS によってスケジュールされているため、保護のために while ループ判定を使用する必要があります。上記の例では、スレッド 2 が pthread_cond_wait を実行する前に、スレッド 1 が条件変数シグナルをトリガーしている可能性があります時間が経過すると、条件変数有効信号が失われ、スレッド 2 は常に pthread_cond_wait 状態になります。したがって、通常は、pthread_cond_wait を呼び出す前に、現在の状態ですぐに実行できるかどうかを判断する必要があります。
if(!g_ bNewThreadRun)
{ pthread_cond_wait(&g_cond, &g_mutex); } ...同時に、システムの中断により pthread_cond_wait が戻る可能性があり、この時点では条件変数シグナルが有効になっていない可能性があります。したがって、while ループを使用して、現在の状態で実行できるかどうかを判断する必要があります。while(!g_ bNewThreadRun) { pthread_cond_wait(&g_cond, &g_mutex); } … … 条件変数は繰り返しトリガーされ、pthread_cond_wait は 1 回しか取得できません。上記の説明によれば、あるスレッドが pthread_cond_wait を実行する前に、別のスレッドが条件変数シグナルを有効に設定すると、シグナルは失われます。











したがって、スレッド 1 の pthread_cond_signal が同時に複数回トリガーされた場合、スレッド 2 の pthread_cond_wait は 1 回だけウェイクアップする可能性があります。これは、残りの pthread_cond_signal がトリガーされたときに、スレッド 2 が後続の操作を実行している可能性があり、pthread_cond_wait 状態にないためです。
シグナルを 1 回トリガーし、ビジネス処理スレッドを 1 回実行する必要がある場合は、セマフォ モードを使用することをお勧めします。

3.4.信号量

次のコード例に示すように、セマフォを使用して、典型的なプロデューサー/コンシューマー パターンを実装できます。

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

std::list を使用します。

pthread_mutex_t リスト_ミューテックス;
リストg_PdtList;

sem_t g_HavePdtSem;

bool g_bExit = false;

void PutPdt(int iPdt)
{ pthread_mutex_lock(&List_Mutex);

g_PdtList.push_back(iPdt);

pthread_mutex_unlock(&List_Mutex);

}

void GetPdt(int& iPdt)
{ pthread_mutex_lock(&List_Mutex);

iPdt = g_PdtList.front();
g_PdtList.pop_front();

pthread_mutex_unlock(&List_Mutex);

}

void* ProducterFunc(void* pParam);
void* ConsumerFunc(void* pParam);

int main()
{ pthread_mutex_init(&List_Mutex, NULL); sem_init(&g_HavePdtSem, 0, 0);

const int MAX_CONSUMER_NUM = 3;

pthread_t ProductThread;
pthread_t ConsumerThread[MAX_CONSUMER_NUM];

pthread_create(&ProductThread, NULL, ProducterFunc, NULL);

for (int i=0; i<MAX_CONSUMER_NUM; i++)
{
    pthread_create(&ConsumerThread[i], NULL, ConsumerFunc, NULL);
}

sleep(3);
g_bExit = true;


pthread_join(ProductThread, NULL);

for (int i=0; i<MAX_CONSUMER_NUM; i++)
{
    pthread_join(ConsumerThread[i], NULL);
}


sem_destroy(&g_HavePdtSem);
pthread_mutex_destroy(&List_Mutex);

return 1;

}

void* ProducterFunc(void* pParam)
{ for (int i=0; i<5; i++) { printf(“Producter make pdt:%d\n”, i); PutPdt(i);



    sem_post(&g_HavePdtSem);
}

return NULL;	

}

void* ConsumerFunc(void* pParam)
{ static int i = 0; int iIndex = i++;

while(!g_bExit)
{
    timespec abstime;
    clock_gettime(CLOCK_REALTIME, &abstime);

    abstime.tv_sec += 3;
    if (sem_timedwait(&g_HavePdtSem, &abstime) == -1)
    {
        if (errno == ETIMEDOUT)
        {
            continue;
        }
    }

    int iPdt = -1;
    GetPdt(iPdt);

    printf("Consumer[%d] get pdt:%d\n", iIndex, iPdt);
}

return NULL;		

}

実行結果の例:
[root@localhost sem_linux_test]# ./thread_test
Producter make pdt:0
Producter make pdt:1
Producter make pdt:2
Consumer[2] get pdt:0
Consumer[0] get pdt:1
Consumer[1] get pdt:2
Producter make pdt:3
Producter make pdt:4
Consumer[0] get pdt:3
Consumer[2] get pdt:4

main 関数が開始されると、sem_init(&g_HavePdtSem, 0, 0); を呼び出してセマフォを初期化します。
その後、main 関数は Product スレッドと 3 Consumer スレッドを作成し、そのうち Product は合計 6 つのプロダクトを生成します。プロダクトが作成されるたびに sem_post(&g_HavePdtSem) が呼び出され、g_HavePdtSem セマフォの値は +1 になります。
各 Consumer スレッドが正常に作成されたら、sem_timedwait(&g_HavePdtSem, &abstime) を呼び出します。これは、セマフォの初期化時に設定された初期値 0 (sem_init の 3 番目のパラメーターは 0) がセマフォが有効になるのを待機しているためです。有効なセマフォを取得した後、sem_timedwait は戻り、g_HavePdtSem セマフォの値を -1 に設定します。セマフォの値が 0 になるまで。

4. スレッドの優先順位

线程属性中包含有线程的调度策略和线程的优先级的信息,在创建线程时可以通过线程的属性设置线程的优先级。不过一般情况下多线程设计很少涉及到线程优先级的修改。
当线程属性中调度策略有如下三个类型。
	SCHED_FIFO:实时调度,先进先出,线程启动后一直占用CPU运行,一直到有比此线程优先级更高的线程处于就绪态才释放CPU
	SCHED_RR:实时调度,时间片轮转算法,当线程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。
	SCHED_OTHER:分时调度,默认算法。

設定されたスレッド優先度は、スレッドのスケジューリング ポリシーが SCHED_FIFO および SCHED_RR の場合にのみ有効です。

次の関数を使用して、スレッドのスケジューリング アルゴリズムと優先度を変更できます。
int pthread_attr_setschedparam(pthread_attr_t *attr,
const struct sched_pa​​ram *param);

int pthread_attr_setschedpolicy(pthread_attr_t *attr,
int ポリシー);

5.LinuxとWinの違い

上記はすべて Linux でのスレッド操作です。Win でのスレッド操作 API は Linux とは異なります。スレッドの
開始と終了の待機
uintptr_t _beginthreadex( void *security,
unsigned stack_size,
unsigned ( *start_address )( void * ),
void *arglist,
符号なし initflag、
符号なし *thrdaddr );

DWORD WINAPI WaitForSingleObject(HANDLE hHandle、
DWORD dwミリ秒)

相互斥锁
HANDLE WINAPI CreateMutex(
__in LPSECURITY_ATTRIBUTES lpMutexAttributes,
__in BOOL bInitialOwner,
__in LPCTSTR lpName
);

BOOL WINAPI ReleaseMutex(
__in HANDLE hMutex
);

Win 環境では、一般にロックではなくクリティカル セクションを使用することが推奨されており、クリティカル セクションの呼び出し効率はロックよりもはるかに高くなります。

临界区
void WINAPI InitializeCriticalSection(
__out LPCRITICAL_SECTION lpCriticalSection
);
void WINAPI DeleteCriticalSection(
__in_out LPCRITICAL_SECTION lpCriticalSection
);

void WINAPI EnterCriticalSection(
__in_out LPCRITICAL_SECTION lpCriticalSection
);

void WINAPI LeaveCriticalSection(
__in_out LPCRITICAL_SECTION lpCriticalSection
);

同期イベント
BOOL WINAPI SetEvent(
__in HANDLE hEvent
);

DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

信号量
BOOL WINAPI ReleaseSemaphore(
__in HANDLE hSemaphore,
__in LONG lReleaseCount,
__out LPLONG lpPreviousCount
);
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

6. 利用可能なコードベース

Windows と Linux でのマルチスレッド操作用の API は異なるため、Win と Linux の両方をサポートするコードで使用する場合、多くの場合、あらゆる場所にコンパイル マクロを追加する必要があり、コードのメンテナンスには役立ちません。
したがって、カプセル化されたコードとダイナミック ライブラリ (オープン ソース コード、著作権の問題なし) を使用して、コンパイル マクロを追加せずに上位層の処理を容易にすることができます。

おすすめ

転載: blog.csdn.net/p309654858/article/details/132145206