プログラミングをしていると、処理を並列して実行したいと思うことが出てきます。
この、処理を並列実行する一つの手段が “マルチスレッド” です。
このページではまずマルチスレッドについて、プログラミング初心者でも理解できるようにわかりやすく解説し、続いてC言語で pthread
を用いたマルチスレッドのプログラムの紹介&プログラムの解説を行います。
Contents
マルチスレッドとは
まず、マルチスレッドがどのようなものであるかのイメージを固めていきます。続いてマルチスレッドのプログラム例を紹介し、そのイメージを具体的なものに落とし込んでいきたいと思います。
スレッドとは
まずはスレッドから説明します。スレッドとは仕事のことです。
プログラムを起動すると、必ず1つのスレッドが生成されます。
具体的にいうと、C言語プログラムの場合は “main
関数を実行するという仕事(スレッド)” が生成されます(例えば Python などであれば、Python スクリプトを上から順に実行するスレッドが生成されます)。このスレッドをメインスレッドなどと呼ぶことがあります。
スポンサーリンク
CPUのコア
その仕事(スレッド)をするのが CPU のコアです。スレッドを仕事として例えるのであれば、コアは仕事をする人です。
コンピュータの脳を司る CPU には複数のコアが搭載されています。つまり仕事をする人が複数います。ですので、同時に複数の仕事をこなすことが可能です。
また、CPU のスペック表を見ると、コア数と一緒にスレッド数が記載されているケースが多いです。こちらのスレッド数は CPU 全体で同時に処理できる仕事(スレッド)の数を表しています。例えばコア数が4つなのにスレッド数が8つの場合は、1つのコアが同時に2つの仕事(スレッド)を処理できることになります。
ただし、スレッド(仕事)1つに対して動作するコアはせいぜい1つだけです。
また、コアは仕事があれば率先して仕事をこなすのですが、仕事がなければ基本的にサボっている状態になります。
もしスレッド(仕事)が1つしかないのであれば、コアが同時に動作するのは1つだけで、他のコアはサボっていることになります。
せっかく手が空いている人がいるのに(コアがあるのに)サボらせているのは勿体無いですよね。
あなたが作成するプログラムを起動した場合、前述の通り main
関数を実行するスレッドが生成されます(もちろん main
関数から呼び出される関数の処理も行ってくれます)。
なので、プログラムを起動するだけで、CPU のコアが自動的に main
関数を実行することになります。
ただし、マルチスレッドプログラミングを行わない限り、実行時に生成される仕事(スレッド)は基本的にはその1つのみです。ですので、コアが複数あったとしても、コアを1つしか使用しないプログラムになっている場合が多いです。
スレッドを複数に分割したものがマルチスレッド
サボっているコアに仕事をさせるために、仕事を小さな単位に分割し、サボっているコアに仕事を割り振るというのがマルチスレッド処理の考え方です。
具体的には、マルチスレッドというのは、仕事(スレッド)を複数の仕事に分割した細分化された仕事です。
仕事(スレッド)が一つしかなければ、その仕事に対して動作するコアは1つのみです。
しかし仕事さえあれば、コアは自分から率先して仕事を見つけてその仕事をこなしますので、仕事が複数あれば複数のコアを動作させることができ、並列して仕事をさせることが可能です。
これによりサボっているコアを有効活用することができ、仕事を完了するのに必要な時間を短縮することも可能です。
なので、マルチスレッドプログラミングでは、まずプログラムで行うメインの仕事を小さな仕事に分割することを考える必要があります。
メインの仕事とは、要は main
関数で実行される処理です。この処理を、どのように小さな仕事に分割するか、要は、どの処理を並列に実行するかについて考える必要があります。
例えば1から100までの数字をそれぞれ2倍することがメインの仕事なのであれば、
- 1から50までの数字をそれぞれ2倍する
- 51から100までの数字をそれぞれ2倍する
という小さな仕事に分割することができます。
これらをスレッドとして生成すれば、1から50までの計算と51から100までの計算を並列に実行することができることになります。
また、画像の回転がメインの仕事であれば、
- 画像の上半分を回転する仕事
- 画像の下半分を回転する仕事
という小さな仕事に分割することができます。
これらをスレッドとして生成すれば、画像の上半分と画像の下半分の回転処理を並列に実行することができるようになります。
どう仕事を分割するかが決まれば、次はその仕事を関数として作成します。
例えば先ほどの例の1つ目で考えると、下記の関数が必要になることになります。
- 1から50までの数字をそれぞれ2倍する関数
- 51から100までの数字をそれぞれ2倍する関数
ただし、実際にスレッドの数だけ関数を用意する必要はなく、関数に渡す引数によって仕事を分割するようにしても良いです。例えば上記の場合は、start
から end
までの数字をそれぞれ2倍する関数を作成し、引数で start
と end
を指定するようにすれば、1つの関数だけで実現することが可能です。
仕事を関数として作成することができれば、あとはその仕事の生成を行います。
この仕事の生成は、関数実行により行うことができます。例えば pthread
が使用できる環境であれば pthread_create
関数によりスレッド(仕事)を生成することができます(Windows なんかだと CreateThread
関数でスレッド生成できたはずです)。
前述の通り、プログラムは実行すると必ず main
関数が実行されますので(main
関数を実行するスレッドが生成される)、main
関数の中、もしくは、main
関数から呼び出す関数の中で上記の関数を実行する必要があります。
さらに、これらの関数(pthread_create
や CreateThread
)では、引数として関数を指定することが可能です。この引数に、先ほど作成した “分割した仕事を記述した関数” を指定します。
また、引数には仕事で参照すべきデータを渡したりすることもできます。
で、仕事を生成すれば(pthread_create
等を実行すれば)、引数として指定した関数を実行するスレッドが生成されますので、自動的にコアがその関数を実行してくれることになります。
ここまで説明したことをまとめておきます。
- マルチスレッドとは大きな仕事を小さな仕事に分割することで、複数のコアで並列動作できるようにするもの
- スレッド(仕事)の内容・やることは関数として記述する
- スレッド(仕事)は
pthread_create
関数・CreateThread
関数などにより作成できる - スレッド(仕事)を作れば、空いているコアが勝手に処理してくれる
マルチスレッドの同期
マルチスレッドの同期とは、他のスレッドとの待ち合わせを行う処理です。
基本的にそれぞれのコアは仕事を独立に自分のペースで行います。しかし、仕事を自分のペースで行われると困るパターンがあります。
例えば鍋料理について考えてみましょう。ここでは鍋料理するというスレッドを「具材を切るスレッド」、「ダシをとるスレッド」、「具材を煮込むスレッド」の3つのスレッドに分割したいと思います。
これらのスレッドは、それぞれのコアが独立して自分のペースで処理されてしまうと困ってしまいます。
「具材を切るスレッド」と「ダシをとるスレッド」はそれぞれ並列して行って良いスレッドです。
しかし「具材を煮込むスレッド」は、「具材を切るスレッド」と「ダシをとるスレッド」の両方が終わってから開始する必要があるスレッドです。
もし具材が切られていなければ具材をまるまる鍋に入れることになります。また、ダシが取れていない状態で「具材を煮込むスレッド」が開始されると、具材に味が染みなくて出来が悪くなってしまいます。
そこで用いるのが同期です。同期とは前述の通り他のスレッドとの待ち合わせを行う処理です。
同期を用いることで、「具材を煮込むスレッド」を「具材を切るスレッド」と「ダシをとるスレッド」の終了するまで待たせ、それぞれが終了してから煮込みを開始させるようにすることができます。これにより美味しいお鍋を作ることができます。
スポンサーリンク
マルチスレッドのプログラム1(スレッド生成)
ここまでの説明で、マルチスレッドがどのようなものであるかがイメージ出来てきたのではないかと思います。
ここからはマルチスレッドの具体的なプログラムを用いて、そのイメージをもっと具体的なものに落とし込んでいきたいと思います。
このページではC言語で pthread
(POSIXスレッド)を用いたマルチスレッドのプログラムを紹介します。動作確認は Mac OSX で行っていますが、おそらく Linux 等でも動作すると思います。
プログラムの仕様
このプログラムは0から39までの40個の整数の4乗を計算し、結果を配列に格納するプログラムになります
。つまり、「0から39までの40個の整数の4乗を計算し、結果を配列に格納する」がメインのスレッド(仕事)になります。
シングルスレッドのプログラム
マルチスレッドのプログラムと対比するためにまずはシングルスレッドのプログラムを載せておきます。
#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つのスレッドが実行されることになります。
#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
ライブラリへのリンクが必要です
コンパイル時(リンク時)に -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 関数はスレッドを生成する関数です。
第1引数はスレッド変数へのポインタを指定します。上記を実行すれば、pthread_create
を実行すると、生成したスレッドの ID が t[0]
に格納されます。
第2引数はスレッドの属性へのポインタを指定します。属性とは例えばスレッドの優先度(仕事の優先度)などを設定するものです。特に属性を設定しないのであれば NULL
を指定するので良いです。
第3引数はスレッドで実行する関数へのポインタを指定します。これは仕事でやる事を指示するようなものです。このプログラムでは func
関数へのポインタを渡しています。
void *func(void *arg);
引数に指定する関数の型は、戻り値は void*
型、かつ引数は void*
型1つである必要があります。関数名はなんでもオーケーです。
第4引数はスレッドを実行する上で参照するデータへのポインタを指定します。これは仕事を行う上で必要になる情報を渡すようなものです。
pthread_create
関数を実行するとスレッドが生成されますので、サボっている(余っている)コアがそのスレッドを実行してくれます。具体的には、pthread_create
関数の第3引数で指定される関数を実行します。
第3引数で指定された関数が終了すると、スレッドも終了することになります。
スレッドが参照するデータの生成
下記で1つ目のスレッドが参照するデータの生成を行っています。
/* 1つ目のスレッドを行う上で必要な情報を格納 */
d[0].start = 0; /* 計算開始する数 */
d[0].num = NUM / 2; /* 計算を行う回数 */
d[0].result = r; /* 計算結果を格納するアドレス */
pthread_create
関数の第4引数で指定できる関数に渡せるデータは1つのみです。
ですので、必要なデータを格納できるように構造体を自分で定義し、その構造体のポインタを渡すのが一般的です。
このプログラムでは、スレッドで計算を開始する数と計算を行う回数、そして計算結果を格納するアドレスを格納できる struct data
構造体を定義し、情報を詰め込んでから pthread_create
関数の第4引数に指定しています。
/* スレッド実行に必要なデータを格納する構造体 */
struct data {
int start;
int num;
int* result;
};
pthread_create
関数の第3引数の関数が実行される際には、上記のデータが引数として渡されますので、その関数内でそれらのデータを参照しながら処理を行わせることができます。
マルチスレッドのプログラム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. が実行されてしまうこともあります。
これによってプログラムが意図しない動きをすることがあり、場合によってはバグになります。
このような順序を保証するために便利なのが同期制御です。
同期ありのプログラム
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(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つのスレッドの処理が完了しないと実行されないことになります。
これにより下記の順序で処理が実行されることが保証されることにになります。
- 配列に値が格納される
- 配列の中身が表示される
ですので、実行するたびに結果が変わってしまうようなこともなく、確実に意図した結果を表示することができます。
今回は同期に pthread_join
関数を用いましたが、他にもメッセージキューや MUTEX など様々な同期方法があります。
スポンサーリンク
マルチスレッドのメリット/デメリット
最後にマルチスレッドを行うことによるメリット/デメリットについて解説します。
メリット
まずはメリットについて解説します。
処理の高速化が可能
マルチスレッドのメリットの一つは高速化です。
計算時間のかかる処理を複数のコアで並列して処理できるようになりますので処理時間が短くなります。
複数の処理の同時実行
また、マルチスレッド を利用することにより、複数の別々の処理を同時に実行することが可能です。
例えば1つのスレッドで scanf
関数でユーザーからの入力待ちを行いながら、他のスレッドで別の処理を同時に実行するようなことも簡単に実現することができます。
デメリット
次にデメリットについて解説します。
プログラムが難しくなる
デメリットの一つはプログラムが難しくなることです。
仕事を細分化するためのプログラムを記述する必要があるのでプログラムが難しくなります。またマルチスレッドプログラミングでは同期処理が必要になり、同期制御のプログラミングも必要になります。
遅くなる可能性がある
デメリットのもう一つはむしろ遅くなる可能性があることです。
マルチスレッドではシングルスレッドに比べて仕事を細分化する処理や同期制御が追加で必要になります。簡単ですぐ終わる仕事をマルチスレッドにしたりすると、これらの処理・制御が追加された分処理時間が増加してしまう必要があります。
スポンサーリンク
まとめ
スレッドとは仕事のことあり、マルチスレッドはスレッドを複数に細分化したものです。マルチスレッドにより複数のコアで並列に処理を実行することが可能です。同期を行う必要がある場合はむしろ遅くなる可能性もありますので注意しましょう。
下のページでは画像の拡大縮小をマルチスレッドで処理するプログラムを解説しています。マルチスレッドのイメージがさらにつきやすくなると思いますのでよろしければ読んでみてください。
【C言語】マルチスレッドで画像の拡大縮小を高速化
[…] https://daeudaeu.com/programming/c-language/multithread/ […]
[…] https://daeudaeu.com/programming/c-language/multithread/ […]
サンプルコードも理解するために簡単すぎず難しすぎずちょうどよく、大変わかりやすかったです。ありがとう。
y.k さん
コメントありがとうございます!
そう言っていただけると大変嬉しいです!
今後も皆さんのタメになる記事を書いていきたいと思いますので、
気が向いたときにでもまた立ち寄っていただけると幸いです。