本ページはプロモーションが含まれています

Linuxスレッド pthread


スレッドについて

UNIXやLinux、WindowsなどのほとんどのOSはマルチタスクなOSです。マルチタスクとは、複数の処理を並列に同時進行で動作させることであり、「コンテキストスイッチ」と呼ぶ、CPU資源を並列するタスクに高速に切り替えながら割り当てる方法でにより実現しています。それらのOSはプログラムを「プロセス」という単位でメモリ上にロードして実行します。そして複数のプロセスは、マルチタスクのしくみによる「マルチプロセス」として並列に動作します。並列実行する各プロセスには、それぞれに独立したメモリ空間が配分され、ソケットなどのプロセス間通信により互いに協調して動きます。
Picture
さらにひとつのプロセスの中で、複数のタスクを並列動作させるしくみがあります。これをマルチスレッドと呼び、そのプロセス内のタスクのことを「スレッド」と呼びます。プロセスが各々独立したプログラムとして論理的なメモリ空間が独立しているのに対し、スレッドは単一プロセス内でメモリ空間を共有しているので、変数スコープの規則どおりにアクセスできます。
Picture
ここではC言語のスレッドライブラリpthreadのAPI関数の使い方について解説します。「pthread」は、UNIX系OSで標準化されている「POSIX標準」に準拠して実装される、スレッドを扱うC言語API関数群です。
pthreadライブラリは、C/C++の開発環境にほとんどの場合標準で含まれています。pthreadライブラリを使用するには、次のようにリンカオプションを指定します。
-lpthread (GNUのgccでは-pthreadでもよい)
そして、C言語ソースではヘッダファイルpthread.hincludeします。
#include <pthread.h>

スレッドの生成

pthread_create()は、任意の関数をスレッドとして生成します。
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void * arg);
thread スレッド識別子。変数のポインタを指定し、スレッド生成後に格納される。
attr スレッドの属性(未使用時はNULLを指定)
start_routine スレッド関数。void*の引数と戻り値の関数ポインタを指定する。
void * arg スレッド関数へ渡す引数(未使用時はNULLを指定)
次の例は、関数th_func()をスレッドとしてメイン関数から生成しています。スレッド処理は、1秒間の待機の前後に実行開始と実行終了を示す文字列を出力します。また、メイン関数ではスレッド生成直後に2秒間の待機の前後に表示を出力します。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void *th_func(void *arg) {
    puts("thread created");
    sleep(1);
    puts("exit thread");
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        puts("Main thread");
        sleep(2);
        puts("exit main");
    }
}
これを実行すると次のような出力が現れます。メイン関数が2秒間待機している間に、並行してスレッドの関数th_func()が実行されたことがわかります。
Main thread
thread created
exit thread
exit main

動作中のプロセスIDとスレッドIDを確認してみる

スレッドは、本体のプロセスの中で動くものですが、psコマンドに-Lオプションをつけると動いているスレッドが確認できます。次のようにメインとスレッドを長時間待機させるようにしてその間ににpsコマンドで観察してみます。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void *th_func(void *arg) {
    puts("thread created");
    sleep(20);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        puts("Main thread");
        sleep(30);
    }
}
先頭の数値はPIDでその次がスレッドIDです。IDの値はその時々によりますが、スレッド実行中は同じプロセスで2つのスレッドIDが現れます。10834が生成されてたスレッドのようです。
$ ps -aL | grep pthertest
  10833   10833 pts/2    00:00:00 pthertest
  10833   10834 pts/2    00:00:00 pthertest
  - 20秒後 -
  10833   10833 pts/2    00:00:00 pthertest

スレッドの終了

スレッドの終了は、そのスレッドの関数がreturnで戻るときか、スレッドの関数の中からpthread_exit()を呼び出したときです。pthread_exit()は、スレッドのどの場所からでも呼び出した時点でスレッドが終わります。
void pthread_exit(void *retval);
retval スレッドの戻り値
引数には生成元へ返す戻り値を指定できます。戻り値を返さない場合はNULLを指定します。戻り値は後述のpthread_join()で取得します。
次の例は、スレッド「th_func()」の中で呼び出す関数func()の中でpthread_exit()を呼び出すことで、th_func()に戻らずにその場でスレッドを終了させています。そのため、returnの前の「return thread」は表示されません。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void func()
{
    puts("thread created");
    pthread_exit(NULL);
}

