安室 浩和(やすむろ ひろかず)

 大手コンピュータ・メーカー勤務。入社以来10数年をソフトウェア開発の現場で過ごし,その後ソフトウェア品質部へ異動。現場への技術支援や品質教育開発などを主に行っている。「APIで学ぶWindows徹底理解」(日経BP社)などを執筆。

 前回はWindowsのプロセスについて解説した。しかし,実は若干歯切れの悪いところがあった。Windowsにおけるプログラム実行の仕組みを説明するに当たって,もう一つの重要な概念を抜きに語らなければならなかったからだ。その重要な概念というのが,今回のテーマ「スレッド」である。

 Javaプログラミングに詳しい方は,標準でThreadというクラスがあるのをご存知だろう。このことからも分かるように,「スレッド」はWindows固有のメカニズムではない。LinuxなどのUnix系OSにおいても,Pthreadに代表されるスレッド・ライブラリを利用可能である。これらは,実装レベルでの違いはあるものの,基本的に同じコンセプトの機能を実現したものなのだ。前回の「プロセス」と併せて今回の「スレッド」を理解することで,プログラム実行のメカニズムを,より深く理解できるようになるはずである。

スレッドは処理の流れを表すもの

 前回と同じように,まずは言葉の意味から始めよう。「スレッド(thread)」は,英語で「細い線」や「糸」を意味する単語である。「プログラムの実行」というシチュエーションに当てはめると,「プログラムの処理の流れ」を意味する言葉になる。

 例として,C/C++の関数を思い浮かべてほしい。関数は,入り口から実行が始まり,分岐やループなどを経由して,return文で実行を終了する。そこまでの処理を指でなぞっていくと,一本の線になるのが分かるだろう。これがスレッドである(図1)。

図1●スレッドの概念。プログラムの実行をたどると一本の線になる
図1●スレッドの概念。プログラムの実行をたどると一本の線になる

 途中にif文などの分岐があっても,一回の実行ではいずれか一つの選択肢だけが実行されるので,やはり一本の線になる。別の関数を呼び出す場合でも,その関数の中が一本なので,全体としてやはり一本につながる。こうして,main関数の先頭から始まってリターンするまでの処理は,すべて一本の線になるというわけだ。このようなプログラムをシングル・スレッド(単一スレッド)プログラムと呼ぶ。

 わざわざそんな言い方をするくらいだから,複数のスレッドを持つプログラムも存在し得る。複数スレッドからなるプログラムのことを「マルチスレッド・プログラム」と呼ぶ。マルチスレッド・プログラムでは,処理の流れを表す「糸」が一本ではない。プログラム開始時にはメイン・スレッド一本で始まるが,それが途中で枝分かれしていくのである。それぞれの糸は,処理が完了した時点で終わりとなって,それ以上は伸びない。枝分かれした糸は,さらに枝分かれする場合もある(図2)。

図2●スレッドが枝分かれしていく様子
図2●スレッドが枝分かれしていく様子

 もちろん,これらのスレッドを“同時”に実行するには二つ以上のプロセサ・コアが必要だ。さもなければ,前回説明したように,少し実行しては切り替える,といったことを繰り返さなくてはならない。前回,Windowsは短い時間間隔でプロセスを切り替えて実行しているように説明したが,実際にはスレッドを単位に切り替えているのである。

 では,枝分かれしたスレッドが合流することもあるのだろうか。厳密な意味では,スレッドが別のスレッドと合流する仕組みは存在しない。処理の終わったスレッドは,システムから消えてなくなるだけである。

 ただし,処理の流れの上では,合流と同様のことが起こる場合がある。前回,プロセスから別のプロセスを起動して,その別プロセスが終了するまで待つ方法を紹介したのを覚えているだろうか。マルチスレッドでも同じことが可能である。プロセスと同様に,スレッドにも個々を識別するスレッド・ハンドルがあるので,それを前回紹介したWaitForSingleObject APIに渡して処理の終了を待ち合わせればよい。あたかもその場所で二つのスレッドの処理が合流したように見えるケースだ。

 前回説明したプロセスと今回のスレッドの関係がいまひとつ分からない,という人もいるかもしれない。前回,プロセスは「実行ファイルからメモリー上に実体化された,プログラムのインスタンス」だと説明した。言い換えれば,プロセスは実行にかかわるすべてのものを含む概念だと考えてよい。Windowsはプロセスごとにメモリー空間を用意し,ウィンドウや描画に使用するGDIオブジェクトなどもプロセスごとに管理する。こうした実行のコンテキストもすべてプロセスの一部である。

 このプロセスの中で,特に「プログラムの実行」の部分を表すのがスレッドだ。言い換えれば,スレッドはプロセスを構成する一要素である。CreateProcess APIによってプロセスを生成すると,Windowsは必ず同時に一つスレッドを作成し,それがプロセスにおけるプログラムの実行を担うことになる。このスレッドを特に「メイン・スレッド」と呼ぶ。

マルチスレッドが有用な三つのケース

 ところで,ここまで読んできて,なぜマルチスレッドが必要なのか疑問に思う人もいるだろう。プログラムをマルチスレッドにする最大の目的は,複数の処理を並行して実行することだ。そもそも一つのプログラム(プロセス)の中で複数の処理を並行して実行する必要があるのだろうか。

 実際のところ,マルチスレッドでなければ絶対に実現できない処理,というのはほとんどない。しかし,マルチスレッドにすることで,簡単かつ効率的に実装できるケースが少なからずある。ここで,具体的な例をいくつか挙げてみよう。

