お得すぎるAmazonPrimeに入っていない人は今すぐ無料体験!

スレッドとプロセスの違いとは?マルチスレッドプログラミングの危険性を解説!

今井

こんにちは!
今井(@ima_maru)です。

今回は、プロセスとスレッドについて軽く触れた後に、マルチスレッドプログラミングの危険性を紹介します。

また、実際にプログラミングして、スレッドがどのように動くのかを確かめていこうと思います。

この記事はこんな方にオススメ!
  • プロセスとスレッドの違いについて知りたい方!
  • スレッドの動作について知りたい方!
  • マルチスレッドプログラミングの危険性について知りたい方!

それでは解説していきます!

プロセスとは?

プロセスとは、OSが実行しているプログラム(のインスタンス)になります。

ほぼほぼ「プロセス」=「プログラム」と思っていただいて大丈夫です。

例えば、Word・Excel・chrome・メモ帳などはそれぞれ1つのプロセスになります。

これらのプロセスがOSによって並列処理されることが「マルチプロセス」と呼ばれます。

今井

いろんなプログラムが同時に動くのは、汎用的なOSでは当たり前のことですね。つまり、WindowsやMacなどは、マルチプロセス(マルチタスクという)のOSということになりますね。

プロセスはOSによって管理されていて同じメモリ領域を共有しない

プロセスが使用していいメモリ領域はOSによって管理されています。

そして、各プロセスは同じメモリ領域を一切共有しません

なので、メモ帳に何か文字を書いたからと言って、Wordに反映されることはあり得ないのです。

だってそもそもメモリ領域が全く別なのですから。

これからいえることは、あるプロセスの処理は、ほかのプロセスに一切影響を与えないということです。

プロセスはメモリ領域に仮想アドレスでアクセスする

プロセスが使用していいメモリ領域は、あらかじめOSから与えられていて決まっています。

そして、OSからメモリ領域にアクセスするために与えられるのが「仮想アドレス」です。

仮想アドレスは、メモリ領域の通し番号のようなもので、0x00000000~0x0000FFFFといったようなものです。

これらはOSによって確保された実際のメモリアドレス=物理アドレスと結び付けられています。

この仕組みは「メモリアドレスの抽象化」と「範囲外アクセスの防止」という二つのメリットを生みます。

「メモリアドレスの抽象化」は、プロセス側が物理アドレスを意識しないでよいこと

「範囲外アクセスの防止」は、そのままですが、範囲外にアクセスができないことです。

もしほかのプロセスのメモリ領域にアクセスしたい場合は、OSが用意しているAPIを用いる必要があります。

スレッドとは?

スレッドとは、CPUから見たプログラムの「実行単位」です。

CPUは基本的に1つのコアで1つの処理しか実行することはできません。

つまり、4コアのCPUでは同時に実行できる処理は4つまでということです。

その処理単位がスレッドと呼ばれます。

言い換えれば、「1つのコアに割り当てられるのは1つのスレッドまで」ですね。

スレッドはプロセスに含まれる

スレッドというのは、プロセスに含まれます。

例えば、

「ユーザーからの入力を受け取るスレッド」
「画面描画をするスレッド」
「音楽を再生するスレッド」
「インターネット通信を行うスレッド」

などなど、1つのプロセス内でも複数の機能でスレッドを分けて実装することができます。

呼ばれ方としては、

シングルスレッドのプロセス:1つしかスレッドを持たないプロセス
マルチスレッドのプロセス:複数スレッドを持つプロセス

このように呼ばれます。

もちろん1つのスレッドのみで機能させることも可能ですが、複数のスレッドを使うことでいくつかのメリットを生むことも確かです。

スレッドはプロセス内の同じメモリ領域を共有する

スレッドは、スレッド同士で同じメモリ領域を共有します。(危険)

ここがプロセスと違います。

そのため、スレッドAとスレッドBが同じメモリ領域を同時に書き換えようとしたとき、バグが起こったりもします。

このようなことが起こらないために、マルチスレッドの環境では、適切なメモリ管理を行わないといけません

マルチスレッドの目的は「並列処理の簡略化」を可能にする

マルチスレッドにすることの目的は、主に「並列処理の簡略化」です。

例えば、シングルスレッドであると、ものすごく重い処理をしているときに画面更新が行われず画面がフリーズしているように見えます。

これを解消するには、画面更新の処理をその重い処理の間に細切れに挟む必要があります。

もちろんシングルスレッドでもこのような記述はできます。

が、マルチスレッドで書けば、もっと簡単に並列処理を実現できるのです。