void *th_func(void *arg) {
    func();
    puts("return thread");
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        puts("Main thread");
        sleep(1);
    }
}
Main thread
thread created

スレッドの終了の待ち合わせ

プロセス本体が終了すると、スレッドもその時点で消滅します。
スレッドを生成した側のメインスレッドが先に終了して、同時にスレッドが意図しないタイミングで強制終了すると想定しない状況に陥るかもしれません。ここまでの例では、メイン関数側がスレッドの処理時間よりも十分に長い時間を待機させるようにしてそれを避けていましたが、メイン関数側は、生成したスレッドの終了するタイミングの長短にかかわらずスレッドの終了を待ち合わせるべきでしょう。
pthread_join()は、生成したスレッドがreturnpthread_exit()で終了するまで待機します。pthread_join()の関数定義は次のとおりです。
int pthread_join(pthread_t th, void **thread_return);
thread スレッド識別子
thread_return スレッドの関数戻り値の格納先(不要な場合はNULL)
次の例は、生成したスレッドが3秒後に処理を終了して戻るまで、生成元がpthread_join()で待機します。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void *th_func(void *arg) {
    puts("thread created");
    sleep(3);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        puts("Main thread");
        pthread_join(th, NULL);
        puts("Main exit");
    }
}
メイン関数の終了を示す「Main exit」は、スレッドが終了する3秒後に表示されます。
Main thread
thread created
 - 3秒 -
Main exit
またpthread_join()は、呼び出した時点で既にスレッドが終了していた場合は何も待たずに返ります。

スレッドの戻り値の取得

スレッド関数がreturnで返す戻り値は、スレッド呼び出し側のpthread_join()で受け取ることができます。pthread_join()の呼び出し前にスレッドが終了しても、スレッドの戻り値は消滅せずに保持されます。すなわち、正確にはスレッドは関数を出たときに消滅するのではなく、pthread_join()を生成側で呼び出した時点で戻り値を含めたスレッドのリソースがすべて消滅するのです。
pthread_join()の第2引数には、スレッドが戻り値を返さない、あるいは戻り値の取得が不要な場合はNULLを指定します。戻り値を取得するときは、任意のポインタ変数のポインタを指定します。
次の例では、スレッドがint型のポインタを返し、メインでpthread_join()の第2引数でそのポインタを取得しています。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void *th_func(void *arg)
{
    int *a;
    printf("thread created\n");
    sleep(3);
    a = (int *)malloc(sizeof(int));
    *a = 123;
    return (void *)a;
}

int main(int argc, char *argv[])
{
    pthread_t th;
    int *r;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        printf("Main thread\n");
        pthread_join(th, (void **)&r);
        printf("thread return %d\n", *r);
        free(r);
    }
}
戻り値の変数はスレッド関数で動的にメモリ確保したので、メイン側でfree()で開放しています。
Main thread
thread created
thread return 123
pthread_exit()に戻り値を与えて返すこともできます。次の例は、スレッドからreturnでなくpthread_exit()で戻り値を返しています。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void *th_func(void *arg)
{
    int *a;
    printf("thread created\n");
    sleep(3);
    a = (int *)malloc(sizeof(int));
    *a = 123;
    pthread_exit((void *)a);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;
    int *r;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        printf("Main thread\n");
        pthread_join(th, (void **)&r);
        printf("thread return %d\n", *r);
        free(r);
    }
}
戻り値の受け取り方はpthread_join()で同様にします。
Main thread
thread created
thread return 123

スレッド関数への引数

スレッドの関数の引数はvoid型ポインタです。任意のデータのポインタをpthread_create()の第4引数に与えます。次の例では、スレッドに文字列を渡しています。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void *th_func(void *arg)
{
    printf("thread arg %s\n", (char *)arg);
    sleep(3);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;
    char *s = "ABCDE";

    if (pthread_create(&th, NULL, th_func, (void *)s) == 0) {
        puts("Main thread");
        pthread_join(th, NULL);
    }
}
Main thread
thread arg ABCDE

スレッドの切り離し

