徹底図解!入門者向け!C言語でのマルチスレッドをわかりやすく解説

プログラミングをしていると、処理を並列して実行したいと思うことが出てきます。

処理を並列実行する一つの手段がマルチスレッドです。

このページではまずマルチスレッドについて、プログラミング初心者でも理解できるようにわかりやすく解説し、続いてC言語で pthread を用いたマルチスレッドのプログラムの紹介&プログラムの解説を行います。

特に前半に関してはプログラミング言語関係なく読める内容になっていますので是非読んでみてください!

マルチスレッドとは

まずマルチスレッドがどのようなものであるかのイメージを固めていきます。そしてマルチスレッドのプログラムで、そのイメージを具体的なプログラムに落とし込んでいきたいと思います。

スレッドとは

まずはスレッドから説明します。スレッドとは仕事のことです。

例えばあなたが作るプログラムは1つの仕事であると言えます。四則演算を行うプログラムであれば、四則演算が仕事ですし、画像を拡大縮小するプログラムであれば、画像を拡大縮小することが仕事となります。

スポンサーリンク

CPUのコア

その仕事(スレッド)をするのが CPU のコです。スレッドを仕事として例えるのであれば、コアは仕事をする人です。

コンピュータの脳を司るCPUには複数のコアが搭載されています。つまり仕事をする人が複数います。ですので、同時に複数の仕事をこなすことが可能です。

また、CPU のスペック表を見ると、コア数と一緒にスレッド数が記載されているケースが多いです。こちらのスレッド数は CPU 全体で同時に処理できる仕事(スレッド)の数を表しています。例えばコア数が4つなのにスレッド数が8つの場合は、1つのコアが同時に2つの仕事(スレッド)を処理できることになります。

ただし、スレッド(仕事)1つに対して動作するコアはせいぜい1つだけです。

また、コアは仕事があれば率先して仕事をこなすのですが、仕事がなければ基本的にサボっている状態になります。

もしスレッド(仕事)が1つしか動作していないのであれば、コアが同時に動作するのは1つだけで、他のコアはサボっていることになります。

せっかく手が空いている人がいるのに(コアがあるのに)サボらせているのは勿体無いですよね。

スレッドを複数に分割したものがマルチスレッド

サボっているコアに仕事をさせるために、仕事を小さな単位に分割し、サボっているコアに仕事を割り振るというのがマルチスレッド処理の考え方です。

具体的には、マルチスレッドというのは、仕事(スレッド)を複数の仕事に分割した細分化された仕事です。

仕事(スレッド)が一つしかなければ、その仕事に対して動作するコアは1つのみです。

しかし、仕事さえあればコアは自分から率先して仕事を見つけてその仕事をこなしますので、仕事が複数あれば複数のコアを動作させることができ、並列して仕事をさせることが可能です。

これによりサボっているコアを有効活用することができ、仕事を完了するのに必要な時間を短縮することも可能です。

なので、マルチスレッドプログラミングではまずプログラムで行うメインの仕事を小さな仕事に分割することを考える必要があります。スレッドの設計です。

例えば 1 から 100 までの数字をそれぞれ 2 倍することがメインの仕事なのであれば、

  • 1 から 50 までの数字をそれぞれ 2 倍する
  • 51 から 100 までの数字をそれぞれ 2 倍する

という小さな仕事に分割することができます。

また、画像の回転がメインの仕事であれば、

  • 画像の上半分を回転する仕事
  • 画像の下半分を回転する仕事

という小さな仕事に分割することができます。

このような仕事の分割により、複数のコアが同時にこれらの仕事を並行して実行することができます。

仕事を分割することができれば、この仕事でやることを記述した関数を作成します。

さらにその仕事を実際に生成します。例えば pthread であれば pthread_create 関数によりスレッド(仕事)を生成します。引数に関数へのポインタを渡すことにより、その仕事がどういうものであるかを指定します。また引数に仕事で参照すべきデータを渡したりすることもできます。

