pthread mutexで排他ロックする方法

提供: C言語入門
移動: 案内検索
スポンサーリンク

マルチスレッドプログラミングでスレッド間で共有データにアクセスするときに、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_trylock は、すでにロックされているときは、すぐに関数が返ります。

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 を使用する場合には、ロックとアンロックの順番に気をつけましょう。

関連項目




スポンサーリンク