MENU
じゃぱざむLINE@バナー
なお
じゃぱざむ運営主
20歳 | WEBメディアの会社で部長をやりながら、じゃぱざむ・特化メディア・WEB制作~コンサル・SEOディレクター・投資・就活相談など幅広くやっています。
今井
エンジニア志望の大学生
21歳 | 独学でWebアプリ開発の勉強をしながら、IT系の記事執筆・アイキャッチ画像やバナー画像などのデザインを行っています。
【5分でわかる】今からプログラミングを学ぶのは遅いのか?

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

未経験からエンジニアを目指す方へ。おすすめのプログラミングスクールのバナー

こんにちは!
今井(@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’を表示する)
#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;
}

[codebox title=”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;
}

[/codebox]

bbbbbbbbbbbaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb...

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

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

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

スレッドの切り替わり|Wandbox

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

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

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

そんな、スレッドの切り替わるタイミングによっては、「複数のスレッドが同じリソースにアクセスしている状況」が発生することがあります。

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

例えば、「同じ変数を参照しているときに、その値を変えようとしたとき」などに起こります。

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

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

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

普通に考えれば、「+1」を10000回実行するスレッドが2個なので、sumの値は0から20000になるはずです。

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

#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の値を一時保存している点です。

sumスレッドAスレッドB
5000sumを読み込み(5000)
5000スレッドBに切り替えスレッドBに切り替え
5000sumは5000(一時保存)sumを読み込み(5000)
5000sumは5000(一時保存)sum+1(5001)
5001sumは5000(一時保存)sumに書き込み(5001)
・・・・・・・・・
6000sumは5000(一時保存)sumに書き込み(6000)
6000スレッドAに切り替えスレッドAに切り替え
6000sum+1(5001)※ここ
5001sumに書き込み(5001)
・・・・・・・・・

この一時保存がスレッドの切り替わりの時に起きてしまうと、このようなバグが起こります。

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

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

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

2億円の入金のはずが…|Wandbox

このようなことが起きないため、スレッドの危険性を排除するようなプログラムを書かなくてはいけません。

そのような設計方法をスレッドセーフといったりもします。

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

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

シングルスレッド

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

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

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

マルチスレッド

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

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

スレッドセーフ

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

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

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

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

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

排他制御

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

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

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

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

シングルトン

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

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

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

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

スレッドローカル

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

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

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

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

まとめ

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

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

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

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

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

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
URLをコピーする
URLをコピーしました!
目次
閉じる