Astroの光線のサムネイル。

pubDate: 2024-05-30

author: sakakibara

astro

公開学習

後退

コミュニティ

マルチプロセスとマルチスレッド

並行処理: asyncioでも書いたが、並行プログラミングを行う場合大まかに3つの選択肢がある。

今回はマルチスレッドについて書こうと思う。 マルチスレッドは文字通り、一つのプロセスで複数のスレッドが並行して実行される処理のことであり、一つのプロセスを細かく分割して行う処理のことである。

が、そもそもCPUというものはそのように処理されているものではなかったかと思うかもしれない。

マルチプロセスとマルチスレッドの違いはなんだろうか? そもそもプロセスとは?スレッドとは?

プロセスは実行中の処理のことであり、 プロセスは

などによって構成される。 (なお、プログラム(予定された処理)は実行可能なプロセスのことである)

OSはプロセスを実行状態、待ち状態、実行可能状態などに遷移させることで複数のプロセスを処理している。このようにプロセスを遷移させるトリガーや優先順位を決定するOSの機能をスケジューラという。

単一コアのCPUを一つもつコンピュータでは、同時に一つのプロセスしか処理できず、多くのプロセスは待ち状態としてメモリに保存されていることになる。待ち状態のプロセスはいずれ実行可能状態に遷移し、CPUによって実行される。プロセスはファイルの読み込みや書き込みなどのリソースを待っている間は処理を行えないのでプロセスはリソースが利用可能になるまで待ち状態に入ることになる。OSはリソースが利用可能になったかどうかを(割り込みなどで)確認し、プロセスを実行可能状態へと遷移させる。

ちょっとここでは書ききれないのでどのようにしてプロセスが処理されるのか、OSの役割と仕組みなどについてはまた別の機会に書こうと思う。

プロセスは実行の処理であるが、スレッドはどうだろう。 スレッドとは一つのプロセス内で並列処理を行うための機構であり、あるプロセスの複数のスレッドはプロセスのリソースを共有できる。

OSはスレッド毎にそれぞれ独立したコンテキストを持ち、スレッド間でコンテキストスイッチを行う。これによりスレッドはプロセス内で実行の最小単位として扱われる。

このため、スレッドは他のスレッドが使用したリソース、ファイルハンドラ、スレッド自体を共有することができる。

気をつけるべきは、スレッドやプロセスは、OSがシステムのリソースを管理するために作り出した抽象的な概念である。

実行中のタスク(プロセス、スレッド)の状態を保存し、待ち状態のタスクの状態を復元することでタスクの切り替えを行うプロセスをコンテキストスイッチという。 これにより、複数のタスクが一つのCPUを共有することが可能になる。

マルチプロセスの場合、あるプロセスで使用していたメモリを他のプロセスに侵害させないためにメモリの保全を行う必要がある。この処理が比較的に重たい。(高度なMMUを搭載してる近代のPCにとっては軽いかも) 対して、マルチスレッドの場合、スレッドはコンテキストスイッチの際に保全する内容がプログラムカウンタやスタックポインタなど比較的軽量なため、マルチプロセスよりも高速にスレッドの切り替えを行うことができる。

マルチスタスクの利点

たとえば、受信したデータを処理して表示するようなプログラムだとマルチタスクで書くと非常にシンプルに書くことができる。

その前に、シングルタスクでこのような処理を考えると

while True:
data = receive()
if data is not None:
buffer.push(data)
while True:
result = process(data)
if result is not None:
break
display(result)
if buffer is empty:
continue
data = buffer.pop()
if process
process(data)
display(data)

のようになる。 しかし、マルチタスクを使い、受信して処理して表示するタスクを受信するタスクと処理して表示するタスクに分けると

while True
data = receive()
if data is not None:
buffer.push(data)

while True:
data = buffer.pop()
result = process(data)
display(result)

にわけることができる。 また、受信データが無い間はCPUは処理タスクに全力を割くことができるため、処理タスクの処理が早くなる。

問題となるのが、受信タスクと処理タスクでどのようにしてデータをやり取りするのか(受信バッファを共有するのか)である。

マルチプロセスの場合、プロセス間で共有するメモリをOSに割り当ててもらってそこにバッファをしたり、プロセス間通信を行うことでデータをやり取りしなければならない。 しかし、マルチスレッドの場合、受信スレッドと処理スレッドは同じプロセスで動作しているため、同じメモリ空間を共有している。そのため、スレッド間で直接データをやり取りすることができる。

マルチスレッドに必要なもの

マルチスレッドを使うためには、以下の3つの機能を持つライブラリなりが必要である。

pythonなどではyieldというものがあるが、これは他のスレッドの割り込みを許すという動作を示すものである。 だが、スレッドでは基本的に常に他のスレッドの割り込みを許し、特別な場合のみ割り込みを禁止するというポリシーで動作する。

ただ、基本的にコンテキストスイッチがどこで発生するかはOSのみぞ知るので、スレッドが処理される順番はOSによって決定され、処理の予測が難しい。

マルチスレッドで動作させると正しく動作しないコードはスレッドセーフではないと言われる。

pythonでマルチスレッドを使おうかと思ったが、GILがあるため、マルチスレッドを使ってもCPUを複数のスレッドで共有することができない。そのため、例としてCを使うことにした。

psコマンド

スレッドを認識するにはpsコマンドをつかうことができる。 スレッドを作成するプログラムfirstThreadを実行し、

ps -T

とすると…

PID SPID TTY TIME CMD
63 63 pts/1 00:00:00 bash
194 194 pts/1 00:00:00 firstThread
194 195 pts/1 00:00:00 firstThread
196 196 pts/1 00:00:00 ps

のように表示される。 ここで注目すべきは, SPIDの部分である。これはスレッドIDであり、よく見てみると、PIDと同じ値が表示されていることに気づく。 実はLinuxは内部ではスレッドに対してSPIDをつけている。 プログラムの起動時のメインスレッドにつけられたSPIDPIDとして採用しているのである。 そのため、複数のスレッドをもつプロセスを実行すると、そのスレッドの分だけPIDが非連番に増えていくことになる。 なお、おなじようなことをtop -Hでも確認できる。