スレッドの生成元がスレッドの終了を待ち合わせなくても問題ない、あるいは必要がない場合、pthread_join()を呼び出さなくてもスレッドは自ら終了しますが、その場合スレッドのリソースが回収されずに残ってしまいます。生成されたスレッドが、生成元と切り離して自らのタイミングで終了してかまわない場合、pthread_create()の後にpthread_detach()を呼び出すことで、そのスレッドは終了と同時にリソースも自動的に消滅します。
次の例では、生成したスレッドをすぐにpthread_detach()で切り離しています。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void *th_func(void *arg) {
    puts("thread created");
    sleep(3);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        pthread_detach(th);
        puts("Main thread");
        sleep(5);
        if (pthread_join(th, NULL) != 0)
            puts("Error join");
    }
}
スレッドが切り離されて実行し終了した後にpthread_join()で待ち合わせても、スレッドはすでに完全に消滅しているのでエラーとなります。
Main thread
Main thread
thread created
Error join

スレッド属性・スレッドの切り離し

pthread_create()の第2引数にはスレッドの属性を指定しますが、たいていはデフォルトとしてNULLとします。ここに明示的に属性を指定することで、スレッドの振る舞いを調整することができます。
スレッドの属性は、pthread_attr_t構造体に属性値を設定した変数を、pthread_create()の引数に与えます。属性の変数を使用するときは次の初期化と破棄の関数を使います。
int pthread_attr_init(pthread_attr_t *); スレッド属性の初期化
int pthread_attr_destroy(pthread_attr_t *); スレッド属性の破棄
上記にpthreas_detach()による生成スレッドをpthread_join()で待ち合わせずに切り離す方法を説明しました。それと同じことをスレッドの属性の設定できます。スレッド属性を設定する関数は、属性の種類によりいくつかあります。スレッドの切り離しの設定は次の関数を使います。
int pthread_attr_setdetachstate(pthread_attr_t *, int); スレッドの切り離しの設定
この関数の第2引数にPTHREAD_CREATE_DETACHEDを指定してスレッドの切り離しを属性変数に設定します。
次の例は、上のpthread_detach()の例と同じことを、スレッド属性の設定に置き換えたものです。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

pthread_mutex_t mu;

void *th_func(void *arg) {
    printf("thread created\n");
    sleep(3);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;
    pthread_attr_t attr;
    int policy;
    int i;

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (pthread_create(&th, &attr, th_func, NULL) == 0) {
        printf("Main thread\n");
        sleep(5);
        if (pthread_join(th, NULL) != 0)
            puts("Error join");
    }
    pthread_attr_destroy(&attr);
}
生成したスレッドを切り離したので、pthread_join()はエラーになります。
Main thread
thread created
Error join

スレッドの強制停止

pthread_cancel()により、実行中のスレッドを、生成した側のスレッドから強制的に停止させることができます。次の例では、スレッドの中で3秒待機するところを、約2秒の時点でメイン関数側から強制停止させています。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void *th_func(void *arg)
{
    puts("thread created");
    sleep(3);
    puts("thread exit");
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        puts("Main thread");
        sleep(2);
        pthread_cancel(th);
        pthread_join(th, NULL);
    }
}
その結果、th_func()が戻る直前の「thread exit」が表示されません。sleep(3)の間に強制停止したからです。
Main thread
thread created
強制停止ではリソースが消去されないのでpthread_join()を呼び出す必要があります。
スレッドの処理のタイミングを考慮せずにpthread_cancel()を行うと、その後が予測できない状況になる可能性があります。通常のスレッドを終わらせる手段としては適当ではありません。無限ループなどの異常に陥ったスレッドを回収するなど、使用する場面はよく吟味すべきです。



排他制御(ミューテックス)

共有する変数の排他

スレッドはプロセス内で動くマルチタスクなので、メモリを共有しています。つまり関数をスレッドとして動作させても、変数のスコープは通常の関数と同様です。次のように、関数の外に宣言する静的変数には、スレッドの生成元のメイン関数からも、スレッドの関数からも参照できます。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

static char *s;

