ITシステムの基礎知識を解説(IT素人さん向け)リンクリストに戻る
先日、業務担当(非ITエンジニア)の顧客からこんな相談を受けた。
「メモリ内で複数のデータを処理すると互いのデータが干渉して不整合が起きる危険性があると思います。
DB(DBMSのこと)を使用した方が良いのではないかな」
その時その人が着手していたのはあるデータ処理バッチなのだが
「データ件数が少ないので全ての処理をメモリの中だけで行ってしまった方が早く簡単で良い」
という話が他の情技師から上がっていた。
それに対する顧客の意見である。
提案されていたのはマルチプロセスによる並列データ処理なので、互いのメモリ空間はOSによって保護されているので、互いに干渉することはない。
これがマルチスレッドの場合は互いのメモリ空間を干渉することがある。
この顧客は多分どこかでマルチスレッドの性質を他の情技師(ITエンジニア)から聞いてこの事を覚えていたのだと思う。
今回はこういう顧客のためにマルチプロセス(マルチタスク)とマルチスレッドの違いを非情技(非ITエンジニア)向けに解説したいと思う。
情技師にとっては初歩的な常識なので、この記事を読む価値はない。
Excelで「勤怠管理2019年10月.xlsx」などのファイルを開くと、Excelの画面上にその.xlsxファイルが開く。
メモ帳やペイント、などを開いてもそれぞれ異なるウインドウで対象ファイルが開く。
Word,PowerPointなどでも同じである。
この場合、それぞれの画面がプロセスという名の実行単位となる。
プロセスはタスクとも呼ばれる。
OSやそれぞれの環境によって両者が区別される事もあるが、プロセスとタスクはほぼ同じ物と考えて差し支えない。
プロセスとはOSによって与えられたプログラムの実行単位である。
全ての情報処理はプロセス(タスク)を単位に実行する。
システムを考える場合はプロセスを中心に考えるのが情技師の常識となる。
OSのメモリ管理のしくみ
現代のOSは全て複数のプロセスを同時並列に実行することができる。
プロセスが使用するメモリはOSによって提供される。
OSによって提供された個別のプロセス用のメモリ空間の事をプロセスインスタンスと呼ぶ。
(ちなみにスレッドインスタンスという物はない)
OSによって提供されるプロセスインスタンスはメモリアドレス0から始まる連続したメモリ空間として提供される。
しかし現実の物理的メモリ空間は必ずしも連続したメモリ領域としては空いていない。
500MBのメモリ空間が必要でも、連続して500MB空いている領域が無い場合もある。
しかし、200MB,200MB,100MBの3つの物理メモリが空いていたとする。
この場合、OSは200MB,200MB,100MBの3つの物理メモリをつなぎ合せ、0番から始まる連続した仮想アドレス番号を与えて、プロセスに提供する。
プロセスは処理を実行する時に仮想アドレスでメモリにアクセスする。
OSはその度に仮想アドレスを物理アドレスに変換してメモリにアクセスする。
プロセスは物理アドレスの状態を意識する必要が無い。
従ってプロセスは提供された仮想アドレスの範囲内のメモリ空間にしかアクセスできない。
OSや他のプロセスの使用するメモリ領域はアクセスできないのだ。
これがプロセスインスタンスである。
マルチプロセスとは
現代のOSは全て複数のプロセスを同時並列に実行することができる。
メモリ空間はプロセスインスタンスで各プロセスごとに分割できるが、CPUは一つしか無い。
ではこのCPUはどのようにプロセスごとに分割するのだろう。
結論から言うと時間で分割する。
ハードウェアのタイマーでCPUに一定時間ごとに割り込みを入れ、CPUに割り当てるプロセスを切り替える。
プロセスがA,B,Cと3つあれば、
A → B → C → B → A → B → C → A → C → A → B → C
と15ミリ秒程度でプロセスを切り替える。
順番は決まっていない。
ファイル読み込み待ちなどで、プロセスがCPUを一時解放する場合もあるから順番は決められない。
このようにCPUを複数のプロセスで時分割で共有することをタイムスライスと呼ぶ。
マルチスレッドとは
マルチプロセスとよく似ていて異なるのがマルチスレッドである。
マルチスレッドは一つのプロセスインスタンスの中でファイルIOやキーボードの入力待ちなどが発生したときでも、CPUを待たせずに効率良くCPUを使用する為に、一つのプロセスインスタンスの中で、タイムスライスを使用して複数の処理を並列実行する仕組みである。
一つのプロセスが内部に複数のスレッドを持ち、CPUをタイムスライスで共有して並列実行する。
マルチプロセスと違い、プロセスの中のスレッドはスレッド専用のインスタンスを持たない。
スレッドは直接プロセスインスタンスにアクセスする。
プロセスがアクセスするのはOSによって提供される仮想アドレスだが、スレッドにはそのようなメモリ保護の仕組みが無く、仮想仮想アドレスのようなものはない。
よってスレッドは直接プロセスインスタンスの仮想アドレスにアクセスする。
スレッドが複数あると、互いにメモリの同じ場所にアクセスしてしまうことで、メモリデータの不整合が生じたり、互いに更新待ち(デッドロック)になる場合がある。
だからマルチスレッドでプログラムを作成するときプログラマーはスレッドのメモリ管理に注意して作らなければならない。
スレッドとスレッドの使用しているメモリ領域が重ならないように人間が管理しなければならない。
(あくまで原則としてだがクラスライブラリなどでこの管理を代行してくれる物もある)
プロセスのメモリ管理はOSが自動で行ってくれるが、スレッドのプロセス内のメモリ管理はプログラマーが手作業で管理するコード(プログラム)を書く。
メモリ管理を失敗すると非常に分かりにくいバグが生じる。
発見するのは容易ではない。
またスレッドがプロセスのメモリを干渉するので、スレッドがダウンするとプロセスごとダウンしてしまう。
メモリモデル
プロセスインスタンスの中のメモリ空間は単純化すると次の4つに分かれる。
(1)共有領域
(2)ヒープ領域
(3)スタック領域
(4)コード領域
(1)共有領域
主にグローバル変数の配置場所となる。
プログラムの全ての関数からアクセス可能な領域である。
領域の範囲やサイズを実行中に変更することができない。
(2)ヒープ領域
プログラム実行中に必要なサイズの連続したメモリ領域を確保する為の場所である。
C言語では malloc() という関数によって確保する。
使い終われば実行中に解放する。
大きなメモリ領域を確保するのに向いている。
メモリ管理はプログラマーが手作業でコードに書く。
この領域に確保したメモリ領域を開放しなければ、メモリーリークというメモリ不足状態になる。
メモリーリークは非常に発見しにくいバグである。
(3)スタック領域
主な変数や配列などの領域確保に使用する。
メモリ管理が簡素な仕組みで自動化されており、メモリ管理がやりやすい。
現代のプログラミング言語はほとんどこのスタック領域を使用するように設計されている。
スレッドの場合プロセスが新規スレッドを作成するとき、そのスレッドが使用できるプロセスインスタンスの一部を開始アドレスとサイズを指定して貸し与える。
貸し与える領域は「プロセスインスタンスのヒープ領域」を貸し与えることも可能だし、「プロセスインスタンスのスタック領域」を貸し与えることもできる。
スレッドは全て専用のスタック領域を有することになる。
ただし他のメモリ領域にアクセスすることは可能になっている。
メモリ保護は行われない。
スタックについては以前この記事で解説した。
スタック(LIFO)という概念
(4)コード領域
プログラムの命令部分が格納される領域である。
定数などもこの領域に存在する。
書き換え不可能で、そのサイズも変更されることはない。
命令を保存しているだけなのでその必要が無い。
スレッドセーフ
マルチスレッドのプログラミングではスレッドのメモリ管理をやりやすいようにスレッドセーフという制約を作ることで、各スレッドのメモリ領域を分割して互いに干渉しないようにする。
これはプログラマーが意識的にプログラミングする時に行う。
当然、スレッドセーフを間違えて実装するとスレッド間のメモリ干渉が起きる。
スレッドセーフは、スレッドが使用するメモリ領域の大半をスレッドに割り当てられたスタック領域内に確保する。
ヒープや共有領域も少しは使用するが、これらを使用する時は排他制御の処理が組み込まれる。
通常はあるスレッドが使用している場合は、他のスレッドが使用できない。
ここを多用するとマルチスレッドを使用する意味が無い。
だから使用するメモリ領域の大半にスタック領域を使用する。
各スレッドは作成時に専用のスレッド領域を有するので、スタックを使用している限り、互いのメモリ領域を干渉することはない。
他のスレッドと情報交換する時や、実行状態を記録する場合は、排他制御してヒープや共有領域を使用する。
この制約を守り、作成されたプログラムのソースコードをスレッドセーフ(Thread-safe)と呼ぶ。
ちなみにこの制約を守っていないコードをノンスレッドセーフ(Non Thread-safe)と呼ぶ。
主にシングルスレッド用のコードである。
マルチプロセスのシングルスレッド処理を行うときに使用する。
制約がない方が自由なコードが書けるので必要が無ければスレッドセーフは使用されない。
よく使われるマルチスレッド
マルチスレッドがよく使われるのは、処理中に待機時間が生じる処理である。
実行中の表示でリアルタイムに進捗を「54%終了」のように表示したいとき。
UI(ユーザーインターフェイス)で、ユーザーの入力待ちが生じるが、待ち時間の間も処理を並列実行したいとき。
その逆に、処理実行中でもUIで次の操作を行いたいとき。
DBMSやファイルなどの入出力で待機時間が生じる場合。
CPUの空き時間や待機時間を無くし、CPUを最大限使用して、高速処理を実行したいとき。
複数の端末と繋がっており、どの接続で待機時間が発生するか分からない時。
終わりに
以上、非情技ユーザーが情技師とプロセスやスレッドの会話をするために必要と思われることを説明した。
これで情技師の言っていることがわかるはずだ。
それほど難しい話ではないと思う。
(自分でコードを書くのは難しい)
よく考えると、非情技でも要点を絞って説明すれば理解できることがITにはたくさん有る。
(自分でコードを書くのは難しいが)
そういう知識をもう少し書きたいと思う。
疲れていると中々書けないのだが。