pthread mutexで排他ロックする方法
マルチスレッドプログラミングでスレッド間で共有データにアクセスするときに、mutex(MUTual EXclusion, ミューテックス)を用いて、排他ロックを行うことがあります。プログラムに競合状態を引き起こすようなコードがあると、計算の整合性、データの整合性が失われます。競合状態を避ける目的で、クリティカルリージョンをロックで保護します。pthread では、pthread_mutex_tとpthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock を用いて、ロックをコントロールします。
読み方
- mutex
- みゅーてっくす
- 競合状態
- きょうごうじょうたい
- MUTual EXclusion
- みゅーちゃる えくすくるーじょん
- クリティカルセクション
- くりてぃかるせくしょん
- critial section
- くりてぃかるせくしょん
- critial region
- くりてぃかるりーじょん
目次
概要
ロックには、
- pthread_mutex_lock
- pthread_mutex_trylock
の2種類の関数が提供されています。 pthread_mutex_lockは、すでにロックされているときに、ロックが解除されて、ロックを取得できるまで待ちます。pthread_mutex_unlockは、すでにロックされているときは、すぐに関数が返ります。
mutex1.c は、単純に2つのスレッドで1つのカウンタをインクリメントするだけのプログラムです。 mutexのロックがある場合と、ない場合の違いをみてみましょう。
counter = counter + 1
mutex1.c のプログラムのロックとアンロックがない場合、2つのスレッドが同時に counter の値を取り出し、それぞれが数を足して、counter に代入した場合、本来、2つのスレッドがそれぞれ1を足して、2になるところ、結果的に1となりうるプログラムです。
for(i=0; i<loop_max; i++){ // lock なし counter++; }
lockとunlockで囲まれた領域は、ロックによって保護されます。 ロックとアンロックの間にある counter++ は、プロセス内部の処理としてはアトミックに処理されます。
for(i=0; i<loop_max; i++){ int r; r = pthread_mutex_lock(&m); if (r != 0) { errc(EXIT_FAILURE, r, "can not lock"); } counter++; r = pthread_mutex_unlock(&m); if (r != 0) { errc(EXIT_FAILURE, r, "can not unlock"); } }
スレッド1がpthread_mutex_lockでロックしている間は、ほかのスレッド2が同じmutexに対して pthread_mutex_lock をしようとすると、スレッド1がアンロックするまで、スレッド2が待たされることになります。
ここでは、1つのロックしか扱いませんが、複数のmutexを扱う場合には、ロックの順番、アンロックの順番に注意しないと、デッドロック状態に陥ります。
pthread_mutex_lock
pthread_mutex_lock は、以下のように動作します。
- 誰もロックされてない
- ロックして、返ります。
- 誰かがロックしている
- アンロックされるまで待つ
pthread_mutex_trylock
pthread_mutex_lock は、以下のように動作します。
- 誰もロックされてない
- ロックして、返ります。
- 誰かがロックしている
- アンロックを待たずに返ります。戻り値は、EBUSY が設定されています。
EBUSYは、errno.hで定義されているので、errno.hをincludeする必要があります。
mutexの初期化
mutexは、初期化と破棄が必要です。
mutexの初期化には、2種類の書き方ができます。
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_init(&mutex, NULL);
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
pthread_mutex_init()は、第2引数でpthread_mutexattr_tを渡すことにより、初期化パラメータを渡すことができます。NULLを渡すとデフォルトの値で初期化されます。
pthread_mutex_destroy(&mutex);
ロックありとなしについて
コンパイル時に -DNOLOCK のオプションをつけることで、ロックなしでコンパイルされます。
$ cc -DNOLOCK -lpthread mutex1.c
pthread_mutex_lockによる排他ロックの例
ソースコード mutex1.c
#include <stdio.h> #include <stdlib.h> #include <err.h> #include <pthread.h> #include <unistd.h> #include <string.h> const size_t loop_max = 65535; pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; int counter = 0; void f1(); void f2(); int main(int argc, char *argv[]) { pthread_t thread1, thread2; int ret1,ret2; ret1 = pthread_create(&thread1,NULL,(void *)f1,NULL); ret2 = pthread_create(&thread2,NULL,(void *)f2,NULL); if (ret1 != 0) { err(EXIT_FAILURE, "can not create thread 1: %s", strerror(ret1) ); } if (ret2 != 0) { err(EXIT_FAILURE, "can not create thread 2: %s", strerror(ret2) ); } printf("execute pthread_join thread1\n"); ret1 = pthread_join(thread1,NULL); if (ret1 != 0) { errc(EXIT_FAILURE, ret1, "can not join thread 1"); } printf("execute pthread_join thread2\n"); ret2 = pthread_join(thread2,NULL); if (ret2 != 0) { errc(EXIT_FAILURE, ret2, "can not join thread 2"); } printf("done\n"); printf("%d\n", counter); pthread_mutex_destroy(&m); return 0; } void f1() { size_t i; for(i=0; i<loop_max; i++){ #ifndef NOLOCK int r; r = pthread_mutex_lock(&m); if (r != 0) { errc(EXIT_FAILURE, r, "can not lock"); } #endif counter++; #ifndef NOLOCK r = pthread_mutex_unlock(&m); if (r != 0) { errc(EXIT_FAILURE, r, "can not unlock"); } #endif } } void f2() { size_t i; for(i=0; i<loop_max; i++){ #ifndef NOLOCK if (pthread_mutex_lock(&m) != 0) { err(EXIT_FAILURE, "can not lock"); } #endif counter++; #ifndef NOLOCK if (pthread_mutex_unlock(&m) != 0) { err(EXIT_FAILURE, "can not unlock"); } #endif } }
コンパイル
cc -lpthread mutex1.c -o mutex1
実行例
% ./mutex1 execute pthread_join thread1 execute pthread_join thread2 done 131070
mutexを使用しないとどうなるのか?
これは、上のコードからmutexのロックとアンロックがないときのプログラムです。 mutex1.c は、単純に2つのスレッドで1つのカウンタをインクリメントするだけのプログラムです。
計算は、65535 * 2 = 131070 となるはずですが、3回実行して、違う結果になっています。
$ /usr/bin/time ./nolock execute pthread_join thread1 execute pthread_join thread2 done 115474 0.00 real 0.00 user 0.00 sys $ /usr/bin/time ./nolock execute pthread_join thread1 execute pthread_join thread2 done 111517 0.00 real 0.00 user 0.00 sys $ /usr/bin/time ./nolock execute pthread_join thread1 execute pthread_join thread2 done 122026 0.00 real 0.00 user 0.00 sys
mutexを使用した場合、どうなるのか?
mutex でカウンタの処理をアトミックにした場合、3回とも正しい結果になっています。 プログラムが単純過ぎて、比較の意味はほとんどありませんが、ロックのある場合とない場合で、ほんの少しだけ、処理時間がロックありの場合、多くなっていることがわかります。
$ /usr/bin/time ./lock execute pthread_join thread1 execute pthread_join thread2 done 131070 0.01 real 0.00 user 0.00 sys $ /usr/bin/time ./lock execute pthread_join thread1 execute pthread_join thread2 done 131070 0.01 real 0.00 user 0.00 sys $ /usr/bin/time ./lock execute pthread_join thread1 execute pthread_join thread2 done 131070 0.01 real 0.01 user 0.00 sys
pthread_mutex_lockによる例
ここでは、pthread_mutex_trylockを利用した例です。ロックできないときは、諦めてcontinueだけします。ループの分だけ、カウントされるわけではありません。ループして、ロックができたときだけ、インクリメントします。
mutex_try1.c の例
ソースコード mutex_try1.c
#include <stdio.h> #include <stdlib.h> #include <err.h> #include <errno.h> #include <pthread.h> #include <unistd.h> #include <string.h> const size_t loop_max = 65535; pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; int counter = 0; void f1(); void f2(); int main(int argc, char *argv[]) { pthread_t thread1, thread2; int ret1,ret2; ret1 = pthread_create(&thread1,NULL,(void *)f1,NULL); ret2 = pthread_create(&thread2,NULL,(void *)f2,NULL); if (ret1 != 0) { err(EXIT_FAILURE, "can not create thread 1: %s", strerror(ret1) ); } if (ret2 != 0) { err(EXIT_FAILURE, "can not create thread 2"); err(EXIT_FAILURE, "can not create thread 2: %s", strerror(ret2) ); } printf("execute pthread_join thread1\n"); ret1 = pthread_join(thread1,NULL); if (ret1 != 0) { errc(EXIT_FAILURE, ret1, "can not join thread 1"); } printf("execute pthread_join thread2\n"); ret2 = pthread_join(thread2,NULL); if (ret2 != 0) { errc(EXIT_FAILURE, ret2, "can not join thread 2"); } printf("done\n"); printf("%d\n", counter); pthread_mutex_destroy(&m); return 0; } void f1() { size_t i; size_t try_again = 0; for(i=0; i<loop_max; i++){ #ifndef NOLOCK int r; r = pthread_mutex_trylock(&m); if (r != 0) { if (EBUSY == r) { try_again++; continue; } errc(EXIT_FAILURE, r, "can not lock"); } #endif counter++; #ifndef NOLOCK r = pthread_mutex_unlock(&m); if (r != 0) { errc(EXIT_FAILURE, r, "can not unlock"); } #endif } printf("%s: try %lu\n", __func__, try_again); } void f2() { size_t i; size_t try_again = 0; for(i=0; i<loop_max; i++){ #ifndef NOLOCK int r; r = pthread_mutex_trylock(&m); if (r != 0) { if (EBUSY == r) { try_again++; continue; } err(EXIT_FAILURE, "can not lock"); } #endif counter++; #ifndef NOLOCK if (pthread_mutex_unlock(&m) != 0) { err(EXIT_FAILURE, "can not unlock"); } #endif } printf("%s: try %lu\n", __func__, try_again); }
コンパイル
cc -lpthread mutex_try1.c -o mutex_try1
実行例
% ./mutex_try1 execute pthread_join thread1 f2: try 49082 f1: try 15857 execute pthread_join thread2 done 66131
まとめ
- クリティカルリージョンは、mutexで保護しましょう。
- mutex で保護する領域は、最小限にしましょう。
- 複数の mutex を使用する場合には、ロックとアンロックの順番に気をつけましょう。
関連項目
- pthreadとは
- pthread errnoとpthreadライブラリ関数の戻り値
- pthread 1つのスレッドを動かす
- pthread 2つのスレッドを動かす
- pthread mutexで排他ロックする方法
- pthreadのスレッド識別子pthread_t型
- pthread スレッドに値を渡す方法
- pthread スレッドから値を返す方法
ツイート