スレッドと排他処理とデッドロック




キーワード: スレッド、Mutex、デッドロック、pthread

1 プロセスとスレッド

「プロセス」とは実行中のひとつのプログラムの事であり、マルチタスクOSでは異なるプロセスに対してメモリ空間は 別々に割り当てられる。異なるプロセス間では互いのメモリ空間にアクセスすることは基本的に出来ない。

一方、「スレッド」とは一つのプロセス内部で別々に独立して実行される実行単位の事であり、 メモリ空間は各スレッド間で共通となる。通常はプロセスを起動するとスレッドが一つだけ起動 して(これをメインスレッドと呼ぶ) main() 関数に書かれたプログラムが実行される。
なおスレッドの起動方法はOSや言語などの環境によって多少異なる。今回は C言語 と pthread ライブラリを用いてスレッドの学習を行うが、スレッドの基本的な使いかたは環境が異なっても ほぼ共通である。例えば pthread ライブラリではなく、Windows API を用いる場合は CreateThread() 関数を 用いてスレッドを起動する。

さて、ソース 1-1 はメインスレッドとは別にスレッドを一つだけ実行する例であり、 コンパイルするには

g++ -O2 -g -Wall -pthread -o 実行ファイル ソースファイル

を実行する。ここで -O2 は最適化実施、 -g はデバッグモード、 -Wall はすべて警告のメッセージを表示、 -pthread は pthread ライブラリをリンクするという意味のオプションである。 (もし-pthreadでコンパイルできない時は-lpthreadを試すこと)

ソース 1-1

#include <stdio.h>
#include <unistd.h>  // sleep()を定義
#include <pthread.h>

// スレッド
void* thread( void* args )
{	
    int counter = 0;
    while( 1 ){
        printf( "thread : %d\n", counter );
        sleep( 1 ); // スレッド 1 秒停止
        counter++;
    }

    return NULL;
}

// メインスレッド
int main() 
{
    pthread_t th;

    // スレッド作成と起動
    pthread_create( &th, NULL, thread, (void *)NULL );

    // キー入力があるまで待つ
    getchar();

    return 0;
}


ソース 1では thread() という関数を main 関数内の pthread_create() 関数を用いて メインスレッドの子スレッドとして起動している。 thread() は単純に整数型変数 counter をカウントアップするだけの関数で、メインスレッド( つまり main 関数 )は getchar()でキー入力があるまで停止する。ここでメインスレッドが終了すると 子スレッドも停止することに注意せよ。

次のソース 1-2 の例は thread() は counter を10回カウントアップするだけのスレッド であるが、整数型変数 counter をグローバル変数にして main 関数の scanf() で counter の値を変更できるようにしてある。このように、メインスレッドと子スレッドの 間では同じメモリアドレス( この例の場合は &counter )にアクセスすることが出来る。

なお、ソース 1-2 の例では main 関数で scanf() を実行するとメインスレッド、すなわち プロセスはすぐに終了してしまうので pthread_join() 関数で起動したスレッドが終了 するのを待っている。

ソース 1-2

#include <stdio.h>
#include <unistd.h>  // sleep()を定義
#include <pthread.h>

int counter = 0;

// スレッド
void* thread( void* args )
{	
    int i = 0;
    for( i = 0; i < 10; ++i , ++counter ){
        printf( "thread : %d\n", counter );
        sleep( 1 ); // スレッド 1 秒停止
    }

    return NULL;
}

// メインスレッド
int main() 
{
    pthread_t th;

    // スレッド作成と起動
    pthread_create( &th, NULL, thread, (void *)NULL );
    scanf( "%d", &counter );

    // スレッド終了を待つ
    pthread_join( th, NULL );

    return 0;
}

問題 1

ソース 1-1 を実行したときと、main 関数を次のように置き換えて実行したときの違いを観察せよ。
int main() 
{
    thread( NULL );
    getchar();

    return 0;
}

2 Mutexによる排他処理


スレッドを多重起動したときに、それぞれのスレッドが好き勝手にメモリやファイルなどの 資源(リソース)にアクセスすると途中で資源の状態が変化してプロセスが思わぬ誤動作を することがある。そこで、あるスレッドが資源にアクセスしている間は、他のスレッドがその 資源にアクセスできないようにブロックする必要がある。そのようなブロック処理のことを 「排他処理」または「排他制御」と呼び、排他制御機構として Mutex (ミューテクス) が 良く使われている。

Mutexは一種の手形であり、あるスレッドは何らかの資源を使用する前にOSなどから Mutexを取得し、資源を使い終わったらMutexをOSに返却する。もし別のスレッドが Mutexを取得済みの場合は、そのスレッドがMutexを返却するまで待つ。

この、Mutexを取得して返却するまでの間の処理部を「クリティカルセクション」と呼ぶ。 ソース 2 は C 言語と pthread ライブラリ を用いて Mutex を使用する例である。 メインスレッド内の pthread_mutex_init() 関数 でMutexを作成し、スレッド内の pthread_mutex_lock() 関数で Mutex の取得、pthread_mutex_unlock() 関数 mutex を返却している。

なお今回は話題にしないが、Mutexを多重化した「セマフォ」も良く使われるのでチェックしておくこと。

ソース 2

#include <stdio.h>
#include <unistd.h>  // usleep()
#include <pthread.h>

pthread_mutex_t mutex;  // Mutex

