マルチスレッド

恐竜本4章です。

マルチスレッドの利点

 マルチスレッドはプロセスよりも軽量な非同期処理単位であり、次のような利点を持つ。

  • 応答性:長い時間がかかる処理の間にユーザー操作を受け付けられる
  • リソース共有:プロセスを作って並列処理する場合、メモリの内容をすべてコピーして、通信もプロセス間通信手法を使う必要があったが、マルチスレッドだと各スレッドは基本的にメモリやリソースを共有している
  • 経済性:プロセスを作成するためにはメモリの割当やリソースの割当が必要だが、スレッドはリソースを共有するので低コストで作成できる。
  • スケーラビリティ:スレッドをマルチコア環境で別コアで実行することができ、処理能力が向上する。1

マルチコア環境でのプログラミング

シングルコアでの非同期処理は、コアの割当を時分割しているだけである一方で、マルチコアを使用すると、処理を同時にコア数分だけ走らせることができる。しかしながら、これを実現できるのは以下の課題をクリアした場合に限る。

  • アクティビティの分割:マルチコアを活用するためにはアプリケーションを並列実行できる領域に分割する必要がある
  • バランス:各タスクの負荷が偏っており、どれかの負荷が軽いとわざわざコアを割り当てるコストに見合わない
  • データの分割:処理を行うデータの領域も分割が必要である
  • データの依存関係:あるタスクのデータに別のタスクが依存している場合には同期が正しく取れいていることを保証しなければならない
  • テストとデバッグ:マルチコアでプログラムが動いていると実行経路が複数存在するのでテスト・デバッグが難しくなる

マルチスレッディングのモデル

スレッドにはユーザースレッドとカーネルスレッドとがある。前者はユーザーモードで実行されるのに対し、後者はカーネルモードで実行される。また、カーネルスレッドはOSのスケジューラにもとづき、マルチコアを活用して実行されるのに対し、ユーザースレッドはOSのスケジューラを使えず、マルチコアでの実行はできない2。ユーザースレッドとカーネルスレッドを対応づけるパターンは複数存在する。

  • 多対1:多数のユーザースレッドを1個のカーネルスレッドに対応付ける。どれか一つのスレッドがブロックするシステムコールを呼ぶと、全スレッドがそのシステムコール終了まで止まる。
  • 1対1:ユーザースレッドをカーネルスレッドと1対1で対応付ける。コアにまたがる並列実行が可能だが、カーネルスレッドの作成はオーバーヘッドが比較的大きいのでパフォーマンスの低下が起こりうる。このモデルを実装している場合、システムによってスレッド数の制限が課される事が多い。
  • N対M:N個のユーザースレッドをM個のカーネルスレッドに対応付ける。ユーザースレッドの数に上限が課されず、どれかのスレッドがシステムコールを呼んでも別のスレッドの実行を妨げない。

スレッドライブラリー

スレッドライブラリーはプログラマーがスレッドを作ったり管理するために使うライブラリーである。POSIXのPthreads、Win32 thread libraryなどがある。Javaのスレッドライブラリーなどは内部でホストシステムのスレッドライブラリーを呼び出している。

  • Pthreads:スレッド作成と同期に関して定めたPOSIX標準。標準なので、Pthreadsを直接使うというより、ユーザは各OSのPthreads実装を使うことになる。
  • Win32 Threads:Windowsで実装されているスレッドライブラリ。