仕事を生成すれば、勝手に空いているコアが自分からその仕事を処理(関数を実行)してくれます

ここまで説明したことをまとめておきます。

タイトル
  • マルチスレッドとはスレッド(仕事)を小さな仕事に分割することで、複数のコアで並列動作できるようにするもの
  • スレッド(仕事)の内容・やることは関数として記述する
  • スレッド(仕事)は pthread_create 関数・CreateThread 関数などにより作成できる
  • スレッド(仕事)を作れば、空いているコアが勝手に処理してくれる

マルチスレッドの同期

マルチスレッドの同期とは、他のスレッドとの待ち合わせを行う処理です。

基本的にそれぞれのコアは仕事を独立に自分のペースで行います。しかし、仕事を自分のペースで行われると困るパターンがあります。

例えば鍋料理でこのパターンを考えてみましょう。ここでは鍋料理するというスレッドを「具材を切るスレッド」、「ダシをとるスレッド」、「具材を煮込むスレッド」の3つのスレッドに分割します。

これらのスレッドは、それぞれのコアが独立して自分のペースで処理されてしまうと困ってしまいます。

「具材を切るスレッド」と「ダシをとるスレッド」はそれぞれ並列して行って良いスレッドです。

しかし「具材を煮込むスレッド」は、「具材を切るスレッド」と「ダシをとるスレッド」の両方が終わってから開始する必要があるスレッドです。もし具材が切られていなければ具材をまるまる鍋に入れることになります。また、ダシが取れていない状態で「具材を煮込むスレッド」が開始されると、具材に味が染みなくて出来が悪くなってしまいます。

そこで用いるのが同期です。同期とは前述の通り他のスレッドとの待ち合わせを行う処理です。

同期を用いることで、「具材を煮込むスレッド」を「具材を切るスレッド」と「ダシをとるスレッド」の終了するまで待たせ、それぞれが終了してから煮込みを開始させるようにすることができます。これにより美味しいお鍋を作ることができます。

スポンサーリンク

マルチスレッドのプログラム1(スレッド生成)

ここまでの説明で、マルチスレッドがどのようなものであるかがイメージ出来てきたのではないかと思います。

ここからはマルチスレッドの具体的なプログラムを用いて、そのイメージをもっと具体的なものに落とし込んでいきたいと思います。

このページではC言語で pthreadPOSIXスレッド)を用いたマルチスレッドのプログラムを紹介します。動作確認は Mac OSX で行っていますが、おそらく Linux 等でも動作すると思います。

プログラムの仕様

このプログラムは0から39までの40個の整数の4乗を計算し、結果を配列に格納するプログラムになります。つまり、「0から39までの40個の整数の4乗を計算し、結果を配列に格納する」がメインのスレッド(仕事)になります。

シングルスレッドのプログラム

マルチスレッドのプログラムと対比するためにまずはシングルスレッドのプログラムを載せておきます。

singlethread.c
#include <stdio.h>
  
#define NUM 40

int main(void){
    /* 0からNUM-1をカウントする変数 */
    int i;
    /* 結果を格納する配列 */
    int r[NUM];

    /* 0からNUM-1の4乗を計算して配列に格納 */
    for(i = 0; i < NUM; i++){
        r[i] = i * i * i * i;
    }

   return 0;
}

スポンサーリンク

マルチスレッドのプログラム

続いて上記のシングルスレッドのプログラムをマルチスレッドにした時のプログラムを紹介します。

このプログラムでは、「0から39までの40個の整数の4乗を計算し、結果を配列に格納する」というスレッドを、「0から19までの20個の整数の4乗を計算し、結果を配列に格納する」と「20から39までの20個の整数の4乗を計算し、結果を配列に格納する」という2つのスレッドに分割しています。

仕事を分割する処理も仕事でありスレッドですので、このプログラムでは3つのスレッドが実行されていることになります。

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