void *th_func(void *arg)
{
    printf("thread ref %s\n", (char *)s);
    sleep(3);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    s = "VWXYZ";
    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        puts("Main thread");
        pthread_join(th, NULL);
    }
}
こうするとスレッド関数th_func()の引数に渡さなくてもスレッドとのデータの取引ができます。
Main thread
thread ref VWXYZ
スコープ範囲の変数をスレッド間で共有できることは便利ですが、一方で、並列動作するスレッド間で同一変数を同時に代入などの変更操作をした場合、操作のタイミングが競合し、互いに意図しない状態になることがあります。
次の例は、外部の静的な整数変数cntをスレッドとメイン関数の両方で代入しています。スレッドの処理はcntを「1, 2, 3」と表示するつもりですが、その最中にぶつかるタイミングでメイン関数でその変数を*10にする代入演算が実行されており、スレッド側のカウントアップが途中で邪魔されたようになっています。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

static int cnt = 0;

void *th_func(void *arg)
{
    int i;
    for (i = 0; i < 3; i++) {
        printf("thread cnt=%d\n", ++cnt);
        sleep(1);
    }
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        sleep(1);
        cnt *= 10;
        pthread_join(th, NULL);
        printf("result cnt=%d\n", cnt);
    }
}
thread cnt=1
thread cnt=11
thread cnt=12
result cnt=12
このような場合は「ミューテックス」により排他制御を実装することで解決できます。ミューテックスはpthread_mutex構造体の変数を識別子として使い、次の関数で操作します。
int pthread_mutex_init(pthread_mutex_t *, pthread_mutexattr_t *); ミューテックスの初期化
int pthread_mutex_lock(pthread_mutex_t *)); ロック(実行権の獲得)
int pthread_mutex_unlock(pthread_mutex_t *); アンロック(実行権の放棄)
int pthread_mutex_trylock(pthread_mutex_t *); ロックの試行
int pthread_mutex_destroy(pthread_mutex_t *); ミューテックスの破棄
ミューテックスの変数を複数のスレッドがロック操作したとき、先にロックしたスレッドのみがアンロックまでの間唯一の処理を進行させる権利を獲得できます。ロックで権利を獲得できなかった他のスレッドは、ロック操作の時点でアンロックされるまで待機させられます。これは、単線線路の電車信号やカラオケのマイク、道路工事の片側交互通行などに例えることができます。
Picture
具体的には、準備としてミューテックスの変数をスレッド間で参照できるスコープに宣言し、最初にその変数をpthread_mutex_init()に与えて初期化します。変数には固有の識別情報が与えられます。互いのスレッドの中で、そのミューテックス変数を指定したpthread_mutex_lock()を呼び出したとき、先着で先に呼び出したスレッドのみが、次にpthread_mutex_unlock()を呼び出すまでの間処理を進行でき、その間他のスレッドはpthread_mutex_lock()関数の中で止まって待機します。アンロックされると、次の順で待たされていたスレッドのpthread_mutex_lock()関数が返り、実行が再開します。
次の例は、前の例をミューテックスを使って改善したものです。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

pthread_mutex_t mu;
static int cnt = 0;

void *th_func(void *arg)
{
    int i;
    pthread_mutex_lock(&mu);
    for (i = 0; i < 3; i++) {
        printf("thread cnt=%d\n", ++cnt);
        sleep(1);
    }
    pthread_mutex_unlock(&mu);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    pthread_mutex_init(&mu, NULL);
    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        sleep(1);
        pthread_mutex_lock(&mu);
        cnt *= 10;
        pthread_mutex_unlock(&mu);
        pthread_join(th, NULL);
        printf("result cnt=%d\n", cnt);
    }
    pthread_mutex_destroy(&mu);
}
スレッドのカウントアップの最中はミューテックスにより排他できたので、期待した結果になりました。
thread cnt=1
thread cnt=2
thread cnt=3
result cnt=30

ミューテックスの属性・同一スレッドの連続ロック