このメリットは、それぞれの処理に注力できること。

重い処理は重い処理だけを1つのスレッドとして書けばよくて、画面更新の処理はまた違ったスレッドで書けばよいのです。

それらのスレッドはコンテキストスイッチと呼ばれる機能によって切り替えられます。

ここはあまり意識しなくてもよいところです。

マルチスレッドはマルチコアCPUにおいての高速化にも使われる

マルチスレッドにすると、CPUの複数のコアに処理を割り当てることができます

同じ作業量でも、1人と2人では処理スピードも2倍違うでしょう。

それと同じで、複数のコアにうまく割り当てることができれば、高速化が期待できます

ただ注意しないといけないのが、シングルコアの場合です。

シングルコアの場合、スレッドは同じコアで切り替えられるので、その切り替え分で逆にパフォーマンスが落ちます。

結局「プロセス」と「スレッド」って何が違うの?

結局のところ、プロセスとスレッドは何が違うのでしょうか。

そもそも、プロセスとスレッドはWordとExcelといった対等な関係ではありません

しいて言うなら、プロセスはWordであり、スレッドはWordの画面描画機能やキーボード入力機能などです。

つまり、階層が違う概念なのです。

OSによってプロセスは管理され、プロセスによってスレッドは管理されます。

また、OSは各プロセスに対して異なるメモリ領域を提供できますが、プロセスはスレッドに対してそれを行うことができません

なので、WordとExcelはメモリ領域を共有することはありませんが、Wordの画面描画スレッドとWordのキーボード入力スレッドは同じWordというプロセスのメモリ領域を共有するのです。

だから、プロセス間では影響は及ぼさないけど、スレッド間ではメモリ領域をちゃんと管理しないとバグるのです。

マルチスレッドプログラミングとは「並列処理」

マルチスレッドプログラミングとは、端的に言えば並列処理です。

スレッドを高速で切り替えることであたかも同時に処理しているように見せる

マルチスレッドは同時に複数のスレッドが処理を行っているように思えますが、特殊な状況でない限り「同時」ではありません。

というのは、CPUのコアが複数ある場合は、各コアで同時に処理できるのですが、たかが4つや8つのコアで100や200といったスレッド数を同時に処理することはできません。

そこでどうマルチスレッドを実現するかというと、

「人間にはわからないほど高速でスレッドの切り替えを繰り返して、あたかも同時に処理を行っているように見せる」

ということを行っています。

C++の標準ライブラリのthreadで確かめてみましょう。

実行する処理
  • スレッドAを作成し関数func_a()を実行させる(’a’を表示する)
  • スレッドBを作成し関数func_b()を実行させる(’b’を表示する)
C++
#include <iostream>
#include <thread>
using namespace std;

void func_a()
{
	for(int i = 0; i < 10000; i++)
		cout << 'a';
}

void func_b()
{
	for (int i = 0; i < 10000; i++)
		cout << 'b';
}

int main()
{
	//スレッド作成
	thread threadA(func_a);
	thread threadB(func_b);

	//スレッド終了待ち
	threadA.join();
	threadB.join();

	return 0;
}
実行結果
bbbbbbbbbbbaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb...

aと表示しているのはスレッドAです。bはスレッドBです。

この実行結果からもわかるように、スレッドAとスレッドBの切り替えを繰り返し行うことにより、あたかも同時に処理しているかのように見せるのです。

試したい方は以下からどうぞ。

参考 スレッドの切り替わりWandbox

スレッドの切り替わるタイミングによっては致命的なバグを引き起こす可能性がある

マルチスレッドでは、スレッドを高速で切り替えながら複数の処理を実行しています。

また、スレッドの切り替わるタイミングを正確に予想することは非常に困難です。

そんな、スレッドの切り替わるタイミングによっては、

「複数のスレッドが同じリソースにアクセスしている状況」

が発生することがあります。

この時に、「ある危険性」が生じます。

例えば、「同じ変数を参照しているときに、その値を変えようとしたとき」なんかがそうです。

何が起こるかというと、期待していた値にならない現象が起こるのです。

実際にやってみましょう。やることは、

実行する処理
  • sumの値を0にセット
  • sumの値に+1を10000回実行する関数add_sum()を用意
  • スレッドAとスレッドBを作成し関数add_sum()を実行させる

普通に考えれば、

+1が10000回×2スレッドで+20000なので、sumの値は0から20000になっているはずです。

ですがそうはいかないのです。

C++
#include <iostream>
#include <thread>
using namespace std;

int sum = 0;

