本物のC

C の知識:スレッド


プロセスとスレッド

カーネルから固有のメモリ領域を与えられて動いているプログラムをプロセス(process)と呼びます。

またプログラムの処理の流れをスレッド(thread)と言いますが、通常はmainの開始からプログラムの終了に到るまで単一のスレッドで処理が完結します。つまり単一のプロセスの中で単一のスレッドのみが走ります。

一方で OS は以下に説明する様なスレッド制御の為のシステムコールも提供しているので、これらによって同一プロセス内に別の新たなスレッドを走らせる事ができます。

カーネルは多数のスレッドを同時並行的に実行してくれるので、例えば非常に時間の掛かる処理を独立したスレッドで行うと、その処理中にも別のスレッドで入力を受け付ける事が可能になります。

CPU が一つしかない場合は基本的に複数のスレッドを代わる代わる処理する形になりますが、CPU が複数あれば異なるスレッドは別の CPU に担わせる事ができます。

巧く処理を分散させられれば CPU の数に応じて高いパフォーマンスが得られるのですが、同じプロセスの中という事はメモリ空間を共有しているので、メモリアクセスの兼ね合いなどをよく考える必要があります。

また C の規格には定められていない機能なので、コードの移植性(「礼儀作法:移植性」を参照)を保つには特別の労力が必要になります。

ここでは UNIX 系の環境で利用可能な pthreads(POSIX threads)について説明します。pthread.hをインクルードし、コンパイル時には-lpthreadオプションを付加しましょう。(MinGW の場合は MinGW Installation Manager から mingw32-pthreads-w32 と mingw32-libpthreadgc をインストールする必要があります。)

例によってintを返す関数は 0 の場合に成功です。

スレッドの作成

pthread_create関数は新たなスレッドを開始します。

int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*), void *restrict arg);

start_routineで指定した関数を実行するスレッドが開始され、threadにその情報が書き込まれます。

attrはスレッドの設定を指示するものですがNULLも可です。(pthread_attr_tの内容はpthread_attr_で始まる関数群により操作します。)またargstart_routineに渡される引数です。

合流とデタッチ

attrで特に指定しなければ、スレッドは合流可能な状態で始まります。即ちスレッドを作成した側は、pthread_joinにより作ったスレッドの終了を待つ事ができます。

int pthread_join(pthread_t thread, void **value_ptr);

value_ptrには関数の返り値が書き込まれます。

合流可能な状態のスレッドは、たとえstart_routineの処理が終わっても一部の資源(ミューテックスやファイルディスクリプタなど)を解放しません。こうした資源はスレッドがpthread_joinへ合流する事で初めて解放されますが、合流させる気が無い場合はpthread_detachにより元のスレッドから切り離す必要があります。

int pthread_detach(pthread_t thread);

実行中のスレッドをデタッチしてもすぐに終了させたりはしませんが、終了後にはすぐその資源が解放されます。

情報と制御

自スレッドの情報はpthread_selfで得られます。

pthread_t pthread_self(void);

pthread_tの値はpthread_equalによって比較されます。

int pthread_equal(pthread_t t1, pthread_t t2);

exitはどのスレッドで呼んでもプログラムを終了させてしまうので、自スレッドのみを終了する場合はpthread_exitを使います。また他のスレッドを停止させる場合はpthread_cancelを使います。

void pthread_exit(void *value_ptr);
int pthread_cancel(pthread_t thread);

排他制御

並走するスレッドが存在する状況下では、一つ一つの命令毎に他のスレッドの動作が挟まる可能性を考慮しなければなりません。

例えば以下のコードは一つのグローバル変数xを 100 個のスレッドから 10000 回インクリメントするものですが、

#include <stdio.h>
#include <pthread.h>
#define N 100
int x = 0;
void *worker(void *arg)
{
int i;
for (i = 0; i < 10000; i++)
x++;
return NULL;
}
int main(void)
{
int i;
pthread_t thread[N];
for (i = 0; i < N; i++)
pthread_create(&thread[i], NULL, &worker, NULL);
for (i = 0; i < N; i++)
pthread_join(thread[i], NULL);
printf("x = %d\n", x);
return 0;
}

この簡単な処理でさえ予想に反してxの値は 781554 や 652187 などのよく分からない数になります。

何故こうなるかと言えば、各スレッドで実行されるx++;が実は

  1. xの値を読む
  2. 値に 1 足した数をxへ書き込む

というメモリに対する二つの命令からなっている事に起因します。

スレッド 1 とスレッド 2 がほぼ同時にx++;を実行した場合、

x スレッド 1 スレッド 2
10 xを読むと 10 だった
10 xを読むと 10 だった
10→11 xに 11 を書く
11 xに 11 を書く

というシナリオを辿る可能性があります。この場合それぞれのスレッドは値が 10 であるxに 1 足したつもりですが、スレッド 2 が書き込む時には既にxが 11 に書き替わっているので、x++;が二度実行されたにも拘わらずxは 1 しか増えない訳です。

競合状態(race condition)と呼ばれるこうした問題を防ぐには、排他制御(exclusive control)によって一連の処理(上の場合x++;)を他のスレッドの介入から保護する必要があります。

ミューテックス

ミューテックス(mutex)は ptheads が提供する排他制御機構の一つで、表裏が「使用中」「空いています」になっている札の様なものです。「使用中」状態にする操作をロック(lock)と言います。

pthread_mutex_t型の変数を定数PTHREAD_MUTEX_INITIALIZERで初期化する事によりミューテックスが用意され、pthread_mutex_destroyで破棄します。(初期化はpthread_mutex_initでも行えます。)

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_destroy(pthread_mutex_t *mutex);

ミューテックスはpthread_mutex_lockpthread_mutex_trylockによりロックし、同じスレッドからpthread_mutex_unlockを呼ぶ事でロックが解除されます。

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

他のスレッドにより既にロックされていた場合、pthread_mutex_lockはそれが解除されるまで待ちますが、pthread_mutex_trylockは直ちにエラーコードEBUSYを返します。

先のコードのxをミューテックスで保護すると以下の様になります。

#include <stdio.h>
#include <pthread.h>
#define N 100
int x = 0;
pthread_mutex_t x_mutex;
void *worker(void *arg)
{
int i;
for (i = 0; i < 10000; i++)
{
pthread_mutex_lock(&x_mutex);
x++;
pthread_mutex_unlock(&x_mutex);
}
return NULL;
}
int main(void)
{
int i;
pthread_t thread[N];
x_mutex = PTHREAD_MUTEX_INITIALIZER;
for (i = 0; i < N; i++)
pthread_create(&thread[i], NULL, &worker, NULL);
for (i = 0; i < N; i++)
pthread_join(thread[i], NULL);
pthread_mutex_destroy(&x_mutex);
printf("x = %d\n", x);
return 0;
}

参考

pthread日記 - table of contents
pthreads の API 一覧と簡潔な説明。
Man page of PTHREADS(JM Project)
エラーコード等の情報は Linux のマニュアルを見るとよい。
The Open Group Base Specifications Issue 7, 2013 Edition
POSIX の仕様。