マルチスレッドなどの並列処理を行う際に必要になるのが排他制御などの同期です。
マルチスレッドは処理を並列実行することで高速化を行なったり、別々の処理を同時に実行したりする上では非常に有用ですが、排他制御を行わないと結果が意図したものにならない場合があります。
排他制御と聞くと難しそうなイメージを持つかもしれませんが、今回紹介する Mutex は排他制御の中では最も簡単なものです。
並列プログラミングを行うのであれば是非マスターしておきましょう!
このページのプログラムで使用する Mutex は POSIX 仕様の pthread_mutex
になります
pthread_mutex
が利用できない環境の場合(特に Windows)は、そのままプログラムが利用できないので注意してください
ただし考え方はどの環境を利用している方でも参考になるはずです!
Contents
排他制御とは
マルチスレッドプログラミングにおける排他制御とは「複数のスレッド間で共有するデータへの同時アクセスによってデータの不整合が起こる場合に、その共有するデータへ同時にアクセスできるスレッド数を制限する」ことです。
排他制御の効果は「データの不整合を防ぐことができる」ことです。
C言語入門者の方がまず学ぶのはシングルスレッドプログラミングですので、共有するデータへの同時アクセスが発生するようなことは基本的にありません。ですので、排他制御に関して意識する必要はありませんでした。
ですが、マルチスレッドプログラミングを行うと同時アクセスが発生する可能性があるので、同時アクセスによりデータの不整合が発生するような場合は排他制御を行う必要があります。
排他制御の必要性
続いて、排他制御の必要性について身近な例を用いて説明したいと思います。
スポンサーリンク
映画館の座席予約システムの例
思い浮かべていただきたいのが映画館の座席予約システムです。
スマホやパソコンから簡単に座席が予約できて便利ですよね。私はとにかく並ぶのが苦手なので、映画見るときは必ず利用しています。
実は、この映画館の座席予約システムにおいても「排他制御」が活躍しています。
排他制御なしの場合に発生する問題
例えば同時に別々の2人が同じ座席を予約しようとしたとしましょう。
もし、排他制御が行われておらず、2人とも座席が予約できてしまうと、映画館に行っていざ座席に座ろうとしても既に他の人が座席に座ってしまっているという状況が発生する可能性があります。
この場合、お客さんはせっかく座席を予約したのに映画が見られないことになります…。こんなシステムを採用している映画館は、きっとお客さんからのクレームが絶えない映画館でしょう…。
このケースでは、同じ座席の予約が同時に行われてしまうと競合が起こり、本来1人しか予約できない座席に対して2人以上が予約できてしまうという「データの不整合」が発生してしまっているのです。
排他制御でデータの不整合を防ぐ
こういったデータの不整合が発生しないように、普通の予約システムでは排他制御が行われ、同時に同じ座席の予約が行われないようになっています。
例えば特定の座席が予約されようとしている間は、他の人からその座席が予約できないように「その座席を選択できなくする」「予約しようとしても “他の人が予約中です” などの画面に遷移させて予約を中止する」などの対策が取られているはずです。
この例では、ある座席へアクセスできる “人数” を制限しましたが、こんな感じで「共有するデータへ同時にアクセスできる “スレッド数” を制限する」のがマルチスレッドにおける排他制御になります。
マルチスレッドを用いれば、同時に様々な処理を並列で実行できるプログラムが作成できるようになりますし、同時に処理ができることで、より便利なプログラムも作れるようになります。
ただし、データの不整合が発生すると、先ほどの映画館座席予約システムのようにむしろ不便になる可能性もあります。
そうならないように、マルチスレッドプログラミングを行う際は、必要に応じて排他制御を行う必要があります。
排他制御なしのプログラミング
まずは排他制御なしの場合にデータの不整合が発生するプログラム例を紹介したいと思います。
これを後ほど排他制御によってデータの不整合が発生しないようにしていきます。
プログラム例
下記は NUM_THREAD
個のスレッドで sum = sum + 1
を ADD_NUM / NUM_THREAD
回ずつ繰り返し、全スレッドの処理終了後に sum
の値を表示するプログラムになります。
要は sum = sum + 1
を ADD_NUM
回実行するプログラムです。
sum
の初期値が 0
、ADD_NUM
が 100000
なので表示される期待値は当然 100000
ですね。
#include <stdio.h>
#include <pthread.h>
#define NUM_THREAD 4
#define ADD_NUM 100000
/* 共有データ */
long long sum = 0;
/* スレッドに渡すデータ */
typedef struct thread_data {
int addNum;
} THREAD_DATA;
void* add(void *arg){
int i;
THREAD_DATA *data = (THREAD_DATA*)arg;;
/* addNum回 sum = sum + 1 を実行 */
for(i = 0; i < data->addNum; i++){
sum = sum + 1;
}
return NULL;
}
int main(void){
pthread_t thread[NUM_THREAD];
THREAD_DATA data[NUM_THREAD];
int i;
for(i = 0; i < NUM_THREAD; i++){
/* ADD_NUMはNUM_THREADで割り切れることを前提としている */
data[i].addNum = ADD_NUM / NUM_THREAD;
}
/* スレッドの開始 */
for(i = 0; i < NUM_THREAD; i++){
pthread_create(&thread[i], NULL, add, &data[i]);
}
/* スレッドの終了待ち */
for(i = 0; i < NUM_THREAD; i++){
pthread_join(thread[i], NULL);
}
/* 計算結果の表示 */
printf("sum = %lld\n", sum);
return 0;
}
pthread を利用しているため、リンク(コンパイル)時には -lpthread
オプションを指定する必要があります。
実行して表示された値は下記のように 100000
よりも小さくなりました。
sum = 94918
スポンサーリンク
期待した結果にならない理由
なぜ期待した値にならなかったのでしょうか?
この理由について解説していきたいと思います。
ポイントは下記の処理です。
sum = sum + 1;
まず sum
はグローバル変数であり、プログラム起動時に作成され、各スレッドで共有されるデータです。
各スレッドで行う処理は「sum = sum + 1
を ADD_NUM / NUM_THREAD
回繰り返す」なので、当然各スレッドは sum
に対してアクセスすることになります。
さらに、スレッドは複数(4つ)なので、各スレッドから “同時に” sum
にアクセスされる可能性があります。
さらに、先程の処理はC言語のソースコードとして1行で記載されていますが、細かく分けると下記の3つの処理に分解されます。
sum
の値を取得するsum
の値に+1
するsum
に 2. の計算結果を代入する
sum
の 値が更新されるのは 3. の処理になります。
これらの処理を、例えば2つのスレッド(仮にスレッドAとスレッドBとします)が並列に実行することを考えてみましょう。
例えばスレッドAで 2. の処理を行っている間にスレッドBで 1. の処理が実行されたとします。
そうなると、スレッドBで取得される sum
の値は、スレッドAで計算した結果が反映されておらず、スレッドAが取得した sum
と同じ値が取得されることになります。
すると、スレッドAとスレッドBは同じ値に対して +1
して sum
に格納することになります。
ここで期待している結果よりも “1 少なくなる” ことになります。
これが積もり積もって最終的に期待していた結果、 100000
よりも sum
が小さい値になってしまうのです。
まさに「複数のスレッド間で共有するデータへの同時アクセスによってデータの不整合が起こる」様子が確認できると思います。
排他制御ありのプログラミング
では次は排他制御を実際に実装していく方法を解説していきたいと思います。
最終的には排他制御なしのプログラミングで紹介したプログラムをデータの不整合が起こらないように改善します。
排他制御を実現するにあたり、このページでは Mutex(より具体的には pthread_mutex
)を利用したいと思います。
問題と解決方法
まずは排他制御なしのプログラミングで紹介したプログラムの問題点と解決方法を整理していきましょう。
先ほどのプログラムの問題は複数のスレッド間で共有されるデータ sum
に同時にアクセスできてしまう点です。
より具体的には、下記の処理が複数のスレッドが同時に実行できてしまうところが問題です。
sum = sum + 1;
こういった複数のスレッドから同時に実行されるとデータの不整合が発生する一連の処理部分を「クリティカルセクション」と呼びます。
クリティカルセクションを複数のスレッドから同時に実行されてしまうとデータの不整合が発生するのであれば、クリティカルセクションを複数のスレッドから実行できないようにしてやれば問題は解決できます。
要は、クリティカルセクションを同時に1つのスレッドからのみ実行可能にしてやれば良いです。
スポンサーリンク
Mutex による排他制御
このような制御を行うのに便利なのが Mutex(ミューテックス)です。
Mutex とはクリティカルセクションを1つのスレッドしか進入できない「鍵付きの個室」として扱うことで排他制御を実現する仕組みです。
POSIX 仕様の Mutex である pthread_mutex
の一般的な使用方法は下記のようになります。要はクリティカルセクションを pthread_mutex_lock
と pthread_mutex_unlock
で囲います。
pthread_mutex_lock(&mutex);
/* クリティカルセクション */
pthread_mutex_unlock(&mutex);
pthread_mutex_lock
は、それ以降の処理(クリティカルセクション)に鍵を掛ける(ロックする)関数になります。
鍵が掛かっている間、他のスレッドが pthread_mutex_lock
を実行してもクリティカルセクションに進入することができません(Mutex のオプション等にもよりますが、pthread_mutex_lock
を実行してもクリティカルセクションに進入できなかったスレッドは、基本的にこの鍵が開けられるまで待たされることになります)。
また pthread_mutex_unlock
は pthread_mutex_lock
により掛けられた鍵を開ける(アンロックする)関数になります。
鍵が開けられるので、他のスレッドがクリティカルセクションに進入することができるようになります。
もし pthread_mutex_lock
関数で待たされているスレッドがあれば、そのスレッドが自動的にクリティカルセクションに進入し、そのスレッドの処理が再開されることになります。
こういった仕組みにより、pthread_mutex_lock
と pthread_mutex_unlock
で囲ったクリティカルセクション内には同時に1つのスレッドしか進入できなくなります。
つまり、クリティカルセクション内の処理を同時に複数のスレッドで実行されることを防ぐことができます。
したがって、pthread_mutex_lock
と pthread_mutex_unlock
の間に「共有データへの同時アクセスによりデータの不整合が発生する可能性のある処理」を実行させるようにすれば、データの不整合も起きなくなります。
排他制御を行うプログラム例
排他制御なしのプログラミングで紹介したプログラムを、Mutex を利用して排他制御するように改善したプログラムは下記のようになります。
Mutex やスレッド生成には POSIX 仕様の pthread を利用しています。
#include <stdio.h>
#include <pthread.h>
#define NUM_THREAD 4
#define ADD_NUM 100000
/* 共有データ */
long long sum = 0;
/* Mutexオブジェクト */
pthread_mutex_t mutex;
/* スレッドに渡すデータ */
typedef struct thread_data {
int addNum;
} THREAD_DATA;
void* add(void *arg){
int i;
THREAD_DATA *data = (THREAD_DATA*)arg;;
/* addNum回 sum = sum + 1 を実行 */
for(i = 0; i < data->addNum; i++){
pthread_mutex_lock(&mutex);
sum = sum + 1;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(void){
pthread_t thread[NUM_THREAD];
THREAD_DATA data[NUM_THREAD];
int i;
/* Mutexオブジェクトの初期化 */
pthread_mutex_init(&mutex, NULL);
for(i = 0; i < NUM_THREAD; i++){
/* ADD_NUMはNUM_THREADで割り切れることを前提としている */
data[i].addNum = ADD_NUM / NUM_THREAD;
}
/* スレッドの開始 */
for(i = 0; i < NUM_THREAD; i++){
pthread_create(&thread[i], NULL, add, &data[i]);
}
/* スレッドの終了待ち */
for(i = 0; i < NUM_THREAD; i++){
pthread_join(thread[i], NULL);
}
/* Mutexオブジェクトの破棄 */
pthread_mutex_destroy(&mutex);
/* 計算結果の表示 */
printf("sum = %lld\n", sum);
return 0;
}
今回は、下記のように必ず結果が 100000
になります。
sum = 100000
プログラムの解説
排他制御なしのプログラムで紹介したプログラムとの一番大きな違いは、もともと下記のように複数スレッドで共有するデータ sum
へアクセスする処理を、
sum = sum + 1;
下記のように pthread_mutex_lock
と pthread_mutex_unlock
で囲うようにしたところです。
pthread_mutex_lock(&mutex);
sum = sum + 1;
pthread_mutex_unlock(&mutex);
これらで囲った部分を同時に処理できるのは1スレッドのみになるので、sum
への同時アクセスが無くなりデータの不整合も起きなくなります。
Mutex は通常下記の手順で使用します。
- Mutex オブジェクトの生成
- Mutex オブジェクトの初期化
- Mutex を利用したロック
- Mutex を利用したアンロック
- Mutex オブジェクトの破棄
3. と 4. に関してはプログラム内で必要な回数実行することになります。
1. Mutex オブジェクトの生成
Mutex オブジェクトの生成は pthread_mutex_t
型変数の宣言により行うことができます。今回はグローバル変数として宣言しています。
/* Mutexオブジェクト */
pthread_mutex_t mutex;
この変数のアドレスが、pthread_mutex_lock
や pthread_mutex_unlock
の引数となります。
この Mutex オブジェクトは同一プログラム内で複数生成して使用することもできます。
2. Mutex オブジェクトの初期化
Mutex オブジェクトの初期化は pthread_mutex_init
で行います。上記プログラムでは main
関数の中で実行しています。
/* Mutexオブジェクトの初期化 */
pthread_mutex_init(&mutex, NULL);
第1引数は Mutexオブジェクト(pthread_mutex_t
型変数)へのアドレス、第2引数には pthread_mutexattr_t
型変数へのアドレスを指定します。
pthread_mutexattr_t
型変数は Mutex オブジェクトの設定を行うための変数になります。今回は初期設定のままで良かったので、NULL
を設定しています。
3. Mutex を利用したロック
Mutex による排他制御で解説したように、Mutex を利用したロックは pthread_mutex_lock
を実行することで行います。
pthread_mutex_lock(&mutex);
第1引数には Mutex オブジェクト(pthread_mutex_t
型変数)へのアドレスを指定します。
pthread_mutex_lock
によりロックされている間、同じ Mutex オブジェクトに対して pthread_mutex_lock
を実行したスレッドは、他のスレッドによって pthread_mutex_unlock
が実行されるまで pthread_mutex_lock
関数の中で待たされることになります。
4. Mutex を利用したアンロック
Mutex による排他制御で解説したように、Mutex を利用したアンロックは pthread_mutex_unlock
を実行することで行います。
pthread_mutex_unlock(&mutex);
第1引数には Mutex オブジェクト(pthread_mutex_t
型変数)へのアドレスを指定します。
pthread_mutex_lock
はクリティカルセクションの前に実行するのに対し、pthread_mutex_unlock
はクリティカルセクションの後に実行します。
これにより、pthread_mutex_lock
関数の中で待たされている他のスレッドがクリティカルセクションに進入できるようになり、自動的に処理が再開されることになります。
5. Mutex オブジェクトの破棄
Mutex が不要になったら最後に破棄を行います。Mutex オブジェクトの破棄は pthread_mutex_destroy
で行います。上記プログラムでは main
関数の中で実行しています。
/* Mutexオブジェクトの破棄 */
pthread_mutex_destroy(&mutex);
第1引数には Mutex オブジェクト(pthread_mutex_t
型変数)へのアドレスを指定します。
スポンサーリンク
共有データ
最初に排他制御とは「複数のスレッドで共有するデータへの同時アクセスによりデータの不整合が起こる場合に、その共有するデータへ同時にアクセスできるスレッド数を制限する」ことであると説明しました。
この説明の中に出てくる「複数のスレッドで共有するデータ」とは具体的にどんなデータであるかについて説明しておきたいと思います。
この共有データに複数スレッドから同時にアクセスされる可能性がある場合は、データの不整合が発生する可能性があります。不整合が発生する場合は排他制御を行う必要があります。
共有データの例
「複数のスレッドで共有するデータ」と聞くとパッとイメージがつかないかもしれませんが、結局各スレッドで実行するのは関数です。
つまり「複数のスレッドで共有するデータ」とは「いろんな関数からアクセス可能なデータ」と考えられます。
グローバル変数
「いろんな関数からアクセス可能なデータ」と聞いて一番最初に思い浮かぶのは「グローバル変数」ではないでしょうか?
グローバル変数は、同じファイル内に定義された関数であれば、どの関数からもアクセスすることが可能であり、どのスレッドからもアクセス可能なデータになります。
例えば下記は異なる関数から同じグローバル変数 a
にアクセスする例になります。
int a = 0;
void funcA(void) {
a = a + 1;
}
void funcB(void) {
a = a - 1;
}
今回紹介したプログラムでも、共有データとしてはグローバル変数を利用していました。
ファイル
あとは「ファイル」です。異なる関数からでも fopen
関数で同じファイル名のファイルを開くと、同じファイルにアクセスすることができます。
つまり、複数スレッドから同時に同じファイルにアクセス可能です。
例えば下記は異なる関数から同じファイル(test.txt)にアクセスする例になります。
void funcA(void) {
FILE *fo;
fo = fopen(“test.txt”, “w”);
/* ファイルに対する処理 */
}
void funcB(void) {
FILE *fo;
fo = fopen(“test.txt”, “w”);
/* ファイルに対する処理 */
}
同じファイルが同時に編集されてしまうとデータの不整合が起きてしまう(前にデータ保存した人の変更が上書きされてしまう等)ことはイメージつきやすいですよね?
アドレス
さらには「アドレス指定」する場合も注意が必要です。
この場合は、より正確に言うと「そのアドレスに存在するデータ」が共有データになります。
C言語ではアドレスさえ同一のものを指定してやれば、異なる関数からでも同一のデータにアクセスすることが可能です。
例えば下記は異なる関数から同じアドレス(main
関数で変数宣言した変数 x
のアドレス)にアクセスする例になります。
void funcA(int *a) {
*a = *a + 1;
}
void funcB(int * a) {
*a = *a - 1;
}
int main(void) {
int x = 0;
funcA(&x);
funcB(&x);
return 0;
}
「共有データ = 排他制御必要」とは限らない
勘違いしないでいただきたいのは、ここで挙げた共有データ全てに対して排他制御が必要というわけでは無いということです。
排他制御が必要なのは、共有データかつ、同時アクセスによりデータの不整合が起こる場合です。
例えば、データの読み込み(データの取得)目的でしか利用しない共有データであれば、複数スレッドから同時にアクセスされてもデータの不整合は起きません。
また、グローバル変数の配列も共有データですが、下の図のように各スレッドでアクセスする領域が重複しないように設計・実装してやれば同時アクセス自体を防ぐこともできます。
実際にこの考え方でマルチスレッド により画像の拡大縮小を行なっている例を下記で説明していますので、興味のある方は是非読んでみてください。
【C言語】マルチスレッドで画像の拡大縮小を高速化共有データでないデータの例
逆に共有データでないデータとしては「ローカル変数」が挙げられます。
ローカル変数とは関数内で変数宣言される変数で、これは関数が実行されるたびに毎回新たに用意されるデータです。
したがって、複数スレッドから同時に同じ関数が実行され、同じ名前の変数にアクセスしたとしても、データの実体としては異なりますので異なるデータにアクセスしていることになります。
なので、ローカル変数は共有データではないですし、排他制御をする必要はありません。
ただし、ポインタの場合は注意が必要です。特にポインタ変数の前に *
演算子をつけてポインタの指す先のデータにアクセスする場合は注意が必要です。
前述の通り、アドレス指定する場合はそのアドレスに存在するデータが共有データになり得ます。
ですので、ポインタ変数自体は異なるデータなので共有データで無いとしても、ポインタの指す先のデータは同一のデータであり、複数スレッド間で共有されるデータである可能性があります。
特にアドレス指定の場合は、そのアドレスに存在するデータが共有データかどうかを確認し、必要に応じて排他制御する必要があります。
スポンサーリンク
Mutex の使用例
最後に Mutex を使用して排他制御を行う実例をいくつか紹介したいと思います。
複数行で共有データへの読み込み・書き込みが行われる場合
排他制御なしのプログラミングで紹介したプログラムでは下記で共有データへの同時アクセスが発生してデータの不整合が発生していました。
sum = sum + 1;
で、これを排他制御ありのプログラミングで下記のように Mutex を利用することで、共有データへの同時アクセスを行わないようにしました。
pthread_mutex_lock(&mutex);
sum = sum + 1;
pthread_mutex_unlock(&mutex);
では、共有データへのアクセスを行う処理を下記のように変更した場合、どのように Mutex を利用すれば良いでしょうか?
int a;
a = sum;
a = a + 1;
sum = a;
共有データへ同時アクセスさせないことを考えれば下記のように Mutex を利用することも考えられます。
int a;
pthread_mutex_lock(&mutex);
a = sum;
pthread_mutex_unlock(&mutex);
a = a + 1;
pthread_mutex_lock(&mutex);
sum = a;
pthread_mutex_unlock(&mutex);
しかし、これだとデータの不整合が起こり得ます。
結局期待した結果にならない理由で説明した現象と同じ現象が発生します。
確かに上記のように Mutex を利用することで共有データへの同時アクセスを防ぐことができます。しかし、排他制御を行う目的はあくまでも「データの不整合を防ぐこと」です。
今回取り上げている処理では「他のスレッドが計算結果を sum
に格納する前に sum
の値を取得してしまう」ことが原因でデータの不整合が発生しています。
なので、データの不整合を防ぐためには「他のスレッドが計算結果を sum
に格納する前に sum
の値を取得しない」ようにする必要があります。
つまり「sum
の値の取得から sum
の値の格納まで」の間に対して排他制御する必要があります。
これを実現するのが下記になります。
int a;
pthread_mutex_lock(&mutex);
a = sum;
a = a + 1;
sum = a;
pthread_mutex_unlock(&mutex);
「共有データへの同時アクセス」が発生するかどうかは排他制御の必要性を見定めるのに良い目印になりますが、排他制御は「共有データへアクセスする処理のみに対して行えば良い」というわけではないので注意してください。
あくまでも重要なのは「データの不整合を防ぐ」ことです。これを考えて排他制御を実装する必要があります。
これは大体の場合、「共有データからのデータの読み込み〜共有データへのデータの書き込み」の間を排他することで実現できると思います。
複数箇所から共有データにアクセスされる場合
排他制御なしのプログラミングや排他制御ありのプログラミングで紹介したプログラムでは、同じ関数から共有データにアクセスしていたので、排他制御は1箇所のみ行えば良かったのですが、複数の関数から同じ共有データにアクセスされる場合もあります。
例えば下記のように funcA
と funcB
から共有データ a
にアクセスする例などが挙げられます。あるスレッドで funcA
を、他のスレッドで funcB
を同時に実行するような場合にデータの不整合が起こりえます(Mutex の初期化等の処理は省略しています)。
int a = 0;
void funcA(void) {
a = a + 1;
}
void funcB(void) {
a = a - 1;
}
このような場合、どのようにして排他制御を行えば良いでしょうか?
例えば下記のように関数ごとに異なった Mutex オブジェクトを用いて排他制御することが考えられます(Mutex の初期化等の処理は省略しています)。
int a = 0;
void funcA(void) {
pthread_mutex_lock(&mutexA);
a = a + 1;
pthread_mutex_unlock(&mutexA);
}
void funcB(void) {
pthread_mutex_lock(&mutexB);
a = a - 1;
pthread_mutex_unlock(&mutexB);
}
これは間違った排他制御になります。前述で Mutex とは「鍵付きの個室」と喩えましたが、Mutex オブジェクトが異なると「鍵付きの個室」も別のものとして扱われます。
なので、funcA
で mutexA
をロックしている最中でも、mutexB
はロックされていませんので funcB
で mutexB
をロックし、クリティカルセクションに進入することができてしまいます。したがって排他制御しているにも関わらず、共有データ a
に同時アクセスできることになります。
共有データ a
への同時アクセスを防ぐためには、同じ Mutex オブジェクトを用いて排他制御を行う必要があります。
int a = 0;
void funcA(void) {
pthread_mutex_lock(&mutexA);
a = a + 1;
pthread_mutex_unlock(&mutexA);
}
void funcB(void) {
pthread_mutex_lock(&mutexA);
a = a - 1;
pthread_mutex_unlock(&mutexA);
}
特に排他制御初心者の方は「同じ共有データへのアクセスを排他する場合、同じ Mutex を用いる」考え方で排他制御を行えば良いと思います。
スポンサーリンク
共有データが複数ある場合
ここまで紹介してきたプログラムでは、全て共有データが1つだけでした。もちろん共有データが複数の場合もあります。
int a = 0;
int b = 0;
void funcA(void) {
a = a + 1;
}
void funcB(void) {
a = a - 1;
}
void funcC(void) {
b = b + 1;
}
void funcD(void) {
b = b - 1;
}
共有データが2つなので、Mutex も2つ用意する必要があるようにも感じますが、実は下記のように1つの Mutex 排他制御は実現できます。
int a = 0;
int b = 0;
void funcA(void) {
pthread_mutex_lock(&mutexA);
a = a + 1;
pthread_mutex_unlock(&mutexA);
}
void funcB(void) {
pthread_mutex_lock(&mutexA);
a = a - 1;
pthread_mutex_unlock(&mutexA);
}
void funcC(void) {
pthread_mutex_lock(&mutexA);
b = b + 1;
pthread_mutex_unlock(&mutexA);
}
void funcD(void) {
pthread_mutex_lock(&mutexA);
b = b - 1;
pthread_mutex_unlock(&mutexA);
}
ただし、異なる共有データに同じ Mutex を用いるとプログラムの並列度が下がります。要は処理効率が低下します。
上記のプログラムであれば、変数 a
にあるスレッドがアクセスしている間でも、変数 b
は変数 a
とは無関係の変数ですので、同時に変数 b
に他のスレッドがアクセスしても問題ないはずです。
ですが、同じ Mutex で排他制御を行っているために、変数 a
にアクセスしている間は変数 b
にもアクセスできないようになってしまっています。余分に排他制御してしまっている感じですね。
ただし共有データへの同時アクセスは起こらないので、データを不整合を防ぐという目的は達成できています。
下記のように共有データごとに Mutex を用意してやれば、変数 a
にアクセスしている間も変数 b
にアクセスはできるようになり、この問題は解消されます(もちろん変数 a
と変数 b
それぞれに対しては排他制御が行われているためデータの不整合は起きません)。
int a = 0;
int b = 0;
void funcA(void) {
pthread_mutex_lock(&mutexA);
a = a + 1;
pthread_mutex_unlock(&mutexA);
}
void funcB(void) {
pthread_mutex_lock(&mutexA);
a = a - 1;
pthread_mutex_unlock(&mutexA);
}
void funcC(void) {
pthread_mutex_lock(&mutexB);
b = b + 1;
pthread_mutex_unlock(&mutexB);
}
void funcD(void) {
pthread_mutex_lock(&mutexB);
b = b - 1;
pthread_mutex_unlock(&mutexB);
}
ただし、このように使用する Mutex オブジェクトの数が増えるとデッドロックが発生する可能性が発生します。
デッドロックについては下記の Wikipedia などを見るとイメージがつくと思います。
https://ja.wikipedia.org/wiki/%E3%83%87%E3%83%83%E3%83%89%E3%83%AD%E3%83%83%E3%82%AF
基本的には共有データの数だけ Mutex オブジェクトを用意するという考え方で良いですが、特にクリティカルセクションの中で他のクリティカルセクションに進入するようなプログラムではデッドロックが起こらないように注意する必要があります。
このような場合は並列度を犠牲にして異なる共有データに対して同じ Mutex オブジェクトを用いるなどの対応を行う必要があります。
デッドロックに関してはいずれ私のサイトでも紹介したいと思います。
まとめ
このページでは特にマルチスレッドにおける「排他制御」について解説しました。
マルチスレッドを利用すれば、いろんな処理を並列に実行できるようになり、作成できるプログラムが広がります。
ただし、処理を並列に実行することにより、共有データへの同時アクセスによりデータの不整合が起こる可能性が発生します。このデータの不整合を防ぐ時に便利なのが「排他制御」です。
排他制御と聞くと難しそうに感じるかもしれませんが、このページで紹介した通り、データの不整合が生じる可能性のある一連の処理(クリティカルセクション)を Mutex のロックとアンロックで囲えば良いだけです。
ただし用意する Mutex の数が増えるとデッドロックが発生する可能性もありますので注意してください。
まずは排他制御を実際に利用して慣れていくのが良いと思います!