レイヤーが低い分野では特に、
マルチ○○○という言葉をよく聞きます。
マルチスレッド、マルチプロセス、マルチコア
などなど、結構思い浮かぶかと思います。
どれも意味合いが違うのですが、
本質はどれも「並列、同時に」ということです。
現実世界で言うなら、
「浮気、不倫」がそれに該当するでしょうか。
仕事、勉強、家事・・・
人には様々なプロセスがあり、
その一つに「恋愛」があります。
一人の女性と付き合って、
破局すれば次の女性・・・
これでは「女性経験値」を
効率的に上げられないですよね。
※ここでの女性経験の定義は、
女性との接し方であったり、
女性の喜ばし方であったり、
忘れてならないxxxの意味を言うとします。
さて、イケイケなメンズは、
恋愛という1プロセスに、
多くの女性スレッドを抱えます。
一度に多くの女性を相手にしながら、
上手いこと関係性を気づいていく。
問題のプログラムであれば、
浮気が上手く機能するわけです。
隠し方が下手であれば、
浮気期間が長ければ長いほど、
また浮気相手が多いほど、
本命にバレる可能性が高まります。
※浮気を推進しているわけではありません(笑)
(残念ながら??)私は、
そんな器用なことは出来なので、
シングルスレッドな男になります。
マルチスレッドプログラミングも同じです。
排他制御が出来てないと、
予期せぬ結果が返ってくるわけです。
本命に刺されるかもしれません。
(幸い??)マルチスレッドでは、
本命とかそういったスレッドに優位はなく、
各スレッドの扱いは基本的に平等ですけど。
ここで簡単に「マルチ○○○」
という言葉をおさらいしておきましょう。
コアとスレッドとプロセスの違い
イメージで言うならば、
プロセスが1つのアプリケーションで
その中にいくつかのスレッドがある
といったところでしょう。
厳密にはプロセスとスレッドには、
リソースの扱い方の違いなどがありますが、
ざっくり言ってどちらも一つの処理単位です。

CPUが全く同時に処理できる
プロセス数もスレッド数も
CPU性能によって限度があります。
CPUが処理できるスレッド数が
限度を超えている場合は、
上の画像(1C1T)のように、
瞬間的に切り替わりながら、
あたかも全く同時にプログラムが
動作しているかのように振る舞います。
CPUの仕様書には、
「4C8T」や「2C2T」のように、
性能指標が書いてあります。
これは1つのCPUに4コアあり、
そして最大8スレッドを
CPUが全く同時に扱える
ということを意味しています。
基本、1コアで扱えるのは1プロセスです。
「コア」はCPUつまりハードウェアの言葉で、
「プロセス」はプログラム
つまりソフトウェア上の言葉です。
実際に見てみましょう。
webブラウザと数個のソフトを
同時に立ちあげているときの
CPUの様子(タスクマネージャ)です。

