コンテンツにスキップ

Top

C言語でpthreadを使ってスレッド処理(非同期処理)をする

C言語で非同期処理をしようと思った場合、pthread か OpenMP ぐらいしか選択肢はない。
(他なんかあるなら知りたい)

ここではpthreadをつかった簡単な非同期処理の実装方法について述べる。

2つのループ処理を順番に実行する

まずはスレッドを使わず。

1~5を順番に表示するforループを2回順番に実行してみる。

sample1.c

#include <stdio.h>

void func() {
    int i = 0;
    for (i = 1; i <= 5; i++) {
        printf("%d\n",i);
    }
}

int main()
{
    func();
    func();
    return 0;
}

$ gcc -o sample1 sample1.c
$ ./sample1
1
2
3
4
5
1
2
3
4
5

まぁ、期待通りというかそのままでしかない結果。
これをpthreadを使って並列実行しよう!

2つのループ処理を並列に実行する

ではこのforループ処理をそれぞれスレッドで実行してみる。

さっきのプログラムに以下を追加する。

①pthread.hをインクルード
②スレッド実行する関数の引数と戻り値の型をpthreadのお作法に合わせて変更
③pthred_t変数を定義する
④pthreadを生成する(pthread_create)
⑤pthreadの待ち合わせをする(pthread_join)

あとはコンパイル時に -pthread をリンクしないといけない、ぐらい。

pthread1.c

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

void* func(void* ptr) {
    int i = 0;
    for (i = 1; i <= 5; i++) {
        printf("%d\n",i);
    }
}

int main()
{
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, func, NULL);
    pthread_create(&thread2, NULL, func, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

$ gcc -o pthread1 pthread1.c -pthread
$ ./pthread1
1
2
3
4
5
1
2
3
4
5

ファッ!?

いやいやいやいや。そんな馬鹿な。
ごちゃまぜに出力されるはずなのに・・・。

と思ったが、1~5はさすがに数が少なすぎた。
100まで増やしたらバラバラになった。
良かった。

スレッド実行する関数に引数を渡す

それぞれのスレッドかどうかよくわかんないので引数で値を渡して表示するように変更。
pthread_createの第4引数が、スレッド実行される関数の引数になる。

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

ちなみに第2引数はスレッド属性というが、使ったことない。常にNULL設定している。
仮に使うとしたらスレッドの優先順位だろう。何も設定しないと親と同じ優先順位になるので、こっちのスレッドちょっと強めに、とかしたい場合は変えたりすることもあるかもしれないしないかもしれない。

で、値渡し。最初のスレッドには"A"を2番目のスレッドには"B"を渡して表示する。

pthread2.c

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

void* func(void* ptr) {
    int i = 0;
    char* str = (char*)ptr;
    for (i = 1; i <= 5; i++) {
        printf("%s=%d\n",str,i);
    }
}

int main()
{
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, func, (void*)"A");
    pthread_create(&thread2, NULL, func, (void*)"B");

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

$ gcc -o pthread2 pthread2.c -pthread
$ ./pthread2
A=1
A=2
A=3
A=4
A=5
B=1
B=2
B=3
B=4
B=5

うん。ぜんぜんスレッド感ない。
だから、数が少ないとこうなるわな。
でもすごい量はここに貼りきれないので、実際やって確認して。
ループ回数を100とか大きい値にしたら以下みたいにABが交互になり始めると思うから。
(PCの性能次第だとは思うけど)

$ ./pthread2
A=1
A=2
A=3
A=4
A=5
A=6
A=7
A=8
A=9
A=10
A=11
B=1
B=2
B=3
B=4
B=5
B=6
B=7
B=8
B=9
B=10
B=11
A=12
A=13
A=14
...

スレッド実行する関数から戻り値を受け取る

スレッドで呼び出される関数の中でreturnすると、その値がpthread_joinの第二引数に格納されて戻ってくる。
わかりにくい。戻り値で帰って来いよ。。。

さらに、関数の中のauto変数をそのまま返してよいわけではない。アドレスが危険なので。
よってmallocなどでメモリを割り当ててreturnする。受け取った側は当然freeしてやる必要がある。

pthread3.c

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

void* func(void* ptr) {
    int i = 0;
    char* str = (char*)ptr;
    void* ret;

    for (i = 1; i <= 5; i++) {
        printf("%s=%d\n",str,i);
    }

    ret = malloc(sizeof(int));
    if (0 == strcmp(str,"A")) {
        *(int*)ret = 0;
    } else {
        *(int*)ret = 999;
    }

    return ret;
}

int main()
{
    void *ret1, *ret2;
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, func, (void*)"A");
    pthread_create(&thread2, NULL, func, (void*)"B");

    pthread_join(thread1, &ret1);
    pthread_join(thread2, &ret2);

    printf("thread1:%d\n", *(int*)ret1);
    printf("thread2:%d\n", *(int*)ret2);

    free(ret1);
    free(ret2);

    return 0;
}

