はじめに
この連載ではUNIX系OSなどで使われるスレッド「pthread」についてサンプルを交えて説明していきます。pthreadはPOSIXが仕様化したスレッドモデルです。サンプルはCと一部C++、調査環境はFedora 8(2.6.23.1-49.fc8)、32bit、glibc-4.1-2、gcc-4.1.2-33およびFedora Core 6(2.6.18-1.2798.fc6)、32bit、glibc-2.5-3、gcc-4.1.1-30を使用しています。
前回の記事
3.同期(1/4):mutex
マルチスレッドに限らず、独立した処理を円滑に行うためには共有情報を意識し保護する必要があります。スレッドは非同期で動作しますが、スレッド内の全処理を非同期で処理する事は少なく、通常は仕様上および効率性の理由から、多少なりともスレッドが参照する一部の情報を他スレッドや呼び出し元と同期(他スレッドの排除)する必要があります。
スレッドはプロセス内の変数を共有しているので、何も対策をとらないと、あるスレッドが書き換えている最中に他のスレッドが更新したり消した時に問題が発生しそうなことは容易に想像がつくでしょう。スレッド間で協調して管理しなくてはいけない情報にアクセスするための機能を、同期処理と言います。そしてPOSIXの仕様では、スレッドの同期処理が3個あります。
- mutex
- spin
- read/write lock
以降、上記3個の同期処理について掘り下げて行きます。
ちなみに同時に複数のスレッドが共有情報を同時に操作できないようにする事を相互排除(mutual exclusion)と言います。更に相互排除が必要な領域の事をクリティカルセクション(critical section)と言います。
3.1 mutex
前述したようにmutexは同期処理の1つで、オーソドックスで使いやすい同期処理です。主なmutex関数は次のとおりで、詳細は適時manやココなどで参照してください。
- int pthread_mutex_init( pthread_mutex_t * mutex, const pthread_mutex_attr_t * mutexattr );
- int pthread_mutex_destroy( pthread_mutex_t * mutex );
- int pthread_mutex_lock( pthread_mutex_t * mutex );
- int pthread_mutex_unlock( pthread_mutex_t * mutex );
- int pthread_mutex_trylock( pthread_mutex_t * mutex );
簡単に説明すると、同期用の変数mutexを初期化し、共有情報へアクセスする直前でmutex_lockし、アクセスを終えたらmutex_unlockを行います。trylockはロックを試してみて、ロック出来なかったときはサスペンドせずにエラーコード「EBUSY」が返ります。mutexの使用を終えたらdestroyする、という感じです。プログラムがそのまま終了するならdestroyの省略もありでしょう。
pthread_mutexattr_t
は、プライオリティ逆転の回避およびmutexのタイプを変更するために使用します。プライオリティ逆転とmutexのタイプについてはそれぞれ別章で説明します。当レポートではそれ以外の章ではNULL(初期設定値)を設定しています。
また、mutexは下記のように初期化することも可能で、どちらも同じ意味です。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_init( &mutex, 0 );
3.2 サンプル
以上の関数を使ったサンプルが下記です。前回の1.1章で使用した「alarm_thread.c」の修正です。登録されたアラーム情報を構造体に格納してますが、同時にスレッドからは用済みになった時点でアラーム情報構造体から切り離しています。その情報の操作をmutexで守っています。
/* gcc alarm_thread_mutex.c -o alarm_thread_mutex -W -Wall -lpthread */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #define BUF_LEN 256 void * alarm_func( void * arg ); typedef struct alarm_str { int sec; char msg[BUF_LEN]; FILE * fp; pthread_t pt; struct alarm_str * prev; struct alarm_str * next; } alarm_t; alarm_t * head = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int main( int argc, char ** argv ) { alarm_t * alarm; FILE * fp; char line[BUF_LEN]; if( argc < 2 ) return( 1 ); fp = fopen( argv[1], "w" ); while( 1 ) { printf( "Alarm ( sec msg ) --> " ); fgets( line, sizeof( line ), stdin ); if( strlen( line ) <= 1 ) { continue; } if( memcmp( line, "quit", 4 ) == 0 ) break; alarm = malloc( sizeof( alarm_t )); alarm -> fp = fp; if( sscanf( line, "%d %s", &alarm -> sec, alarm -> msg ) < 2 ) { continue; } pthread_mutex_lock( &mutex ); alarm -> next = head; alarm -> prev = 0; if( head ) { head -> prev = alarm; } head = alarm; pthread_mutex_unlock( &mutex ); pthread_create( &alarm -> pt, NULL, &alarm_func, alarm ); pthread_detach( alarm -> pt ); } pthread_mutex_destroy( &mutex ); fclose( fp ); return 0; } void * alarm_func( void * arg ) { alarm_t * alarm = ( alarm_t * )arg; alarm_t * current = 0; int cnt; for( cnt = 0; cnt < alarm -> sec; cnt ++ ) { flockfile( alarm -> fp ); fprintf( alarm -> fp, "[%d] (%d) %s\n", alarm -> sec, cnt, alarm -> msg ); fflush( alarm -> fp ); funlockfile( alarm -> fp ); sleep( 1 ); } pthread_mutex_lock( &mutex ); for( current = head; current; current = current -> next ) { if( current -> pt != pthread_self( )) { continue; } if( current -> next != 0 ) { current -> next -> prev = current -> prev; } if( current -> prev != 0 ) { current -> prev -> next = current -> next; } if( head == current ) { head = current -> next; } break; } free( current ); pthread_mutex_unlock( &mutex ); return 0; }
3.3 注意点
これは全同期処理で言えることですが、前述したようにmutexがロックされている間、そのmutexをロックしたいスレッドはサスペンドします。ロックとアンロックの処理順序等によってはデッドロックが発生する事を常に考慮し、回避しなくてはいけません。
また、ロックしてからの処理に多くの時間が必要である場合、その間他のスレッドがサスペンドしているわけですから、トータルの処理時間は長くなってしまいます。同期するべき箇所を見極め、mutexによるロック対象はなるべく狭めるべきです。