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

プログラミングをしていると、処理を並列して実行したいと思うことが出てきます。処理を並列実行する一つの手段がマルチスレッドです。このページではまずマルチスレッドについて、プログラミング初心者でも理解できるようにわかりやすく解説し、続いてC言語でpthreadを用いたマルチスレッドのプログラムの紹介&プログラムの解説を行います。特に前半に関してはプログラミング言語関係なく読める内容になっていますので是非読んでみてください!

マルチスレッドとは

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

スレッドとは

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

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

CPUのコア

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

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

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

ただし、コアは仕事があれば率先して仕事をこなすのですが、仕事がなければ基本的にサボっている状態になります。せっかく仕事のできる人がいるのに(コアがあるのに)サボらせているのは勿体無いですよね。

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

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

仕事(スレッド)が一つしかなければ、一つのコアしか仕事をしません。しかし、仕事さえあればコアは自分から率先して仕事を見つけてその仕事をしますので、仕事が複数あれば複数のコアを動作させることができ、並列して仕事をさせることが可能です。また、これによりサボっているコアを有効活用することができます。

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

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

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

という小さな仕事に分割することができます。また、画像の回転がメインの仕事であれば、

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

という小さな仕事に分割することができます2つのコアが同時にこの2つの仕事を並行して実行することができます。

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

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

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

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

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

マルチスレッドの同期

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

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

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

これらのスレッドはそれぞれのコアが独立して自分のペースで処理されてしまうと困ってしまいます。「具材を切るスレッド」と「ダシをとるスレッド」はそれぞれ並列して行って良いスレッドです。しかし「具材を切るスレッド」は、「具材を切るスレッド」と「ダシをとるスレッド」の両方が終わってから始める必要のあるスレッドです。もし具材が切られていなければ具材をまるまる鍋に入れることになります。またダシが取れていなければ、鍋の味が悪くなってしまいます。

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

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

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

このページではC言語でpthread(POSIXスレッド)を用いたマルチスレッドのプログラムを紹介します。動作確認は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};

    int a[NUM];

    /* スレッドを格納する配列 */
    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]);

   return 0;
}
pthreadライブラリ

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

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

プログラムの解説

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

スレッドの変数定義

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

    /* スレッドを格納する配列 */
    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関数へのポインタを渡しています。

void *func(void *arg);

関数の型としては、戻り値がvoid*型であり、引数もvoid*型でなければならないことに注意してください。関数名はなんでもオーケーです。

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

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

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

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

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

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

pthread_create関数の第三引数で指定できる関数に渡せるデータは1つのみです。ですので、必要なデータを格納できるように構造体を自分で定義し、その構造体のポインタを渡すのが一般的です。

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

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

スポンサーリンク

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

続いて同期の必要性がわかるプログラムを紹介したいと思います。

このプログラムでは「0から20までの19個の整数の4乗を計算して結果を配列に格納し、さらにその後全ての結果に+1する」をメインのスレッドとています。

そしてこのスレッドを「0から9までの10個の整数の4乗を計算し、結果を配列に格納する」と「10から19までの10個の整数の4乗を計算し、結果を配列に格納する」と「結果に+1する」という3つのスレッドに分割して実行したものになります。同期の必要性が際立つように、func関数の中にusleep関数を実行しています。

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

#define NUM 20
#define NUM_THREAD 3

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

void *func(void *arg);
void *funcDiv(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;
        usleep(1000);
    }

    return NULL;
}

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

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

    return NULL;
}

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

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

    int a[NUM];

    /* スレッドを格納する配列 */
    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]);
   
     /* 3つ目のスレッドを行う上で必要な情報を格納 */
    d[2].start = 0; /* 計算開始する数 */
    d[2].num = NUM; /* 計算を行う回数 */
    d[2].result = r; /* 計算結果を格納するアドレス */

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

   return 0;
}

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

0 : 0
1 : 1
2 : 1
3 : 1
4 : 1
5 : 1
6 : 1
7 : 1
8 : 1
9 : 1
10 : 10001
11 : 1
12 : 1
13 : 1
14 : 1
15 : 1
16 : 1
17 : 1
18 : 1
19 : 1

明らかに結果がおかしいことが分かると思います。例えば2の4乗は16なので+1したら17が結果となるはずですが、表示は1となっています。

これは「0から9までの10個の整数の4乗を計算し、結果を配列に格納する」と「10から19までの10個の整数の4乗を計算し、結果を配列に格納する」のスレッドが終わる前に「結果に+1する」のスレッドが実行されてしまっているためです。

これを解決するのが同期処理です。同期を追加したプログラムが下記の通りになります。

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

#define NUM 20
#define NUM_THREAD 3

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

void *func(void *arg);
void *funcDiv(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;
        usleep(1000);
    }

    return NULL;
}

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

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

    return NULL;
}

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

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

    int a[NUM];

    /* スレッドを格納する配列 */
    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);
   
     /* 3つ目のスレッドを行う上で必要な情報を格納 */
    d[2].start = 0; /* 計算開始する数 */
    d[2].num = NUM; /* 計算を行う回数 */
    d[2].result = r; /* 計算結果を格納するアドレス */

    /* 3つ目のスレッドを作成 */
    pthread_create(&t[2], NULL, funcDiv, &d[2]);
    
    /* 配列に格納された結果を表示 */
    for(i = 0; i < NUM; i++){
        printf("%d : %d\n", i, r[i]);
    }

   return 0;
}

このプログラムの実行結果は下記の通りです。

0 : 0
1 : 2
2 : 17
3 : 82
4 : 257
5 : 626
6 : 1297
7 : 2402
8 : 4097
9 : 6562
10 : 10001
11 : 14642
12 : 20737
13 : 28562
14 : 38417
15 : 50626
16 : 65537
17 : 83522
18 : 104977
19 : 130322

プログラムの解説

スレッド生成などの基本的な部分はマルチスレッドのプログラム1(スレッド生成)と同じです。

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

スレッドの同期は下記で行っています。

    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);

pthread_join関数は他のスレッドが終わるのを待つ関数です。第一引数にはスレッドを指定し、第二引数はスレッドの実行結果を格納するデータへのポインタです。スレッド実行結果が不要であれば第二引数はNULLでもオーケーです。

2つのスレッド両方が終わらないと上記から次の処理に移りませんので、「0から9までの10個の整数の4乗を計算し、結果を配列に格納する」と「10から19までの10個の整数の4乗を計算し、結果を配列に格納する」が終わらないと「結果に+1する」のスレッドの生成は行われません。

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

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

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

メリット

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

処理の高速化が可能

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

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

デメリット

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

プログラムが難しくなる

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

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

遅くなる可能性がある

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

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

まとめ

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

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

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

コメントを残す

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