pthread_mutex_init()の第2引数にはミューテックスの属性を指定しますが、たいていはデフォルトとしてNULLとします。ここに明示的にいくつかの属性を指定することで、ミューテックスの振る舞いを調整することができます。
ミューテックスの属性は、pthread_mutexattr_t構造体に属性値を設定した変数を、pthread_mutex_init()の引数に与えます。ミューテックスの属性は次の関数を使います。
int pthread_mutexattr_init(pthread_mutexattr_t *); ミューテックス属性の初期化
int pthread_mutexattr_settype(pthread_mutexattr_t *, int); ミューテックス属性の設定
int pthread_mutexattr_destroy(pthread_mutexattr_t *); ミューテックス属性の破棄
属性にPTHREAD_MUTEX_RECURSIVE_NPを指定すると、デフォルトでは同一スレッド内で連続してロックすれば二度目のロックで普通通り待機になるところを、そのまま通り抜けることができます。ただし連続させたロックの回数だけアンロックを呼び出さないとアンロックされません。これは属性の定数名から連想できるように関数の再帰で役に立ちます。スレッドの中で再帰関数を呼び出し、その中でミューテックスのロックを行っている場合、再帰呼び出しによる自らの再ロックで止まることなく再帰を続けられます。
属性の変数はミューテックスの変数と同様に使用前の初期化して使用後に破棄します。次の例は、スレッドの中で1〜nの整数の和を求める再帰関数の中でミューテックスを使っています。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

pthread_mutex_t mu;
pthread_mutexattr_t muattr;
static int sum = 0;

int recsum(int x)
{
    int ret = 0;
    pthread_mutex_lock(&mu);
    sleep(1);
    if (x > 0)
        ret = x + recsum(x - 1);
    pthread_mutex_unlock(&mu);
    return ret;
}

void *th_func(void *arg)
{
    sum = recsum(5);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    pthread_mutexattr_init(&muattr);
    pthread_mutexattr_settype(&muattr, PTHREAD_MUTEX_RECURSIVE_NP);
    pthread_mutex_init(&mu, &muattr);
    //pthread_mutex_init(&mu, NULL);
    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        sleep(1);
        pthread_mutex_lock(&mu);
        pthread_join(th, NULL);
        printf("result sum=%d\n", sum);
        pthread_mutex_unlock(&mu);
    }
    pthread_mutexattr_destroy(&muattr);
    pthread_mutex_destroy(&mu);
}
ミューテックスの属性にPTHREAD_MUTEX_RECURSIVE_NPを設定しているので、スレッドの再帰関数は止まらずに実行されます。
result sum=15

ロックの試行

pthread_mutex_trylock()は、ミューテックスが既にロックされているかどうかを確認し、まだロックされていなければ、pthread_mutex_lock()と同様にロックを行い、すでにロックされていればロックせずにEBUSYを返して処理を継続します。上記はミューテックスの属性を設定して連続ロックを回避した例でしたが、pthread_mutex_trylock()でも同一スレッドの多重ロックを回避できます。
次は、上のスレッドの再帰関数の例を、pthread_mutex_trylock()で置き換えたものです。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

pthread_mutex_t mu;
static int sum = 0;

int recsum(int x)
{
    int ret = 0;
    int trylock;
    if ((trylock = pthread_mutex_trylock(&mu)) == EBUSY)
        puts("mutex locked");
    sleep(1);
    if (x > 0)
        ret = x + recsum(x - 1);
    if (trylock == 0)
        pthread_mutex_unlock(&mu);
    return ret;
}

void *th_func(void *arg)
{
    sum = recsum(5);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    pthread_mutex_init(&mu, NULL);
    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        sleep(1);
        pthread_mutex_lock(&mu);
        pthread_join(th, NULL);
        printf("result sum=%d\n", sum);
        pthread_mutex_unlock(&mu);
    }
    pthread_mutex_destroy(&mu);
}
mutex locked
mutex locked
mutex locked
mutex locked
mutex locked
result sum=15

条件によるミューテックスの実行制御

ミューテックスのロック・アンロックによる処理区間の排他制御に加え、スレッド間の通知により他方のスレッドの待機を解除するような直接的な実行制御ができます。スレッドの待機が、解除の通知が届いたという条件により解除され実行が再開するのです。
条件は、pthread_cond_t構造体の変数を識別子として使い、次の関数を使用します。
int pthread_cond_init(pthread_cond_t *, pthread_condattr_t *); 条件変数の初期化
int pthread_cond_signal(pthread_cond_t *); 待機中スレッドの再開を通知
int pthread_cond_wait(pthread_cond_t *, pthread_mutex_t *); 再開通知まで待機
int pthread_cond_timedwait(pthread_cond_t *, pthread_mutex_t *, struct timespec *); 再開通知まで待機(タイムアウト)
int pthread_cond_destroy(pthread_cond_t *cond); 条件変数の破棄
これらの中の待機関数にはミューテックス変数を共に指定します。ミューテックスはロックでなければなりません。指定するミューテックスがロックであり、pthread_cond_signal()による通知が届いた条件で待機が解除されます。
次の例は、上の排他制御の例と同様な3回カウントアップするスレッドの例を、条件を使って置き換えたものです。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