$ gcc -o pthread3 pthread3.c -pthread
$ ./pthread3

~

thread1:0
thread2:999

うまくいった。

スレッド実行する関数同士で排他をする

それぞれのスレッドが好き勝手動き回られるとそれはそれで困ることもある。
ので、それぞれ交互に実行するようにしてみる(スレッドの意味無しだが、まぁサンプルなので)

排他に使うのは pthread_mutex_t という変数。
これを pthread_mutex_init で初期化して使う。
変数は各々のスレッドから見えるようグローバルな位置で定義する必要があり、使用後は pthread_mutex_destroy で破棄しなければならない。
この排他変数は pthread_mutex_lock をしてから pthread_mutex_unlock をするまでの間、ほかのpthread_mutex_lockをしているスレッドを排他できる。

pthread_mutex.c

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

pthread_mutex_t mutex;

void* func(void* ptr) {
    int i = 0;
    char* str = (char*)ptr;
    void* ret;

    pthread_mutex_lock(&mutex);
    for (i = 1; i <= 100; i++) {
        printf("%s=%d\n",str,i);
    }
    pthread_mutex_unlock(&mutex);

    ret = malloc(sizeof(int));
    if (0 == strcmp(str,"A")) {
        *(int*)ret = 0;
    } else {
        *(int*)ret = 999;
    }

    return ret;
}

int main()
{
    void *ret1, *ret2;
    pthread_t thread1, thread2;

    pthread_mutex_init(&mutex, NULL);

    pthread_create(&thread1, NULL, func, (void*)"A");
    pthread_create(&thread2, NULL, func, (void*)"B");

    pthread_join(thread1, &ret1);
    pthread_join(thread2, &ret2);

    printf("%d\n", *(int*)ret1);
    printf("%d\n", *(int*)ret2);

    free(ret1);
    free(ret2);

    pthread_mutex_destroy(&mutex);

    return 0;
}

$ gcc -o pthread_mutex pthread_mutex.c -pthread
$ ./pthread_mutex
A=1
A=2
A=3
A=4
A=5
B=1
B=2
B=3
B=4
B=5
0
999

・・・いや、今までと変わらんし。だから、5件じゃだめなんよ。件数少なすぎ。
でも100件とかにしたら変わるから実際にやってみてね!

各スレッドごとにCPUを割り当てる(Linux)

せっかくスレッドで動いていて、CPUも複数あるのであれば、それぞれのCPUをそれぞれのスレッドに割り当てたほうが処理も高速化されて良い。
ので、スレッド1、スレッド2にそれぞれ、CPU0、CPU1を割り当てる。(CPU番号は0番から)
この割り当てることを CPUアフィニティ という。
pthread_setaffinity_npという関数でわりあてるが、これはLinuxのみの機能みたい。

実際に割り当たっているかどうかは今までのサンプルだと全くわからんので、まずはスレッド内の処理を無限ループに変更。
CPU番号を変えながら実行して、htopで使われるCPUがわかるのを確認してみよう。
注意として、pthread.hのインクルードより上に #define _GNU_SOURCE を定義しておかないとダメ。

pthread_affinity.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

void* func(void* ptr) {
    char* str = (char*)ptr;

    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    if (0 == strcmp(str,"A")) {
        CPU_SET(0, &cpuset);
    } else {
        CPU_SET(1, &cpuset);
    }
    pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);

    while (1) {
    }
}

int main()
{
    cpu_set_t cpu_set;

    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, func, (void*)"A");
    pthread_create(&thread2, NULL, func, (void*)"B");

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}

$ killall pthread_affinity
$ ./pthread_affinity &
$ htop

ちなみにmainのCPU番号を変えたい場合は、main関数の中で自身のthreadをpthread_self()で受け取って設定すればよい。
サンプルコードは以下。

int main() {

~

    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(0, &cpuset);
    pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);

以上。