kakts-log

programming について調べたことを整理していきます

C: Pthreadによるスレッド作成とデタッチ

概要

CにおいてPthread(POSIX Thread library)ライブラリを使い、pthread_create()によりスレッドの作成をした後、別のスレッドからpthread_join()を実行し、作成したスレッドの終了を待ち、終了状態を得ることが可能です。

場合によっては、作成したスレッドの終了状態を得る必要がない場合もあります。
この場合はスレッドの終了状態が残り続けるよりも、システム側が終了したスレッドを自動的に破棄する方がよいです。
そこで、pthread_detach()を実行することで、スレッドをデタッチ、つまりスレッドの終了状態を破棄することができます。

ここでは、Pthreadのpthread_create()を用いた基本的なスレッド作成方法について説明した上で、pthread_join()によって指定したスレッドの終了を待つ方法や、pthread_detach()を実行した際に、別スレッドからpthread_join()を実行しても終了状態が取れなくなることを確認します。

pthread_create()によるスレッドの作成

pthread_create()について

pthread_create()により、実行したプロセス内でスレッドを新規作成します。

pthreadライブラリでの関数定義は下記のとおりです。 詳細はリンクを確認ください。
Ubuntu Manpage: pthread_create - create a new thread

       #include <pthread.h>

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

新規作成されたスレッドは、第3引数のstart_routineで指定した関数を実行します。 第4引数に、start_routineの関数の引数を指定します。

スレッドの作成例

ここではシンプルに、pthread_create()を実行してスレッドを作成する例を示します。

#include <pthread.h>
#include <stdio.h>

static void * threadFunc(void *arg)
{
    char *s = (char *) arg;
    printf("%s", s);
    return (void *) strlen(s);
}

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

    int s; 

    // スレッドの作成
    // 作成されたスレッドは第三引数の関数に第4引数の値を渡して実行する
    s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");

    // スレッド作成失敗
    if (s != 0) {
        printf("pthread_create failed\n");
        return -1;
    }
    printf("pthread_create succeeded\n");
    return 0;
}

基本的なスレッドの作成方法はこのとおりです。

スレッドの終了を待つ。

前項で紹介したコードは、main()でスレッドを作成して、指定した関数を実行したシンプルな処理となります。
fork()におけるwait()のように、スレッドにおいても、作成したスレッドの終了を待って、終了状態を得るためのpthread_join()関数が用意されています。
作成されたスレッドとは別のスレッドから実行することで、該当のスレッドの終了を待つことができます。

pthread_join()について

pthread_join()を別のスレッドで実行することで、指定したスレッドの終了を待って、終了状態を得ることができます。

Ubuntu Manpage: pthread_join - join with a terminated thread

       #include <pthread.h>

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

第1引数 threadに、終了を待つ対象のスレッド情報を渡します。 第2引数には、 スレッドによって実行された関数の戻り値を受けるポインタを渡します。

戻り値は、0ならば正常で、それ以外の場合はerrnoを示します。

スレッドの終了後、pthread_join()を実行するまで、システム内部でこの終了したスレッドの情報が残り続けます。 スレッドで実行させる処理によっては、pthread_join()でスレッドの終了を待つ必要がない場合があり、その場合は終了したスレッドの情報は不要となります。

不要な情報が残っているとシステムでリソースを消費してしまうため、スレッド終了時に破棄させるように設定することで、この問題を解決できます。
スレッドの情報を破棄させる方法の1つとして、pthread_detach()の実行があります。

pthread_detach()について

pthread_detach()は、スレッドに割り当てられるリソースが、終了時に回収可能であることを知らせるために使われます。

Ubuntu Manpage: pthread_detach - detach a thread

       #include <pthread.h>

       int pthread_detach(pthread_t thread);

引数に、対象のスレッドの情報を渡します。

前述したように、スレッド終了後に他のスレッドが終了状態を必要としない場合に使用するべきものです。

スレッドのデタッチする・しない場合の挙動について

ここで、スレッドを作成後、スレッドをデタッチした場合とそうでない場合にどういう挙動になるかを説明します。