(1)GUIアプリケーションで,時間のかかる処理を行う場合

 前回触れたように,WindowsのGUIアプリケーションは,「Windowsからのメッセージを受け取り,それに応答する処理を行う」ことを繰り返す「メッセージ・ループ」を中心に動作している(リスト1)。Windowsからのメッセージとは,ユーザーのキー入力やマウス操作,Windowsの終了などをアプリケーションに通知する「システムからのお知らせ」のようなものだ。


int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst,
                   PSTR szCmdLine, int nCmdShow)
{

  // ・・・アプリケーションの初期化処理など(省略)・・・

  MSG msg;  // メッセージ情報を格納する構造体

  // メッセージ・ループ
  // GetMessage APIでメッセージを一つずつ取り出す
  while(GetMessage(&msg, NULL, 0, 0)){
    // 必要に応じてキー入力メッセージなどを変換
    TranslateMessage(&msg);
    // メッセージをあて先のウィンドウ・プロシジャへ送って処理させる
    DispatchMessage(&msg);
  }

  return msg.wParam;
}
リスト1●典型的なメッセージ・ループのコード。GetMessage APIでメッセージを一つずつ取り出し,ウィンドウ・プロシジャに送って処理させる。ウィンドウ・プロシジャの処理が終了するまでDispatchMessage APIからリターンしないため,処理に時間がかかると次のメッセージの処理に進めない

 これらを処理しないまま放置すると,例えば「ウィンドウをマウスでドラッグして移動する」「アプリケーションを終了する」などのユーザーの操作にアプリケーションが反応しなくなる。また,ウィンドウの上に別のウィンドウを重ねて,それをどけたような場合にも,再描画が行われない。アプリケーションが暴走したわけでもないのに,何をしても操作を受け付けず,表示が真っ白になってしまうアプリケーションというのを見たことがあるだろう。あれがまさにその状態である。

 こうしたユーザーの操作に応答しない状態を防ぐには,一つのメッセージに対する処理を短時間で済ませるほかない。いわゆる「0.1秒ルール」である。しかし,アプリケーションで実行したい処理は,必ずしも0.1秒で完了するものばかりではない。例えば,巨大なファイルを読み込む場合や,インターネット経由でサーバーと通信する場合などが典型的である。

 このような場合,シングル・スレッド・プロセスでは一連の処理を細かくぶつ切りにして,少しずつ実行するしかない。ステップを一つ実行してはメッセージ・ループに戻り,処理待ちのメッセージがなければ,またその次のステップの処理をする,ということを繰り返すのだ。実際,マルチスレッドが利用できなかった16ビットWindowsの時代には,こうしたプログラミングが当たり前だった。

 ただ,こうした手法は「どのような単位で処理を分割するか」「前段の処理結果をどのように保存・復元するか」などを工夫する必要があり,プログラムが複雑になりがちだ。さらに,いくら分割しても,場合によっては想定以上に時間がかかってしまう処理(サーバーが応答しないなど)もあり,一筋縄でいく問題ではない。

 そこで代わりによく利用されるのが,時間のかかる処理をメッセージ・ループとは別のスレッドで実行するという,マルチスレッドの手法である。メッセージに対する応答としては,時間のかかる処理を実行するスレッドを起動するだけで,すぐにメッセージ・ループへ戻るようにする。そうすることで,次にメッセージが送られてきたとき(ユーザーが次の操作をしたときなど)にもすぐに応答できるというわけだ。

(2)複数のクライアント要求を受け付けるサーバーの場合

 Webサーバーをはじめとするサーバー・プログラムの場合,一つのクライアントからの要求を処理している間に,別のクライアントの要求を受け付けられないのは問題である。一つの処理に時間がかかる場合や,多数のクライアントが存在する場合,サーバーの接続性や応答性が大幅に下がってしまうだろう。

 この場合もマルチスレッドが便利である。サーバーはクライアント要求を受け付けるたびに,その要求を処理する新しいスレッドを起動して,そのまま再びクライアント要求を待機する状態に戻ることができる。

 もっとも,マルチスレッドといっても結局は順にスレッドを切り替えて実行しているだけなので,あまりたくさんのスレッドを起動してしまうとパフォーマンスがどんどん低下してしまう。したがって,一般にはあらかじめ最大のスレッド数を決めておき,その数を超えた場合にはクライアントに少し待ってもらう,という実装にすることが多い。

(3)複数の処理を同時に実行したいアプリケーションの場合

 そのものずばりの例だが,実際にこうしたアプリケーションは意外と多い。ゲームやマルチメディア・アプリケーションが典型的な例だ。BGMを流しながらアニメーションを行ったり,動画の表示を滑らかにするために,メインの処理とは別にバックグラウンドで前処理(ディスクからの読み込みや変換など)を行ったり,という場合にはマルチスレッドが便利である。また,アニメーションGIFなどを複数表示する際なども,それぞれのGIFのアニメーションを独立したスレッドで動かすようにすれば,プログラムの構成が簡単になる。

 詳しくは別の回に説明するが,マルチスレッドにもそれなりにやっかいな部分がある。したがって乱用するのは禁物だが,シングル・スレッドでは複雑な問題も,マルチスレッドの視点で見るとシンプルに解決できる場合が少なくない。そういう意味でも,一つの実装手段として,マルチスレッドの知識を持っておくことは有用だ。