スレッドの排他制御:排他制御の有無を動作検証する
(プログラムの概要)
複数スレッドを生成し並列で処理を行うといった場合、スレッド間での排他制御が必要になります。内部メモリはスタックが別なので問題ありませんが、外部メモリやヒープメモリは全スレッドで共通となります。これらのメモリを複数スレッドで同時に使用すると不整合が起きることになります。これを制御するのが排他制御で具体的にはmutexを使用します。
サンプルプログラムでは10個のスレッドを生成し、各スレッドで1万回外部メモリをカウントアップします。最終的にはカウンターの値が10万になるはずですが、排他制御の有無でどのように変わるか見てみましょう。
1.排他制御をしない場合
/* * sample program * mutual exclusion test program */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h> #define MAX_THREADS 10 #define MAX_CNT 10000 int grobal_cnt; void thread(void); int main(void) { int i = 0; pthread_t thread_id[MAX_THREADS]; void *thread_return; printf("sample program(%s) start\n", __FILE__); /* create new threads */ for(i = 0; i < MAX_THREADS; i++){ if(pthread_create(&thread_id[i], NULL, (void *)thread, NULL) < 0){ perror("pthread_create error"); exit(1); } } /* wait for the end of threads */ for(i = 0; i < MAX_THREADS; i++){ if(pthread_join(thread_id[i], &thread_return) < 0){ perror("pthread_join error"); exit(1); } } printf("grobal_cnt = %d\n", grobal_cnt); return 0; } void thread(void) { int i = 0, cnt = 0; printf("thread[%lu] start\n", pthread_self()); /* counter increment */ for(i = 0; i < MAX_CNT; i++){ cnt = grobal_cnt; cnt++; grobal_cnt = cnt; } printf("thread[%lu] end\n", pthread_self()); pthread_exit(0); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
(プログラムの概要)
main関数の動作
25~30L:スレッドの生成
thread()関数を10個スレッドとして生成しています。
33~38L:スレッドの終了待ち
pthread_join()で全ての子スレッドが終了するのを待っています。pthread_join()は第一引数で渡されたIDのスレッドがreturnで終了するかpthead_exit()で明示的に終了するのを待ちます。

スレッドの動作
外部メモリであるgrobal_cntを内部メモリにコピー後カウントアップしgrobal_cntに設定するという動作を1万回繰り返します。一度内部メモリにコピーしている間に処理が他のスレッドに切り替わる動きを検証していますのでgrobal_cnt++としてはダメです。


(コンパイルと動作)
gcc -Wall nomutex.c -lpthread
実行毎にgrobal_cntの値は変わると思いますが、100000になることは少ないと思います。
(マルチコアのCPUでは顕著になると思います。)
実行結果 sample program(nomutex.c) start thread[3085818768] start thread[3077426064] start thread[3077426064] end thread[3085818768] end thread[3060640656] start thread[3069033360] start thread[3060640656] end thread[3069033360] end thread[3052247952] start thread[3052247952] end thread[3043855248] start thread[3027069840] start thread[3035462544] start thread[3027069840] end thread[3035462544] end thread[3018677136] start thread[3018677136] end thread[3010284432] start thread[3043855248] end thread[3010284432] end grobal_cnt = 57448

2.排他制御をした場合
/* * sample program * mutual exclusion test program */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <pthread.h> #define MAX_THREADS 10 #define MAX_CNT 10000 int grobal_cnt; pthread_mutex_t mutex; void thread(void); int main(void) { int i = 0; pthread_t thread_id[MAX_THREADS]; void *thread_return; printf("sample program(%s) start\n", __FILE__); pthread_mutex_init(&mutex, NULL); /* create new threads */ for(i = 0; i < MAX_THREADS; i++){ if(pthread_create(&thread_id[i], NULL, (void *)thread, NULL) < 0){ perror("pthread_create error"); exit(1); } } /* wait for the end of threads */ for(i = 0; i < MAX_THREADS; i++){ if(pthread_join(thread_id[i], &thread_return) < 0){ perror("pthread_join error"); exit(1); } } printf("grobal_cnt = %d\n", grobal_cnt); return 0; } void thread(void) { int i = 0, cnt = 0; printf("thread[%lu] start\n", pthread_self()); /* counter increment */ for(i = 0; i < MAX_CNT; i++){ pthread_mutex_lock(&mutex); cnt = grobal_cnt; cnt++; grobal_cnt = cnt; pthread_mutex_unlock(&mutex); } printf("thread[%lu] end\n", pthread_self()); pthread_exit(0); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
(プログラムの概要)
プログラムの変更点ですが、15Lでmutexという排他制御用のメモリを宣言し、25Lで初期化を行っています。
実際に排他制御を行っているのは子スレッドのカウントアップ前後で、51Lでロックを行い55Lで解放しています。pthread_mutex_lock()~pthread_mutex_unlock()で囲まれた間が排他制御される区間になります。あるスレッドがロックを行いカウントアップの処理を行っている間は他のスレッドがロックを行おうとしても待たされる為、grobal_cntにアクセスするスレッドは常に1つになります。
(コンパイルと動作)
gcc -Wall mutex.c -lpthread
排他制御を行った場合は何回実行してもgrobal_cnt=100000となり、意図した動きができています。
実行結果 sample program(mutex.c) start thread[3086310288] start thread[3086310288] end thread[3077917584] start thread[3077917584] end thread[3069524880] start thread[3069524880] end thread[3061132176] start thread[3052739472] start thread[3044346768] start thread[3035954064] start thread[3027561360] start thread[3035954064] end thread[3044346768] end thread[3052739472] end thread[3027561360] end thread[3019168656] start thread[3010775952] start thread[3061132176] end thread[3019168656] end thread[3010775952] end grobal_cnt = 100000

(補足)
全てのスレッドがgrobal_cntにアクセスする前にmutexを使って排他制御をしているから正常な動作を行えます。mutexのlock/unlockを行わずにgrobal_cntにアクセスするようなスレッド1つでも存在すると他のスレッドで排他制御を行っている意味は無くなってしまいます。排他制御をする場合は全てのスレッドで漏れなく行うようにします。

次回はmutexの誤った使い方をした場合の動作不能=デッドロックについてです。