例1 スレッドをデタッチせず、pthread_join()を使った場合

こちらはスレッドを利用した際に、終了状態を待つ必要がある場合の一般的な例となります。

大まかな流れは下記のとおりです。

  1. メインスレッドでptheread_create()を実行し、スレッドを作成
  2. 作成されたスレッドではpthread_create()で指定した関数を実行し、終了する
  3. メインスレッド側で2のスレッドの終了をpthread_join()を実行して待つ

この流れで、3でメインスレッド側でpthread_join()を実行し、スレッドが終了するまで待ち、スレッドの終了状態を取得します。


#include <pthread.h>
#include <stdio.h>

static void * threadFunc(void *arg)
{
    char *s = (char *) arg;
    printf("%s", s);
    return (void *) strlen(s);
}

int main(int argc, char *argv[])
{
    pthread_t t1;
    void *res = NULL;

    int s; 

    // スレッドの作成
    // 作成されたスレッドは第三引数の関数に第4引数の値を渡して実行する
    s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");

    // スレッド作成失敗
    if (s != 0) {
        printf("pthread_create failed");
        return -1;
    }
    // 作成したスレッドが終了するのを待つ
    sleep(3);
    printf("Message from main()\n");

    // 作成したスレッドの終了を待つ
    // スレッドによって実行された関数の戻り値を&resに格納する
    s = pthread_join(t1, &res);
    if (s != 0) {
        printf("pthread_join failed");
        return -1;
    }

    printf("Thread returned %ld\n", (long) res);
    return 0;
}

main()関数でスレッド作成後、sleep(3)を実行し、作成したスレッドが終了するのを待った後、pthread_join()を実行します。 第2引数に渡したresに関数の実行結果が渡るので、実行後その結果を表示しています。

# ./simple_thread
Hello world
Message from main()
Thread returned 12

スレッドの関数に Hello world\n を渡したので、その分の文字列長を表示できているのがわかります。

例2 自スレッドで終了を待たずにpthread_detach()を実行した場合

作成したスレッド側で、終了前にpthread_detach()を実行させた後、別のスレッドでpthread_join()をした際にどうなるのかを確認します。

例1とは大きくコードは変わらず、pthread_create()で指定した関数内で、終了前にpthread_detach()を実行しています。
自スレッドで実行するため、引数には時スレッドの情報を取得するpthread_self()を渡します。

#include <pthread.h>
#include <stdio.h>

static void * threadFunc(void *arg)
{
    char *s = (char *) arg;
    printf("%s", s);

    int res;
    // 自スレッドをデタッチする
    res = pthread_detach(pthread_self());
    if (res != 0) {
        print("pthread_detach failed");
        return -1;
    }
    
    return (void *) strlen(s);
}

int main(int argc, char *argv[])
{
    pthread_t t1;
    void *res = NULL;

    int s; 

    // スレッドの作成
    // 作成されたスレッドは第三引数の関数に第4引数の値を渡して実行する
    s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");

    // スレッド作成失敗
    if (s != 0) {
        errExitEN(s, "pthread_create");
    }
    sleep(3);
    printf("Message from main()\n");

    // 作成したスレッドの終了を待つ
    // 作成したスレッドが実行する関数内でpthread_detach()を実行しているため
    // ここでエラーが出る
    s = pthread_join(t1, &res);
    if (s != 0) {
        errExitEN(s, "pthread_join");
    }

    printf("Thread returned %ld\n", (long) res);
    exit(EXIT_SUCCESS);
}

この例だと、スレッドで実行する関数でpthread_detach()を実行しており、 メインスレッド側でpthread_join()を実行する際にはすでに作成したスレッドはデタッチされているため、スレッドの終了状態が取れずにエラーとなります。

# ./simple_thread_detach 
Hello world
Message from main()
ERROR [EINVAL Invalid argument] pthread_join

実行結果はこのとおりです。

pthread_create()を実行時にスレッド属性としてデタッチをするように設定もできますが、今回はpthread_detach()によるスレッドのデタッチを行い、デタッチした際の挙動について確認できました。