#define NUM 40
#define NUM_THREAD 2

/* スレッド実行に必要なデータを格納する構造体 */
struct data {
    int start;
    int num;
    int* result;
};

void *func(void *arg);


/* 関数func:スレッドでやること */
/* 引数arg:スレッド(仕事)を行う上で必要な情報 */
void *func(void *arg){
    int i;
    struct data *pd = (struct data *)arg;

    /* argで指定された情報に基づいて処理 */
    for(i = pd->start; i < pd->start + pd->num; i++){
        pd->result[i] = i * i * i * i;
    }

    return NULL;
}

int main(void){
    /* 0からNUM-1をカウントする変数 */
    int i;

    /* 結果を格納する配列 */
    int r[NUM] = {0};

    /* スレッドを格納する配列 */
    pthread_t t[NUM_THREAD];
    
    /* スレッドを実行する上で必要な情報を格納する配列 */
    struct data d[NUM_THREAD];

    /* 1つ目のスレッドを行う上で必要な情報を格納 */
    d[0].start = 0; /* 計算開始する数 */
    d[0].num = NUM / 2; /* 計算を行う回数 */
    d[0].result = r; /* 計算結果を格納するアドレス */

    /* 1つ目のスレッドを作成 */
    pthread_create(&t[0], NULL, func, &d[0]);

    /* 2つ目のスレッドを行う上で必要な情報を格納 */
    d[1].start = NUM / 2; /* 計算開始する数 */
    d[1].num = NUM / 2; /* 計算を行う回数 */
    d[1].result = r; /* 計算結果を格納するアドレス */

    /* 2つ目のスレッドを作成 */
    pthread_create(&t[1], NULL, func, &d[1]);

    /* スレッドの終了を待機 */
    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);

    return 0;
}
pthreadライブラリ

pthread を使用するためには pthread ライブラリへのリンクが必要です

コンパイル時(リンク時)に -lpthread を追記してやることで pthread ライブラリへのリンクを行うことができます。

プログラムの解説

ではマルチスレッドプログラムの解説をしていきたいと思います。

スレッドの変数定義

main関数内の下記でスレッド配列の定義を行っています。

pthread_t型の変数宣言
    /* スレッドを格納する配列 */
    pthread_t t[NUM_THREAD];

pthread_tがスレッドを格納する型となります。

ここではスレッドの数の分(NUM_THREAD 分)の配列として変数を定義しています。

この pthread_t 型や後述する pthread_create 関数は pthread.h で定義・宣言されています。ですので、 pthread によるマルチスレッドプログラミングを行うためには pthread.h をインクルードする必要があります。

スレッドの生成(pthread_create関数)

main 関数内の下記で1つ目のスレッドの生成を行っています。

スレッドの生成
/* 1つ目のスレッドを作成 */
pthread_create(&t[0], NULL, func, &d[0]);

pthread_create 関数はスレッドを生成する関数です。

第一引数はスレッド変数へのポインタを指定します。

第二引数はスレッドの属性へのポインタを指定します。属性とは例えばスレッドの優先度(仕事の優先度)などを設定するものです。特に属性を設定しないのであれば NULL を指定するので良いです。

第三引数はスレッドで実行する関数へのポインタを指定します。これは仕事でやる事を指示するようなものです。このプログラムでは func 関数へのポインタを渡しています。

pthreadに渡す関数の型
void *func(void *arg);

引数に指定する関数の型は、戻り値は void* 型かつ引数は void* 型1つである必要があります。関数名はなんでもオーケーです。

第四引数はスレッドを実行する上で参照するデータへのポインタを指定します。これは仕事を行う上で必要になる情報を渡すようなものです。

pthread_create 関数を実行するとスレッドが生成されますので、サボっている(余っている)コアがそのスレッドを実行してくれます。具体的には、pthread_create 関数の第三引数で指定される関数を実行します。

第三引数で指定された関数が終了すると、スレッドも終了することになります。