// スレッド1
void* thread1( void* args )
{
    // 他のスレッドが mutex を返却するを待つ
    // mutex が返却されたら mutex を取得してブロック
    pthread_mutex_lock( &mutex );
	
    // ここからクリティカルセクション

    int i = 0;
    for( i=0; i < 10; ++i){
        printf( "thread1 - %d\n", i );
        usleep( 1000 );  // スレッドを 1000 マイクロ秒停止
    }
	
    // ここまでクリティカルセクション

    // mutex返却
    pthread_mutex_unlock( &mutex );

    return NULL;
}


// スレッド2
void* thread2( void* args )
{
    // 他のスレッドが mutex を返却するを待つ
    // mutex が返却されたら mutex を取得してブロック
    pthread_mutex_lock( &mutex );
	
    // ここからクリティカルセクション

    int i = 0;
    for( i=0; i < 10; ++i){
        printf( "thread2 - %d\n", i );
        usleep( 2000 );  // スレッドを 2000 マイクロ秒停止
    }
	
    // ここまでクリティカルセクション

    // mutex返却
    pthread_mutex_unlock( &mutex );

    return NULL;
}


// メインスレッド
int main() 
{
    pthread_t th1, th2;
	
    // mutex 作成
    pthread_mutex_init( &mutex, NULL );
	
    // スレッド1,2の作成と起動
    pthread_create( &th1, NULL, thread1, (void *)NULL );
    pthread_create( &th2, NULL, thread2, (void *)NULL );
	
    // スレッド終了を待つ
    pthread_join( th1, NULL );
    pthread_join( th2, NULL );
	
    // mutex 開放
    pthread_mutex_destroy( &mutex );
	
    return 0;
}

問題2

ソース 2 において、Mutexを使用したときと Mutex を使用しないときの違いを観察し、何故このようなことが起きたのか考察せよ。

3 デッドロック


排他処理を正しく行わないとスレッド間で「デッドロック」をおこしてプロセスが停止する場合がある。

例えばソース 3 では mutex1 と mutex2 の二つの Mutex があり、スレッド1では mutex1 を取得した後に mutex2 の開放を待っている。一方スレッド2は mutex2 を取得した後に mutex1 の 開放を待っている。ところが mutex2 は mutex1 が開放されない限り取得することが出来ないので スレッド1もスレッド2も先に進むことが出来ずに停止したままになる。

これがデッドロックであり、ソース 3のような Mutex のたすき掛けが典型的な原因として生じる。

ソース 3

#include <stdio.h>
#include <unistd.h>  // sleep()
#include <pthread.h>

pthread_mutex_t mutex1;  // Mutex
pthread_mutex_t mutex2;  // Mutex

// スレッド1
void* thread1( void* args )
{
    // mutex1 を取得してブロック
    pthread_mutex_lock( &mutex1 );
    printf( "スレッド1が mutex1 を取得しました\n" );

    sleep( 1 );
    printf( "スレッド1が mutex2 の開放を待ってます\n" );

    // 他のスレッドが mutex2 を返却するのを待つ
    // mutex2 が返却されたら mutex2 を取得してブロック
    pthread_mutex_lock( &mutex2 );
    printf( "スレッド1が mutex2 を取得しました\n" );

    // ここからクリティカルセクション

    printf( "スレッド1 実行中\n" );
    sleep( 2 );

    // ここまでクリティカルセクション

    // mutex2 開放
    pthread_mutex_unlock( &mutex2 );
    printf( "スレッド1が mutex2 を開放しました\n" );

    // mutex1 開放
    pthread_mutex_unlock( &mutex1 );
    printf( "スレッド1が mutex1 を開放しました\n" );

    printf( "スレッド1 終了\n" );

    return NULL;
}


// スレッド2
void* thread2( void* args )
{
    // mutex2 を取得してブロック
    pthread_mutex_lock( &mutex2 );
    printf( "スレッド2が mutex2 を取得しました\n" );

    sleep( 1 );
    printf( "スレッド2が mutex1 の開放を待ってます\n" );

    // 他のスレッドが mutex1 を返却するのを待つ
    // mutex1 が返却されたら mutex1 を取得してブロック
    pthread_mutex_lock( &mutex1 );
    printf( "スレッド2が mutex1 を取得しました\n" );

    // ここからクリティカルセクション

    printf( "スレッド2 実行中\n" );
    sleep( 2 );

    // ここまでクリティカルセクション

    // mutex1 開放
    pthread_mutex_unlock( &mutex1 );
    printf( "スレッド2が mutex1 を開放しました\n" );

    // mutex2 開放
    pthread_mutex_unlock( &mutex2 );
    printf( "スレッド2が mutex2 を開放しました\n" );

    printf( "スレッド2 終了\n" );

    return NULL;
}


// メインスレッド
int main() 
{
    pthread_t th1, th2;
	
    // mutex1,2 作成
    pthread_mutex_init( &mutex1, NULL );
    pthread_mutex_init( &mutex2, NULL );
	
    // スレッド1,2の作成と起動
    pthread_create( &th1, NULL, thread1, (void *)NULL );
    pthread_create( &th2, NULL, thread2, (void *)NULL );
	
    // スレッド終了を待つ
    pthread_join( th1, NULL );
    pthread_join( th2, NULL );
	
    // mutex 開放
    pthread_mutex_destroy( &mutex1 );
    pthread_mutex_destroy( &mutex2 );
	
    return 0;
}

問題 3

デッドロックしないように ソース 3 を修正せよ。ただし、以下の条件を課す。

(1) mutex1、mutex2 の2つの mutex を使うこと。

(2) sleep の秒数を変更して mutex の lock、unlock のタイミングを調節しないこと。

(3) クリティカルセクションに入る前に mutex の unlock をしないこと。