void add_sum()
{
	for (int i = 0; i < 10000; i++)
		sum++;
}

int main()
{
	//初期値を表示
	cout << "スレッド作成前 sum:" << sum << endl;

	//スレッド作成
	thread threadA(add_sum);
	thread threadB(add_sum);

	//スレッド終了待ち
	threadA.join();
	threadB.join();

	//結果を表示
	cout << "スレッド終了後 sum:" << sum << endl;

	return 0;
}
実行結果
スレッド作成前 sum:0
スレッド終了後 sum:13362

ですが結果は残念なことに「13362」。(実行するごとに変わる)

実際に皆さんも以下から何回か試してみてください。実行するごとに違う値が表示されるはずです。

参考 スレッドによる誤差の確認Wandbox

これはどうしてでしょうか?
説明しましょう。

まず、ちょうどスレッドが切り替わるタイミングで、変数sumの値が「5000」だったとして考えましょう。

その状況でスレッドAがまずsumの値を「5000」として一時保存します。

つぎにスレッドBに切り替わりsumの値「5000」に+1して変数sumに代入します。

そうすると、sumの値は「5001」になりますよね。

実は、この状況でスレッドBがいくらsumの値を変更しても意味がないのです。

それは、次にまたスレッドAに切り替わる時にわかります。

スレッドAは先ほど一時保存していたsumの値「5000」をもとに処理を開始します。

つまり、「5000」に+1をした「5001」をsumに代入するのです。

そうすると、スレッドBで+1という操作をいくら行おうと、このスレッドAの処理で5001になってしまうのです。

おかしいですが、スレッドの切り替えとリソースの一時保存のおかげでこのような現象が起こってしまうのです。

これが銀行のシステム内で起きたら大変ですね。

2つの100万円の振り込みが起こったのにもかかわらず、片方しか反映されないなんてなったら、消えてしまった100万円をどうするのでしょうか。

内容はちょっと違えど、以下のようなことです。

参考 2億円の入金のはずが...Wandbox

マルチスレッド関連の用語

ここでは、マルチスレッド関連でよく使われる用語について軽く触れておきます。

シングルスレッド

シングルスレッドとは、そのままの意味で、1つのスレッドしか持たない実行環境のことです。

処理は順序通りに行われるので、マルチスレッド特有の危険性がありません。

並列処理と逆の意味で、「逐次処理」といえばよいでしょうか。

マルチスレッド

マルチスレッドとは、複数のスレッドが並列で処理される実行環境のことです。

リソースの管理をしっかりと行わないと、再現性の低いバグ(稀に起こるバグ)を発生させる危険性があるので、注意が必要です。

スレッドセーフ

スレッドセーフとは、「マルチスレッドの実行環境でも大丈夫」という意味です。

先ほどのようなバグが起こらないように開発するということです。

例えば、複数のスレッドから同時にアクセスされる可能性があっても、読み取りのみであれば問題ありません。

つまり、読み取り専用のリソースはスレッドセーフといえます。

一方、値の変更が起こるリソースは、排他制御などで管理を行います。

排他制御

排他制御とは、あるスレッドがリソースにアクセスしているときは、ほかのスレッドがアクセスできないようにロックをかけることです。

ようは、変数に「スレッドAが使用中」とか「使用可能」とかの情報をつけるみたいなことです。

使用中であれば、ほかのスレッドは使用可能になるまで待つことになります。

こうすることで1つのリソースに複数の書き込みが同時に起こることを防止します。

シングルトン

シングルトンとは、インスタンスが1つしか生成できないように制限するという「デザインパターン」です。

つまり、全スレッドが同じインスタンスを共有するということになります。

これは、インスタンスを複数作ってしまうと不都合な場合に使われます

例えば、インスタンス作成の処理が重い場合などです。

スレッドローカル

スレッドローカルとは、各スレッドが持つローカルな記憶領域です。

各スレッドが持っている作業スペースのようなものです。

マルチスレッドでは、各スレッドが一度この記憶領域に変数をREADして、処理を加えてからWRITEします。

先ほどの例のように、READとWRITEの間にほかのスレッドが行った処理は反映されないので、注意が必要ということなのです。

まとめ

プロセスとスレッド、そしてマルチスレッドの危険性について書きました。

この辺はややこしくて頭が痛くなりそうですね。

自分も間違ってる点などあるかもしれないので、遠慮せずにご指摘いただければと思います。

以上「スレッドとプロセスの違いとは?マルチスレッドプログラミングの危険性を解説!」でした!

今井

最後までご覧いただきありがとうございます。