この記事は合計 4505 ワードあり、推定読了時間は 8 分です。
目次
クリティカルセクションの概念
前の例では、タスクを処理するスレッドを 1 つだけ作成しようとしましたが、次に複数のスレッドを作成してみます。
ただし、最初に「クリティカルゾーン」という概念を拡張する必要があります。
クリティカル セクションとは、共有リソース (共有デバイスや共有メモリなど) にアクセスするプログラム フラグメントを指します。これらの共有リソースには、複数のスレッドが同時にアクセスすることはできません。スレッドがクリティカル セクションに入ると、他のスレッドまたはプロセスは待機する必要があります (例: 制限付き待機待機メソッド)。これらの共有リソースが確実に使用されるように、クリティカル セクションの入り口と出口でいくつかの同期メカニズムを実装する必要があります。は、たとえばsemaphoreを使用して取得されます。
複数のスレッドが同時に実行されると問題が発生しやすくなりますが、この問題は共有リソースへのアクセスに焦点を当てています。
関数の実行時にクリティカル セクションで問題が発生するかどうかに応じて、関数の種類は次の 2 つのカテゴリに分類できます。
- スレッドセーフ機能
- スレッドアンセーフ機能
スレッドセーフ関数は、複数のスレッドによって同時に呼び出されても問題を引き起こしませんが、非
拡大する:
Linux であっても、Windwos であっても、開発者は非スレッド セーフ関数を設計するときに、同じ関数を持つスレッド セーフ関数も設計するため、スレッド セーフ関数と非スレッド セーフ関数を区別する必要はありません。
スレッドセーフな関数の名前は、 function の中に接尾辞 _r を付けるのが一般的ですが、プログラミングでこのように関数式を書くと非常に面倒になるため、ヘッダー宣言の前に定義することができます。ファイル。_REENTRANTマクロ。
より高速なコード作成エクスペリエンスを追求している場合は、コードを作成するときに_REENTRANTマクロを参照する代わりに、入力パラメーターをコンパイルするときに-D_REENTRANT を追加できます。
マルチスレッドタスクをシミュレートする
次に、この問題を反映するシナリオをシミュレートしてみましょう。サンプル コードは次のとおりです。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_NUM 100
void *thread_inc(void *arg);
void *thread_des(void *arg);
long num = 0;
int main(int argc, char *argv[])
{
pthread_t thread_id[THREAD_NUM];
int i;
for (i = 0; i < THREAD_NUM; i++)
{
if (i % 2)
pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
else
pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
}
for (i = 0; i < THREAD_NUM; i++)
pthread_join(thread_id[i], NULL);
printf("result: %ld \n", num);
return 0;
}
void *thread_inc(void *arg)
{
for (int i = 0; i < 100000; i++)
num += 1;
return NULL;
}
void *thread_des(void *arg)
{
int i;
for (int i = 0; i < 100000; i++)
num -= 1;
return NULL;
}
操作結果:
明らかに、結果は実際に必要な「0」値ではなく、出力値は時間の経過とともに変化します。
では、なぜそのような非現実的な値が現れるのでしょうか?
以下のような状況があります。
スレッド A が変数 λ=98 へのアクセスを開始すると、スレッド B もアクセスを開始するため、この時点でスレッド A とスレッド B の両方が λ=98 の値を取得します。スレッドはその値を+1計算した結果、99を取得してリソース変数への更新要求を開始しましたが、このときスレッドBも同様の操作を行い、同じく先ほど取得した値λ=98をもとに、最終結果は、A が λ=99 を計算し、B も λ=99 を計算し、最後に更新された値も 99 でしたが、実際には 100 になるはずです。
まとめると、この種の問題が発生する原因は、同じリソースに同時にアクセスして処理する際に「時間差」が生じ、最終的な結果が実際の状況と乖離してしまうことにあります。
理由を理解すると、この問題は簡単に解決できます。つまり、同時にアクセスされるリソースの読み取りおよび書き込み権限を制限し、スレッドを同期します。
スレッドの同期
スレッド同期の場合、「ミューテックス」と「セマフォ」という 2 つの概念定義 に依存する必要があります。
ミューテックス
Mutex は、複数のスレッドによる同時アクセスを制限するために使用され、主にスレッド同期アクセスの問題を解決する「ロック」メカニズムです。
ミューテックスには、pthread.h ライブラリに作成と破棄のための特別な関数もあります。その関数構造を見てみましょう。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t * mutex , const pthread_mutexattr_t * attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);
//成功时返回 0 ,失败时返回其他值。
/* 参数定义
mutex: 创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值。
attr: 传递即将创建的互斥量属性,没有需要指定的属性时可以传递NULL。
さらに、特定のミューテックス属性を構成する必要がない場合は、PTHREAD_MUTEX_INITIALIZERマクロを使用して初期化できます。例は次のとおりです。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
ただし、初期化には pthread_mutex_init 関数を使用するのが最善です。マクロのデバッグ時にエラー ポイントを特定するのが難しく、pthread_mutex_init のミューテックス属性の設定もより直感的で制御しやすいためです。
ミューテックスのロックとロック解除
上記の 2 つの関数は作成と破棄にのみ使用されますが、最も重要なものはロックとロック解除の 2 つの操作関数であり、その構造は次のとおりです。
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);
// 成功时返回 0 ,失败时返回其他值 。
クリティカル セクションに入る前に呼び出す必要がある関数はpthread_mutex_lockです。この関数を呼び出すときに他のスレッドがクリティカル セクションに入ったことが判明した場合、内部のスレッドがpthreaed_mutex_unlockを呼び出さない限り、この時点ではpthread_mutex_lock関数は値を返しません。クリティカルセクションを終了する機能。
クリティカルセクションの一般的な構造設計は次のとおりです。
pthread_mutex_lock(&mutex);
//临界区的开始
//..........
//..........
//临界区的结束
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock()とpthread_mutex_lock() は一般にペアの関係にある ことに特に注意してください。スレッドがクリティカル セクションを抜けた後にロックが解放されないと、クリティカル セクションに入るのを待っている他のスレッドはロックを解除できなくなります。ブロッキング状態が解消され、最終的には「デッドロック」状態になります。
次に、ミューテックスを使用して前の問題を解決してみましょう。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_NUM 100
void *thread_inc(void *arg);
void *thread_des(void *arg);
long num = 0;
pthread_mutex_t mutex;
int main(int argc, char *argv[])
{
pthread_t thread_id[THREAD_NUM];
int i;
pthread_mutex_init(&mutex, NULL);
for (i = 0; i < THREAD_NUM; i++)
{
if (i % 2)
pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
else
pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
}
for (i = 0; i < THREAD_NUM; i++)
pthread_join(thread_id[i], NULL);
printf("result: %ld \n", num);
pthread_mutex_destroy(&mutex);
return 0;
}
void *thread_inc(void *arg)
{
pthread_mutex_lock(&mutex);
// ↓临界区代码执行块
for (int i = 0; i < 100000; i++)
num += 1;
// ↑临界区代码执行块
pthread_mutex_unlock(&mutex);
return NULL;
}
void *thread_des(void *arg)
{
pthread_mutex_lock(&mutex);
for (int i = 0; i < 100000; i++)
{
// ↓临界区代码执行块
num -= 1;
// ↑临界区代码执行块
}
pthread_mutex_unlock(&mutex);
return NULL;
}
操作結果:
結果は最終的に正解でした~
特に注意が必要なのは、ロック領域を設計するときに、全員が境界を慎重に考慮し、正確に「ロック」する必要があるコード実行ポイントと終了できる「リリース」ポイントを確認する必要があることです。これにより、「」への頻繁な呼び出しを回避できます。 lock". " および "unlock" 操作を実行できるため、オペレーティング システムのコード実行効率が向上します。
信号量
セマフォはミューテックスに似ており、スレッド同期の方法でもあります。一般にセマフォは 2 進数の 0 と 1 で表されるため、この種のセマフォは「バイナリー セマフォ」とも呼ばれます。
セマフォの作成方法と破棄方法は次のとおりです。
#include <semaphore.h>
int sem_init(sem_t * sem , int pshared, unsigned int value);
int sem_destroy(sem_ t * sem);
//成功时返回0,失败时返回其他值
/* 参数含义
sem: 创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值。
pshared: 传递0时,创建只允许1个进程内部使用的信号量。传递其他值时,创建可由多个进程共享的信号量。
value: 指定新创建的信号量初始值。
*/
ミューテックスと同様に、「ロック」機能と「ロック解除」機能があります。
#include <semaphore.h>
int sem_post(sem_ t * sem);
int sem_wait(sem_t * sem);
//成功时返回0,失败时返回其他值。
/* 参数含义
sem: 传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait信号量减1。
*/
sem_init関数 を呼び出すと、オペレーティング システムはセマフォ オブジェクトを作成し、セマフォ値を初期化します。sem_post 関数が呼び出された場合、値は +1 になり、sem_wait 関数が呼び出された場合は -1 になります。
スレッドが sem_wait 関数を呼び出してセマフォの値を 0 にすると、そのスレッドはブロッキング状態になりますが、このときに他のスレッドが sem_post 関数を呼び出すと、以前にブロックされていたスレッドはブロッキング状態を抜け出して、ブロッキング状態に入ることができます。クリティカルセクション。
セマフォのクリティカル セクション構造は一般に次のとおりです (セマフォの初期値が 1 であると仮定します)。
sem_wait(&sem); //进入到临界区后信号量为0
// 临界区的开始
// ..........
// ..........
// 临界区的结束
sem_post(&sem); // 信号量变为1
セマフォは通常、スレッド タスクで強い順序を持つ同期の問題を解決するために使用されます。