pthread_mutex_t mu;
pthread_cond_t cd;
static int cnt = 0;

void *th_func(void *arg)
{
    int i;
    for (i = 0; i < 3; i++) {
        printf("thread cnt=%d\n", ++cnt);
        sleep(1);
    }
    pthread_cond_signal(&cd); 
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    pthread_mutex_init(&mu, NULL);
    pthread_cond_init(&cd, NULL);
    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        pthread_mutex_lock(&mu);
        pthread_cond_wait(&cd, &mu); 
        cnt *= 10;
        pthread_mutex_unlock(&mu);
        pthread_join(th, NULL);
        printf("result cnt=%d\n", cnt);
    }
    pthread_cond_destroy(&cd);
    pthread_mutex_destroy(&mu);
}
この例は、スレッドではミューテックスの排他は行っていません。そのかわり、スレッド再開の通知を使って明示的にメイン側を待機させています。この例でのミューテックス変数は、区間排他の役割ではなく条件待機のために用意したものです。
thread cnt=1
thread cnt=2
thread cnt=3
result cnt=30

タイムアウト付きの条件による実行制御

pthread_cond_wait()は、通知まで永久に待機し続けます。pthread_cond_timedwait()は、タイムアウトを付けて待機することができます。タイムアウト時間は、gettimeofday()で得られるようなエポック時刻を指定します。「3秒」のような時間幅ではなく「時刻」なので注意が必要です。pthread_cond_timedwait()はタイムアウトしたときETIMEDOUTを返します。
次は、上の例に2秒のタイムアウトをつけたものです。ただし、例のためにわざとタイムアウトが発生するようにしています。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>

pthread_mutex_t mu;
pthread_cond_t cd;
static int cnt = 0;

void *th_func(void *arg)
{
    int i;
    for (i = 0; i < 5; i++) {
        printf("thread cnt=%d\n", ++cnt);
        sleep(1);
    }
    pthread_cond_signal(&cd); 
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;
    struct timespec to;
    time_t now;


    pthread_mutex_init(&mu, NULL);
    pthread_cond_init(&cd, NULL);
    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        pthread_mutex_lock(&mu);
        time(&now);
        to.tv_sec = now + 2;
        to.tv_nsec = 0;
        if (pthread_cond_timedwait(&cd, &mu, &to) == ETIMEDOUT) {
            puts("cond timedout");
            pthread_cond_wait(&cd, &mu);
        }
        cnt *= 10;
        pthread_mutex_unlock(&mu);
        pthread_join(th, NULL);
        printf("result cnt=%d\n", cnt);
    }
    pthread_cond_destroy(&cd);
    pthread_mutex_destroy(&mu);
}
tv_secの代入を10(秒)などとすれば、タイムアウトせずにpthread_cond_timedwait()pthread_cond_wait()と同様に振る舞います。
thread cnt=1
thread cnt=2
cond timedout
thread cnt=3
thread cnt=4
thread cnt=5
result cnt=50

デッドロック

ミューテックスは共有する変数や一連の処理(条件分岐と演算を一気に実行しなければならない箇所など)の、データの種別や処理内容により複数用意してそれぞれ排他制御することができます。このとき注意しなければならないのは、スレッド間で2つ以上のミューテックスを使うときに、互いに別々のミューテックスをそれぞれが同タイミングでロックして、さらにその排他区間内で、互いに逆のミューテックスをロックしようとしたとき、それらのアンロックが、互いに他方を排他している中にあるため どちらもその場で永久に止まってしまう「デッドロック」と呼ばれる状況に陥ることがあります。複数のミューテックスを駆使した複雑なプログラムでは、意図せずにデッドロックの型を作り上げてしまうことがあります。
Picture
次の例は、デッドロックの状態になります。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