スレッドが参照するデータの生成

下記で1つ目のスレッドが参照するデータの生成を行っています。

1つ目のスレッド用のデータ
    /* 1つ目のスレッドを行う上で必要な情報を格納 */
    d[0].start = 0; /* 計算開始する数 */
    d[0].num = NUM / 2; /* 計算を行う回数 */
    d[0].result = r; /* 計算結果を格納するアドレス */

pthread_create 関数の第3引数で指定できる関数に渡せるデータは1つのみです。

ですので、必要なデータを格納できるように構造体を自分で定義し、その構造体のポインタを渡すのが一般的です。

このプログラムでは、スレッドで計算を開始する数と計算を行う回数、そして計算結果を格納するアドレスを格納できる struct data 構造体を定義し、情報を詰め込んでから pthread_create 関数の第四引数に指定しています。

スレッド処理用のデータ
/* スレッド実行に必要なデータを格納する構造体 */
struct data {
    int start;
    int num;
    int* result;
};

マルチスレッドのプログラム2(同期)

続いてプログラムを用いて同期の必要性について説明していきたいと思います。

実は先ほど紹介したマルチスレッドのプログラムの中で使用している pthread_join は同期を行う関数の1つになります。

この pthread_join の有無でどのようにプログラムの動きが変わるかを確認していきましょう。

スポンサーリンク

同期なしのプログラム

マルチスレッドのプログラムで紹介したプログラムを少し変更し、main 関数の最後で配列の中身を表示するようにしてみました。

まずは pthread_join なしの場合の動きを確認していきましょう。

同期なし
#include <stdio.h>
#include <pthread.h>

#define NUM 40
#define NUM_THREAD 2

/* スレッド実行に必要なデータを格納する構造体 */
struct data {
    int start;
    int num;
    int* result;
};

void *func(void *arg);


/* 関数func:スレッドでやること */
/* 引数arg:スレッド(仕事)を行う上で必要な情報 */
void *func(void *arg){
    int i;
    struct data *pd = (struct data *)arg;

    /* argで指定された情報に基づいて処理 */
    for(i = pd->start; i < pd->start + pd->num; i++){
        pd->result[i] = i * i * i * i;
    }

    return NULL;
}

int main(void){
    /* 0からNUM-1をカウントする変数 */
    int i;

    /* 結果を格納する配列 */
    int r[NUM] = {0};

    /* スレッドを格納する配列 */
    pthread_t t[NUM_THREAD];
    
    /* スレッドを実行する上で必要な情報を格納する配列 */
    struct data d[NUM_THREAD];

    /* 1つ目のスレッドを行う上で必要な情報を格納 */
    d[0].start = 0; /* 計算開始する数 */
    d[0].num = NUM / 2; /* 計算を行う回数 */
    d[0].result = r; /* 計算結果を格納するアドレス */

    /* 1つ目のスレッドを作成 */
    pthread_create(&t[0], NULL, func, &d[0]);

    /* 2つ目のスレッドを行う上で必要な情報を格納 */
    d[1].start = NUM / 2; /* 計算開始する数 */
    d[1].num = NUM / 2; /* 計算を行う回数 */
    d[1].result = r; /* 計算結果を格納するアドレス */

    /* 2つ目のスレッドを作成 */
    pthread_create(&t[1], NULL, func, &d[1]);

    for (i = 0; i < NUM; i++) {
        printf("%d : %d\n", i, r[i]);
    }
    
    return 0;
}

配列に格納された結果を表示すると下記のようになりました。

0 : 0
1 : 0
2 : 0
3 : 0
4 : 0
5 : 0
6 : 0
7 : 0
8 : 0
9 : 0
10 : 0
11 : 0
12 : 0
13 : 0
14 : 0
15 : 0
16 : 0
17 : 0
18 : 0
19 : 0
20 : 0
21 : 0
22 : 0
23 : 0
24 : 0
25 : 0
26 : 0
27 : 0
28 : 0
29 : 0
30 : 0
31 : 0
32 : 0
33 : 0
34 : 0
35 : 0
36 : 0
37 : 0
38 : 0
39 : 0