スレッドに関する検討事項

  • fork()とexec():マルチスレッドプログラムでforkを呼び出したとき、新たに作られるプロセスはスレッドを全部引き継ぐべきか、引き継がないべきか。これは、execを呼んでいるか否かで(スレッドライブラリが?コンパイラが?)判断する。execはマルチスレッドだろうとシングルスレッドだろうと通常通り指定されたプログラムをメモリに読み込んでプロセス全体を置き換えれば良い。execを呼んだ場合複数スレッドがあっても破棄される。そのため、fork後すぐexecを呼んでいるならfork後にスレッドを引き継ぐ必要はなく、execを呼んでいないならスレッドを引き継ぐ必要がある。
  • スレッドのキャンセル:実行されているスレッドをキャンセルする方法として、別スレッドからすぐにスレッドを終了するコマンドを呼ぶ、停止したい側のスレッドがフラグをたて、停止される側のスレッドが定期的に停止フラグを見に行くというものが考えられる。前者では、スレッド固有のリソースはOSが削除するが、システムワイドなリソースは削除されない。そのため、開放されないリソースが残る可能性がある。後者のほうが行儀よくスレッドをキャンセルすることができる。
  • シグナル:ctrl+cやkillではOSからプログラムにシグナルが送信される。UNIX系では各スレッドがどのシグナルを受け取るかを指定できる。ただし、シグナルを複数回処理してはならないので、あるシグナルを受け取れるスレッドが複数存在する場合、OSが最初に見つけたスレッドにシグナルが渡る。WindowsではAsynchronous procedure calls(APC)という仕組みが同等物として存在する。APCはプロセスではなくスレッドを宛先として送られるので、前述の問題は起こらない。
  • スレッドプール:スレッド作成はプロセス作成より軽量と言っても無限に作れるわけではないし、多少の時間がかかる。サーバーなどでトラフィックが急増した場合に毎回スレッドを新規作成するとパフォーマンス低下を引き起こす。そのため、予めスレッドを作ってプールしておき、処理をワーカーに振り分けるという仕組みが考えられ、これをスレッドプールという。Win32 APIではスレッドプールを使う機能が用意されている。
  • スレッド固有のデータ:全データをスレッド間で共有するだけでなく固有のデータを持ったほうが良い場面がある。
  • スケジューラの活性化(scheduler activations):N対Mのマルチスレッドを採用しているシステムでは、Lightweight process(LWP)というデータ構造をユーザースレッドとカーネルスレッドの間に置いている。カーネルはアプリケーションにupcallと呼ばれる方法で、特定のスレッドがブロックされそうになっていることを通知する。その際、カーネルは新規のLWPをアプリケーションに割り当てる。upcallを受け取ったupcall handlerは別のスレッドを割り当てられたLWP上で実行する。ブロックされていたスレッドが実行可能になった場合もカーネルがupcallを行い、新たなLWPを割り当てる。割り当てられたLWP上でupcall handlerは動けるようになったスレッドをどのLWP上で動かすか決める。3

Windowsでのスレッド

Windowsにかぎらず各スレッドが保持するデータ構造としては、

  • スレッドID
  • レジスターのセット
  • ユーザースタック、カーネルスタック
  • プライベートストレージエリア(ランタイムライブラリやDLLが使用)

これらをcontextと呼ぶ。Windowsでは、更に、

  • ETHREAD:スレッドデータ全体の統括
  • KTHREAD:カーネルスレッドブロック
  • TEB:スレッド環境ブロック

というデータ構造がある。ETHREADでは自分が属するプロセスへのポインタや、自分のスレッドの開始ルーチンのアドレス、KTHREADへのポインタを保持する。KTHREADはスケジュール情報や同期情報、TEBへのポインタを保持する。ETHREADとKTHREADはカーネル空間に存在する。一方で、TEBはユーザー空間に存在する。TEBにはスレッド識別子、ユーザーモードスタック、スレッド固有データ(thread-local storage)などが保持されている。

Linuxでのスレッド

Linuxではプロセスとスレッドの区別は明確ではなく、両方「タスク」と呼ばれている。スレッドを作成する際にはclone()関数を呼び出す。引数に、clone呼び出し元と共有するリソースをフラグで指定できる。ファイルシステム情報・メモリ空間・シグナルハンドラ・オープン中のファイルを共有するかどうか選べる。全く共有しないことを選んだ場合、fork関数に似たような振る舞いとなる。

  1. PythonだとGILがあるのでPython以外の言語で実装したときの話ですね
  2. 出典:https://zenn.dev/hsaki/books/golang-concurrency/viewer/kernel
  3. ここの説明では図やフローチャートがなくあまり良くわかりませんでした

コメント

タイトルとURLをコピーしました