4C8TのCPU(core i7 8650U)です。
多くのプロセスとスレッドが生成され、
それらが処理されているのが
分かるかと思います。
まとめると、
CPUにはコアがあり、
"コア"でプロセスを扱い、
"プロセス"でスレッドを扱います。
そしてどの程度の数を
一度に扱えるかは、
CPUの性能によります。
補足:
同時処理しなければいけないスレッドが
CPUが同時処理できる数以上にある場合、
どのスレッドを処理するかは、
全てOSに気まぐれ次第になります。
それでは実際にC++で、
マルチスレッドをやってみましょう。
Linuxを使うのでご用意を。
マルチスレッド:pthreadライブラリ
スレッドを意識していない
入門的なプログラミングは、
1スレッドだけのプログラムに該当します。
マルチスレッドプログラミングをするために、
ここでは「pthreadライブラリ」を使います。
<pthread.h>をインクルードします。
pthreadライブラリには、
マルチスレッドプログラミング用に、
様々な変数と関数が用意されています。
一回で全てを網羅するのは大変なので、
入門として使うものだけを、
ソースコードの説明と同時に紹介します。
まずはマルチスレッドではない、
1スレッドのプログラムです。
「1からnまでの和を出力する関数」を用意しました。
main文でその関数を呼び出す、
という簡単なプログラムです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include<stdio.h> #include<stdlib.h> int sum; void functionSum(int y){ int x = 1; sum = 0; while(x<=y){ sum += x; x++; } printf("Sum from 1 to %d is %d\n", y, sum); } int main(void){ int n = 10000; functionSum(n); return 0; } |
関数を呼び出すときに、
どの数まで足すか指定します。
これは10000までの和計算を
一回しか行ってないです。
しかし10000までと30000まで
の和を求めたいとしたら、
普通は10000を指定した関数の次に、
30000を引数にした関数を呼びます。
10000が終わって30000を
計算していては非効率ですよね。
10000までの和を求めるスレッドと
30000までの和を求めるスレッドを
作って、並列に計算してみましょう。
マルチスレッドのソースがこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include<stdio.h> #include<stdlib.h> #include<pthread.h> int sum; void *functionSum(void *arg){ int *y = (int*)arg; int x = 1; sum = 0; while(x<=*y){ sum += x; x++; } printf("Sum from 1 to %d is %d\n", *y, sum); } int main(void){ pthread_t thread1, thread2; int n1, n2; n1 = 10000; pthread_create(&thread1, NULL, functionSum, &n1); n2 = 30000; pthread_create(&thread2, NULL, functionSum, &n2); pthread_join(thread1, NULL); pthread_join(thread2, NULL); return 0; } |
何をやっているのかと言うと・・・
新しくスレッドを作るために、
まずpthread.hをインクルードします。
そして実際にスレッドを作るときは、
pthread_t型の変数を宣言して、
スレッドの入れ物を用意します。
これにはスレッドの識別IDが入ります。
実際にスレッドを動作させるときは、
pthread_create関数を使います。
1 |
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg); |
第一引数:
スレッドIDのポインタ
第二引数:
作成するスレッドの属性を指定するパラメータ。通常はNULL(デフォルトの属性)。
第三引数:
スレッドで実行したい関数ポインタ。関数のフォーマットは、void型ポインタを仮引数にとり、void型を戻り値とする必要がある。
第四引数:
第三引数で指定した関数の引数。
pthread_createで関数を実行したら、
そのスレッドは終了します。
作ったスレッドが不必要になったら、
完全に消去させます。
OSが保持するコンテキスト切り替えに
必要なスレッド情報を破棄させます。
pthread_join関数を使います。
1 |
int pthread_jon(pthread_t thread, void **value_ptr); |
第一引数:
pthread_t型変数、スレッドID
第二引数:
スレッドの戻り値の格納先を指定。今回は不必要なのでNULLを指定。
ところで実際に、
プログラムを実行させてみると、
以下のようになってしまいます。

ちなみに、
pthreadライブラリを使用する時は、
gccでコンパイルするときに、
-pthreadオプションを付けて下さい。
10000までの和は5005000
30000までの和は450015000
が正しい結果となります。
どうでしょうか。
正しい結果が返ってくるときもあるが、
間違った結果が返ってくるときもあります。
これでは正しいプログラムとは言えないです。
実はこのような結果になってしまうのは、
以下のことが原因となっています。
スレッドとメモリ共有
原因の話に入る前に、
メモリと変数の説明を少しします。
スレッドはそのプロセスが
保持するアドレス空間を
全スレッド間で共有します。
プロセス間では、
アドレス区間を共有しません。

sum変数はプログラムの冒頭で、
グローバル変数として宣言しています。
グローバルで宣言された変数や配列は、
メモリのデータ領域に確保されます。
ちなみに関数内で宣言された変数は、
メモリのスタック領域に確保されます。
このデータ領域にある変数は、
どのスレッドからでもアクセス可能です。
スタック領域もポインタを渡すなり、
アドレス空間を共有しさえすれば疑似的に可能です。
スタックやデータ領域の詳しい説明は、
他の記事に任せるとしましょう。
話しを戻しまして、
main文で作成したスレッドは、
並列処理をしながら、
お互いにsum変数にアクセスしています。
問題は「sum += x」にあります。
この一文をアセンブリ的に見てみましょう。
- メモリからsumに格納された値をレジスタAに格納する。
- メモリからxに格納された値をレジスタBに格納する。
- レジスタAとレジスタBを足して、結果をレジスタAに格納する。
- レジスタAの値をsumが示すメモリに書き込む。
ソースコード上では一行に見えても、
CPUとメモリ内では
上記の4行の処理が起きています。
この4行を処理しているうちに、
スレッドが切り替わり、
本来は期待しないsumの値が
読み込まれたり書き込まれたりします。
したがってあるスレッドがsumを
使っている間は他のスレッドは、
sumを使えないようにする必要があります。
これを排他制御と言います。
長くなってしまったので、
排他制御は次の記事で
書こうかと思います。
最後まで読んでいただきありがとうございました。
※今回のプログラムは、
マルチスレッドのプログラミングを
分かりやすい例で行うために、
あえてsumをグローバル宣言しました。