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は苦手ですか?わかります。

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

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

C言語のprintfの出力文字を装飾する

C言語プログラムのデバッグをしていると、標準出力と標準エラー出力、もしくはプロセスごとにログの色を変えたり、装飾したいってことがあります。

初回はprintfで出力する文字および背景色の装飾を行います。

f:id:syllobenex:20210725204145p:plain

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

文字の装飾・色・背景色を変える方法

変えたい文字の前にエスケープシーケンスを使って装飾コードを入れれば見え方が変わります。 見え方を元に戻したい場合は、標準コードを入れれば元に戻ります。

文字の装飾

装飾 コード 解説
太文字 \x1b[1m
暗文字 \x1b[2m
下線 \x1b[4m
点滅 \x1b[5m
反転 \x1b[7m 文字色と背景色を反転します
隠れ文字 \x1b[8m 文字が見えなくなります
標準 \x1b[0m
    printf("\x1b[1m 太文字 \x1b[0m\n");
    printf("\x1b[2m 暗文字 \x1b[0m\n");
    printf("\x1b[4m 下線 \x1b[0m\n");
    printf("\x1b[5m 文字の点滅 \x1b[0m\n");
    printf("\x1b[7m 色の反転 \x1b[0m\n");
    printf("\x1b[8m 隠れ文字 \x1b[0m <-ここに文字が隠れています\n");

文字色の変更

装飾 コード 解説
\x1b[40m
\x1b[41m
\x1b[42m
\x1b[43m
\x1b[44m
マゼンタ \x1b[45m
シアン \x1b[46m
ライトグレー \x1b[47m
ダークグレー \x1b[100m
ライトレッド \x1b[101m
ライトグリーン \x1b[102m
ライトイエロー \x1b[103m
ライトブルー \x1b[104m
ライトマゼンタ \x1b[105m
ライトシアン \x1b[106m
標準 \x1b[49m
    printf("\x1b[40m 黒背景 \x1b[49m\n");
    printf("\x1b[41m 赤背景 \x1b[49m\n");
    printf("\x1b[42m 緑背景 \x1b[49m\n");
    printf("\x1b[43m 黄背景 \x1b[49m\n");
    printf("\x1b[44m 青背景 \x1b[49m\n");
    printf("\x1b[45m マゼンタ背景 \x1b[49m\n");
    printf("\x1b[46m シアン景 \x1b[49m\n");
    printf("\x1b[47m ライトグレー背景 \x1b[49m\n");
    printf("\x1b[100m ダークグレー背景 \x1b[49m\n");
    printf("\x1b[101m ライトレッド背景 \x1b[49m\n");
    printf("\x1b[102m ライトグリーン背景 \x1b[49m\n");
    printf("\x1b[103m ライトイエロー背景 \x1b[49m\n");
    printf("\x1b[104m ライトブルー背景 \x1b[49m\n");
    printf("\x1b[105m ライトマゼンタ背景 \x1b[49m\n");
    printf("\x1b[106m ライトシアン背景 \x1b[49m\n");
    printf("\x1b[49m デフォルト背景 \x1b[49m\n");

背景色の変更

装飾 コード 解説
\x1b[30m
\x1b[31m
\x1b[32m
\x1b[33m
\x1b[34m
マゼンタ \x1b[35m
シアン \x1b[36m
ライトグレー \x1b[37m
ダークグレー \x1b[90m
ライトレッド \x1b[91m
ライトグリーン \x1b[92m
ライトイエロー \x1b[93m
ライトブルー \x1b[94m
ライトマゼンタ \x1b[95m
ライトシアン \x1b[96m
標準 \x1b[39m
    printf("\x1b[30m 黒文字 \x1b[39m\n");
    printf("\x1b[31m 赤文字 \x1b[39m\n");
    printf("\x1b[32m 緑文字 \x1b[39m\n");
    printf("\x1b[33m 黄文字 \x1b[39m\n");
    printf("\x1b[34m 青文字 \x1b[39m\n");
    printf("\x1b[35m マゼンタ文字 \x1b[39m\n");
    printf("\x1b[36m シアン文字 \x1b[39m\n");
    printf("\x1b[37m ライトグレー文字 \x1b[39m\n");
    printf("\x1b[90m ダークグレー文字 \x1b[39m\n");
    printf("\x1b[91m ライトレッド文字 \x1b[39m\n");
    printf("\x1b[92m ライトグリーン文字 \x1b[39m\n");
    printf("\x1b[93m ライトイエロー文字 \x1b[39m\n");
    printf("\x1b[95m ライトブルー文字 \x1b[39m\n");
    printf("\x1b[96m ライトマゼンタ文字 \x1b[39m\n");
    printf("\x1b[97m ライトシアン文字 \x1b[39m\n");
    printf("\x1b[39m デフォルト文字色 \x1b[39m\n");

まとめ

注意点として、改行コード(\n)の後に標準コードを入れて背景色を戻した場合、次の行に背景色が残る場合があります。

    printf("\x1b[41m 赤背景 \n\x1b[49m");
    printf("\n");

f:id:syllobenex:20210726223549p:plain

今回紹介している方法はC言語としていますが、bashのターミナルに依存しているのでbashの出力(echo)はもちろん、pythonやその他言語でも同じように文字コードを入れてやれば見え方が変わります。