pubDate: 2024-05-30
author: sakakibara
並行処理: asyncioでも書いたが、並行プログラミングを行う場合大まかに3つの選択肢がある。
今回はマルチスレッドについて書こうと思う。 マルチスレッドは文字通り、一つのプロセスで複数のスレッドが並行して実行される処理のことであり、一つのプロセスを細かく分割して行う処理のことである。
が、そもそもCPUというものはそのように処理されているものではなかったかと思うかもしれない。
マルチプロセスとマルチスレッドの違いはなんだろうか? そもそもプロセスとは?スレッドとは?
プロセスは実行中の処理のことであり、 プロセスは
などによって構成される。 (なお、プログラム(予定された処理)は実行可能なプロセスのことである)
OSはプロセスを実行状態、待ち状態、実行可能状態などに遷移させることで複数のプロセスを処理している。このようにプロセスを遷移させるトリガーや優先順位を決定するOSの機能をスケジューラという。
単一コアのCPUを一つもつコンピュータでは、同時に一つのプロセスしか処理できず、多くのプロセスは待ち状態としてメモリに保存されていることになる。待ち状態のプロセスはいずれ実行可能状態に遷移し、CPUによって実行される。プロセスはファイルの読み込みや書き込みなどのリソースを待っている間は処理を行えないのでプロセスはリソースが利用可能になるまで待ち状態に入ることになる。OSはリソースが利用可能になったかどうかを(割り込みなどで)確認し、プロセスを実行可能状態へと遷移させる。
ちょっとここでは書ききれないのでどのようにしてプロセスが処理されるのか、OSの役割と仕組みなどについてはまた別の機会に書こうと思う。
プロセスは実行の処理であるが、スレッドはどうだろう。 スレッドとは一つのプロセス内で並列処理を行うための機構であり、あるプロセスの複数のスレッドはプロセスのリソースを共有できる。
OSはスレッド毎にそれぞれ独立したコンテキストを持ち、スレッド間でコンテキストスイッチを行う。これによりスレッドはプロセス内で実行の最小単位として扱われる。
このため、スレッドは他のスレッドが使用したリソース、ファイルハンドラ、スレッド自体を共有することができる。
気をつけるべきは、スレッドやプロセスは、OSがシステムのリソースを管理するために作り出した抽象的な概念である。
実行中のタスク(プロセス、スレッド)の状態を保存し、待ち状態のタスクの状態を復元することでタスクの切り替えを行うプロセスをコンテキストスイッチという。 これにより、複数のタスクが一つのCPUを共有することが可能になる。
マルチプロセスの場合、あるプロセスで使用していたメモリを他のプロセスに侵害させないためにメモリの保全を行う必要がある。この処理が比較的に重たい。(高度なMMUを搭載してる近代のPCにとっては軽いかも) 対して、マルチスレッドの場合、スレッドはコンテキストスイッチの際に保全する内容がプログラムカウンタやスタックポインタなど比較的軽量なため、マルチプロセスよりも高速にスレッドの切り替えを行うことができる。
たとえば、受信したデータを処理して表示するようなプログラムだとマルチタスクで書くと非常にシンプルに書くことができる。
その前に、シングルタスクでこのような処理を考えると
のようになる。 しかし、マルチタスクを使い、受信して処理して表示するタスクを受信するタスクと処理して表示するタスクに分けると
と
にわけることができる。 また、受信データが無い間はCPUは処理タスクに全力を割くことができるため、処理タスクの処理が早くなる。
問題となるのが、受信タスクと処理タスクでどのようにしてデータをやり取りするのか(受信バッファを共有するのか)である。
マルチプロセスの場合、プロセス間で共有するメモリをOSに割り当ててもらってそこにバッファをしたり、プロセス間通信を行うことでデータをやり取りしなければならない。 しかし、マルチスレッドの場合、受信スレッドと処理スレッドは同じプロセスで動作しているため、同じメモリ空間を共有している。そのため、スレッド間で直接データをやり取りすることができる。
マルチスレッドを使うためには、以下の3つの機能を持つライブラリなりが必要である。
pythonなどではyieldというものがあるが、これは他のスレッドの割り込みを許すという動作を示すものである。 だが、スレッドでは基本的に常に他のスレッドの割り込みを許し、特別な場合のみ割り込みを禁止するというポリシーで動作する。
ただ、基本的にコンテキストスイッチがどこで発生するかはOSのみぞ知るので、スレッドが処理される順番はOSによって決定され、処理の予測が難しい。
マルチスレッドで動作させると正しく動作しないコードはスレッドセーフではないと言われる。
pythonでマルチスレッドを使おうかと思ったが、GILがあるため、マルチスレッドを使ってもCPUを複数のスレッドで共有することができない。そのため、例としてCを使うことにした。
スレッドを認識するにはpsコマンドをつかうことができる。
スレッドを作成するプログラムfirstThread
を実行し、
とすると…
のように表示される。
ここで注目すべきは, SPID
の部分である。これはスレッドIDであり、よく見てみると、PID
と同じ値が表示されていることに気づく。
実はLinuxは内部ではスレッドに対してSPID
をつけている。
プログラムの起動時のメインスレッドにつけられたSPID
をPID
として採用しているのである。
そのため、複数のスレッドをもつプロセスを実行すると、そのスレッドの分だけPID
が非連番に増えていくことになる。
なお、おなじようなことをtop -H
でも確認できる。