Re:ADME

とりあえず読んどけって記事がモットーです

並列起動するスレッドの正しい引数の渡し方

pthead_createで同じ関数スレッドを複数並列起動したら引数の値が渡したはずの値になってない! ってことがあります。

pthread_createの引数の渡し方を考えれば分かることなのですが、マルチスレッドプログラミングの経験が浅い頃はハマりました。

まずはよくあるNG事例を元に紹介します。

実施環境:Ubuntu 18.04.5 LTS
コンパイラ:gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

間違った書き方

NGコード

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

// 引数で指定された秒数後にログ出力
void *thread_func(void *arg)
{
    int para = *(int*)arg;
    sleep(*para);
    printf("私は %d 番目のスレッドです\n", *para);
}


int main(void)
{
    pthread_t th[5];
    int i;

    // 各スレッドに0〜4を順に渡しているつもり
    for (i=0; i < 5; i++) {
        pthread_create(&th[i], NULL, thread_func, (void*)&i);
    }

    // スレッドの終了を待つ
    for (i=0; i < 5; i++) {
        pthread_join(th[i], NULL);
    }

    return 0;
}

実行結果

f:id:syllobenex:20210727010531p:plain

1秒ごとに0~4の出力を期待しているのに、不定な値になっています。

スレッドに渡しているのは、変数 i のアドレスです。 変数 i は親がforループを回すたびに書き換わるので、スレッドが変数 i に格納されている値を読もうとしても親が書き換えた後か前の値を参照することになります。

このため、「同じスレッドを複数並列起動し、引数に応じて処理を変える」といったよくあるマルチスレッドのコードを書く時は注意が必要となるのです。

ではどうするか。 回避策は簡単で、スレッドごとに異なるアドレス空間に引数の領域を用意するだけです。

正しい引数の書き方1

まずは一番簡単な方法。 起動するスレッドの個数が決まっているなら、スレッドの数だけ配列を用意するだけです。

OKコード

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

// 引数で指定された秒数後にログ出力
void *thread_func(void *arg)
{
    int para = *(int*)arg;
    sleep(*para);
    printf("私は %d 番目のスレッドです\n", *para);
}


int main(void)
{
    pthread_t th[5];
    int i;
    int arg[5];    // スレッドごとの引数を格納する配列

    // 各スレッドに0〜4を順に渡しているつもり
    for (i=0; i < 5; i++) {
        arg[i] = i;
        pthread_create(&th[i], NULL, thread_func, (void*)&arg[i]);
    }

    // スレッドの終了を待つ
    for (i=0; i < 5; i++) {
        pthread_join(th[i], NULL);
    }

    return 0;
}

実行結果

f:id:syllobenex:20210727010934p:plain

正しい引数の書き方2

次に、mallocで動的にメモリ領域を確保する方法。

マルチスレッドのプログラムを目的とする場合、スレッドの個数が決まっていないことも多いでしょう。 また、スレッドの数が多いならばその分配列を用意するというのも無駄です。 引数の数・サイズが大きくなれば尚更です。

ならば、スレッドを起動する前にメモリ領域を用意してやるだけです。

注意が必要なのは、mallocするなら当然、freeも必要。 しかし、引数に渡し終えたからと言ってスレッドが引数を取得する前にfreeすると元の木阿弥。 スレッド側のローカル変数に代入した後にfreeしてやるなど、工夫が必要です。

OKコード

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

// 引数で指定された秒数後にログ出力
void *thread_func(void *arg)
{
    int para = *(int*)arg;
    free(arg);    // freeを忘れずに!
    sleep(*para);
    printf("私は %d 番目のスレッドです\n", *para);

}


int main(void)
{
    pthread_t th[5];
    int i;

    // 各スレッドに0〜4を順に渡しているつもり
    int *arg;
    for (i=0; i < 5; i++) {
        arg = (int*)malloc(sizeof(int));
        *arg = i;
        pthread_create(&th[i], NULL, thread_func, (void*)arg);
    }

    // スレッドの終了を待つ
    for (i=0; i < 5; i++) {
        pthread_join(th[i], NULL);
    }

    return 0;
}

実行結果

f:id:syllobenex:20210727224220p:plain

まとめ

pthreadやmallocは苦手ですか?わかります。

けど、スレッドプログラミングとメモリの動的確保は相性がいいです。

上のコードをコピペすれば大体はカバーできるかと思うので、実際に試してみてください。