同じような結果にならなかった方も多いかもしれません。この結果は実行するたびに異なります。

これは、pthread_create で生成されたスレッドと同時に main 関数の処理も実行されているためです。

たまたま pthread_create で生成されたスレッドでの配列への値の格納が “完了した後” に配列の表示が行われた場合は配列の全要素にちゃんとした計算結果が格納されて表示されます。

ただし、pthread_create で生成されたスレッドでの配列への値の格納が “完了する前” に配列の表示が行われた場合は、上記の結果のように計算結果が格納される前に配列の中身が表示されてしまうことになります。

ここで重要なのは、前述の同期なしのプログラムでは下記の順序が保証されていないところです。

  1. 配列に値が格納される
  2. 配列の中身が表示される

マルチスレッドでは、各スレッドが同時に実行されますので、上記の 1. と 2. が同時に実行されます。ですので、1. の方が先に実行されることももちろんありますが、2. の方が先に実行されたり、1. が実行されている間に 2. が実行されてしまうこともあります。

これによってプログラムが意図しない動きをすることがあり、場合によってはバグになります。

このような順序を保証するために便利なのが同期制御です。

同期ありのプログラム

pthread_join により同期制御を行うようにしたのが下記プログラムになります。

同期なし
#include <stdio.h>
#include <pthread.h>

#define NUM 40
#define NUM_THREAD 2

/* スレッド実行に必要なデータを格納する構造体 */
struct data {
    int start;
    int num;
    int* result;
};

void *func(void *arg);


/* 関数func:スレッドでやること */
/* 引数arg:スレッド(仕事)を行う上で必要な情報 */
void *func(void *arg){
    int i;
    struct data *pd = (struct data *)arg;

    /* argで指定された情報に基づいて処理 */
    for(i = pd->start; i < pd->start + pd->num; i++){
        pd->result[i] = i * i * i * i;
    }

    return NULL;
}

int main(void){
    /* 0からNUM-1をカウントする変数 */
    int i;

    /* 結果を格納する配列 */
    int r[NUM] = {0};

    /* スレッドを格納する配列 */
    pthread_t t[NUM_THREAD];
    
    /* スレッドを実行する上で必要な情報を格納する配列 */
    struct data d[NUM_THREAD];

    /* 1つ目のスレッドを行う上で必要な情報を格納 */
    d[0].start = 0; /* 計算開始する数 */
    d[0].num = NUM / 2; /* 計算を行う回数 */
    d[0].result = r; /* 計算結果を格納するアドレス */

    /* 1つ目のスレッドを作成 */
    pthread_create(&t[0], NULL, func, &d[0]);

    /* 2つ目のスレッドを行う上で必要な情報を格納 */
    d[1].start = NUM / 2; /* 計算開始する数 */
    d[1].num = NUM / 2; /* 計算を行う回数 */
    d[1].result = r; /* 計算結果を格納するアドレス */

    /* 2つ目のスレッドを作成 */
    pthread_create(&t[1], NULL, func, &d[1]);

    /* スレッドの終了を待機 */
    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);
    
    for (i = 0; i < NUM; i++) {
        printf("%d : %d\n", i, r[i]);
    }
    
    return 0;
}

このプログラムの実行結果は下記の通りです。今度は実行するたびに結果が変わることがなく、確実に下記の結果を表示することができます。

0 : 0
1 : 1
2 : 16
3 : 81
4 : 256
5 : 625
6 : 1296
7 : 2401
8 : 4096
9 : 6561
10 : 10000
11 : 14641
12 : 20736
13 : 28561
14 : 38416
15 : 50625
16 : 65536
17 : 83521
18 : 104976
19 : 130321
20 : 160000
21 : 194481
22 : 234256
23 : 279841
24 : 331776
25 : 390625
26 : 456976
27 : 531441
28 : 614656
29 : 707281
30 : 810000
31 : 923521
32 : 1048576
33 : 1185921
34 : 1336336
35 : 1500625
36 : 1679616
37 : 1874161
38 : 2085136
39 : 2313441