pthread_mutex_t mu1;
pthread_mutex_t mu2;

void *th_func(void *arg)
{
    puts("thread wait 1");
    pthread_mutex_lock(&mu1);
    puts("thread lock 1");
    sleep(1);
    puts("thread wait 2");
    pthread_mutex_lock(&mu2);
    puts("thread lock 2");
    sleep(1);
    pthread_mutex_unlock(&mu2);
    pthread_mutex_unlock(&mu1);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t th;

    pthread_mutex_init(&mu1, NULL);
    pthread_mutex_init(&mu2, NULL);
    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        puts("main wait 2");
        pthread_mutex_lock(&mu2);
        puts("main lock 2");
        sleep(1);
        puts("main wait 1");
        pthread_mutex_lock(&mu1);
        puts("main lock 1");
        sleep(1);
        pthread_mutex_unlock(&mu1);
        pthread_mutex_unlock(&mu2);
        pthread_join(th, NULL);
    }
    pthread_mutex_destroy(&mu2);
    pthread_mutex_destroy(&mu1);
}

スレッドの優先度設定

スレッドの並列動作はOSにより適切にスケジューリングされるので、各スレッドの実行時間の優先順位を気にすることは滅多にありませんが、スレッドパラメータを設定することでスレッドの優先度を任意に調整できます。
pthread_setschedparam()に、スレッド識別子と、int型定数のスケジューリング・ポリシーの種類、struct sched_paramに格納するスレッドのパラメータの中の優先度を設定することで、そのスレッドのスケジューリングや優先度を調整できます。また、pthread_getschedparam()では現在の設定が取得できます。スレッド生成元のメインスレッドに対し設定したいときは、pthread_self()により自身のスレッド識別子を得ることができます。
struct sched_paramsched_priorityに優先度を設定します。通常は100未満の整数で、値が大きいほど高優先になります。ポリシーはSCHED_RR(ラウンドロビン)、SCHED_FIFOSCHED_DEFAULTがあり、SCHED_DEFAULTはデフォルトのスケジューリングです。デフォルトスケジューリングのときは、優先度の設定は無視されます。優先度を設定したいときは、ポリシーにSCHED_RRSCHED_FIFOを設定します。
次の例は、メイン関数とスレッドが1msのスリープを3000回(3秒間)ループ回転するだけの処理を、ほぼ同時に実行するもので、メイン関数側の優先度を高く設定しています。互いのスレッドが3000回のループでミューテックスを取り合い、どちらが早くループを3000回終えるかによって優先度の作用を確認します。
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>

pthread_mutex_t mu;

void *th_func(void *arg) {
    int i;
    printf("thread created\n");
    for (i = 0; i < 3000; i++) {
        pthread_mutex_lock(&mu);
        usleep(1000);
        pthread_mutex_unlock(&mu);
    }
    puts("Thread end");
    return NULL;
}

// Policy FIFO:1 RR:2 OTHER:0
// デフォルトはOTHER
// FIFO/RRのときに優先度が設定できる。 

int main(int argc, char *argv[])
{
    pthread_t th;
    struct sched_param param;
    int policy;
    int i;

    pthread_mutex_init(&mu, NULL);
    if (pthread_create(&th, NULL, th_func, NULL) == 0) {
        printf("Main thread\n");
        pthread_getschedparam(pthread_self(), &policy, &param);
        //pthread_getschedparam(th, &policy, &param);
        param.sched_priority = 70;
        pthread_setschedparam(pthread_self(), SCHED_RR, &param);
        //pthread_setschedparam(th, SCHED_RR, &param);
        for (i = 0; i < 3000; i++) {
            pthread_mutex_lock(&mu);
            usleep(1000);
            pthread_mutex_unlock(&mu);
        }
        puts("Main end");
        pthread_join(th, NULL);
        printf("Main exit\n");
    }
    pthread_mutex_destroy(&mu);
}
この結果は、明らかにメイン関数のループが早く終わりました。つまりスレッドより優先されたことがわかります。上のコメントアウトの部分を逆にスレッドに優先度を設定するようにしたら、結果も逆になります。
Main thread
thread created
Main end
- 5秒くらいの長い間 -
Thread end
Main exit