プログラムの解説

先ほどの同期なしのプログラムとほぼ同じプログラムになります。

スレッドの同期(pthread_join 関数)

ただし、今回のプログラムではスレッドの同期を下記で行っています。

pthread_joinによる同期
    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);

pthread_join 関数は他のスレッドが終わるのを待つ関数です。第1引数にはスレッドを指定し、第2引数はスレッドの実行結果を格納するデータへのポインタです。

スレッド実行結果が不要であれば第2引数は NULL でもオーケーです。

pthread_join 関数は前述の通り第1引数に指定したスレッドの処理が終了するまで待つ関数です。

ですので、上記の pthread_join 関数の実行により pthread_create で生成した2つのスレッドの処理が終了しないと、pthread_join 関数実行以降に記述された処理は実行されません。

したがって、pthread_join 関数実行以降に記述された配列の中身を表示する処理は、配列への値の格納を行う2つのスレッドの処理が完了しないと実行されないことになります。

これにより下記の順序で処理が実行されることが保証されることにになります。

  1. 配列に値が格納される
  2. 配列の中身が表示される

ですので、実行するたびに結果が変わってしまうようなこともなく、確実に意図した結果を表示することができます。

今回は同期に pthread_join 関数を用いましたが、他にもメッセージキューや MUTEX など様々な同期方法があります。

スポンサーリンク

マルチスレッドのメリット/デメリット

最後にマルチスレッドを行うことによるメリット/デメリットについて解説します。

メリット

まずはメリットについて解説します。

処理の高速化が可能

マルチスレッドのメリットの一つは高速化です。

計算時間のかかる処理を複数のコアで並列して処理できるようになりますので処理時間が短くなります。

複数の処理の同時実行

また、マルチスレッド を利用することにより、複数の別々の処理を同時に実行することが可能です。

例えば1つのスレッドで scanf 関数でユーザーからの入力待ちを行いながら、他のスレッドで別の処理を同時に実行するようなことも簡単に実現することができます。

デメリット

次にデメリットについて解説します。

プログラムが難しくなる

デメリットの一つはプログラムが難しくなることです。

仕事を細分化するためのプログラムを記述する必要があるのでプログラムが難しくなります。またマルチスレッドプログラミングでは同期処理が必要になり、同期制御のプログラミングも必要になります。

遅くなる可能性がある

デメリットのもう一つはむしろ遅くなる可能性があることです。

マルチスレッドではシングルスレッドに比べて仕事を細分化する処理や同期制御が追加で必要になります。簡単ですぐ終わる仕事をマルチスレッドにしたりすると、これらの処理・制御が追加された分処理時間が増加してしまう必要があります。

スポンサーリンク

まとめ

スレッドとは仕事のことあり、マルチスレッドはスレッドを複数に細分化したものです。マルチスレッドにより複数のコアで並列に処理を実行することが可能です。同期を行う必要がある場合はむしろ遅くなる可能性もありますので注意しましょう。

下のページでは画像の拡大縮小をマルチスレッドで処理するプログラムを解説しています。マルチスレッドのイメージがさらにつきやすくなると思いますのでよろしければ読んでみてください。

マルチスレッドで画像の拡大縮小を高速化

4 COMMENTS

y.k

サンプルコードも理解するために簡単すぎず難しすぎずちょうどよく、大変わかりやすかったです。ありがとう。

返信する
daeu

y.k さん

コメントありがとうございます!
そう言っていただけると大変嬉しいです!

今後も皆さんのタメになる記事を書いていきたいと思いますので、
気が向いたときにでもまた立ち寄っていただけると幸いです。

返信する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です