レベル: 初級 Cameron Laird (Cameron@Lairds.com), Vice President, Phaseit, Inc.
2002年 8月 01日 並行性(concurrency) -- 多重処理 (multi-processing) -- について、多くの人々は間違った解釈をしています。今月の「サーバー・クリニック」では、サーバーでの仕事を安全にこなしていくために必要な並行性の基本的な考え方を紹介します。
多重処理という名のもとに、数多の誤解が生じています。ほとんどの大学課程において、また数多くのプログラミングの教科書が、並行性の概念を明確に説明していますが、これは難しいテーマであり、私たちのほとんどが、再教育講習を受けてもよいくらいです。
並行性 (Concurrency)とは、同時に複数の「アプリケーション」が実行している状態のことを指します。ここで「アプリケーション」という言葉を括弧付きで使ったのは、並行性の意味が状況に依存するからです。Linuxのホストは、つねに、そのプロセス・テーブルを、数に差はあるとしても、同時に実行される1群のプログラムで埋めます。ネットワーク・プロトコル・デーモン、cronマネージャー、カーネル自身などです。そして、多くの場合、もっと多くのプログラムがこれに加わります。Linuxは、マルチタスキング・オペレーティング・システムであり、このような働きをするように設計されています。
一般的な単一プロセッサー・ホストでは、複数のタスクが本当に 同時に実行されることはありません。カーネル中のスケジューラーと呼ばれる部分が、すべてのジョブに順番が回るように、ジョブの切り替えを行います。みなさんがプログラムのソースを編集したり、音楽を再生させている間、ブラウザーはダウンロードを行います。並行性は、この外見上の 同時性に、大いに関っているのです。
並行性の2つの側面
並行性についてのユーザーからの視点とプログラミングモデルというのはどちらも、単一資源に対するアクセスのスケジューリングの問題であるということを心に留めておいてください。しかしながらこれを補完する、並行性についての2つめの、裏側から見た意味合いがあります。プロセッサー自体の性能を追求している人々は、こちらの別の側面を強調します。彼らにとっては、「多重処理」ということが意味するところは、一般に、単一のタスクを複数の部分に分割し、複数の中央処理装置 (CPU) が共同して処理できるようにすることです。これは、ハードウェアが余分に必要であったり、プログラミングが複雑なものになったとしても、外部クロックで測定したときに、1個のジョブをより短い時間で完了したいという考え方です。
並行性の2つの側面は、いずれも、スケジューリング、すなわちCPUへのタスクの割り付けと関係することです。また、両方とも、ユーザビリティーに関係してきます。ところが、これら2つの側面は、やっかいなことに、世の中一般に混同されています。初心者のプログラマーは、並行性の最も重要な手法の1つである「マルチスレッド化 (multi-threading)」という手法について、間違った考えを抱く傾向が顕著です。「スレッド化 (threading)」と、しばしば略される手法ですが、誤解には以下のようなものが含まれます。
- スレッド化によって、プログラムを高速に実行できるようになる。
- スレッド化が、並行性の唯一の実現方法である、もしくは唯一の実用的な方法である。
- N-ウェイのホストは、単一プロセシングのホストのほぼN倍の速度で処理を行う。
こうした誤解は、コンセプトをほんの少し明確にするだけで、たちどころに是正される部類のものです。
経験の浅い開発者は、よくこう言います。「私のプログラムは処理が遅すぎる。もっと速くするのために、どうすればスレッド化できるのだろうか。」多くの場合、答は「それは無理」ということになります。既存のシングル・タスキングのアプリケーションを、処理方式を見直さずに、ただ単にマルチタスキングの一部にしても、かえって計算処理が増える だけです。一般に、そのようなプログラムを「スレッド化」すると、処理時間は長く なります。
もちろん、こうした誤解がはびこっているのには理由があります。多くのプログラムは、部品に分解することで、隘路 (bottlenecks) の解消を図ることができます。スペース・シャトルの再突入のシミュレーションのような計算中心のジョブは、1個のCPUではなく8個のCPUにばら撒いて処理すれば、かなり処理時間を短縮できることでしょう。もっと一般的な例としては、入出力による処理の「遮断」が起こらないようにプログラムを再構成するというものです。一般ユーザー・レベル (consumer-level) のアプリケーションで、キーボード入力やディスクからのデータのスワップ・インやネットワークからのメッセージの到着を待機する間、有益な仕事を行える場合には、何も犠牲を払うことなく速度向上できることでしょう。
スレッド化の限界
しかし、こうした速度向上をスレッド化に頼るのは危険です。速度向上には、深い分析が必須です。速度向上は、十分には利用されていない資源を使えるようになったときにのみ可能なのです。その上、スレッド化だけが並行性を実現する方法だというわけではなく、スレッド化が最善の方法でないこともよくあります。
学術文献には、実際に使い物になると思える並行性のモデルが、少なくとも10個以上研究、紹介されています。スレッド化の他にも、プログラム的な意味での多重処理、コルーチン、イベント・ベースのプログラミング、および、おそらく継続や、ジェネレーターも、さらにはもっと難解な仕組みなど、いろいろと耳にしたことがあるのではないでしょうか。たとえば、ジェネレーターをサポートするがスレッドはサポートしていない言語があるときに、ジェネレーターを使ってスレッドのエミュレーションを記述できる (あるいは、その逆も可能) という意味で、これらの手法は、すべて、形式的にほぼ等価です。
もちろん、適切なものをプログラミングするということと、抽象的な意味で等価であることとは違います。信頼性の高いアプリケーションを開発してスケジュールどおりに納品しようという場合、いろいろな並行性モデルの間には、実際的な差異があります。たとえば、スレッド化には、もう何年もの間言われてきている弱点があります。スレッド化の場合、比較的低レベルのプログラミング構成物です。問題を生じずにプログラムを作成することが困難です。スレッドを操るプログラムは、データの不整合性、デッドロック、抜け出せないロッキング、優先順位の逆転に陥りやすい性質があります。Javaは最近になって、スレッド化の性能の問題から、並行性の概念の核としてマルチスレッドだけをサポートするという初期の思想を捨てています。スレッド対応のデバッガーは、悪評を買うほどの高値が付けられています。
ただ、悪いニュースばかりではありません。時間をかけて基本的な考え方を明確に理解するなら、みなさんがXMLやLDAPなどの専門領域で行っている作業と同様、信頼性の高い方法でスレッドを利用することができます。もっとすぐにというのであれば、もっと安全な (場合によっては、もっと高速な) 並行性モデルを、いろいろな状況に合わせて利用することができます。
数多くの状況で、アプリケーションをマルチタスク化するための最善の方法は、アプリケーションを、スレッドではなく、共同して処理をするプロセス に分解することです。プログラマーは、通常、この事実に抵抗します。1つには歴史的な理由からです。プロセスは、スレッドよりも、はるかに「重い」のが常でしたし、それはWindowsタイプのOSでは、今でも当てはまります。しかし、最近のLinuxでは、別々のプロセス間のコンテキスト・スイッチは、それと同等な、同一プロセス内のスレッド間のコンテキスト・スイッチよりも15%しか余計に時間がかからない場合もあります。そうした時間的なコストと引き換えに得られるのは、はるかに理解しやすく、堅固なプログラミング・モデルです。数多くのプログラマーが、独立したプロセスを問題を生ぜずに記述することができます。スレッドを採用した場合、問題を生じないものは、相対的に少なくなります。
では、マルチスレッドではなくマルチプロセスにしたほうがよいのは、どんな場合なのでしょうか。たとえば、「コントロール・パネル」というグラフィカル・ユーザ・インターフェース (GUI) を使って、幾つかの大規模な計算の結果をモニターし、データベースのレコードの読み出しと更新を行い、さらには外部の物理デバイスに関するレポートを表示するものとします。これら全部を1個のプロセスとして処理し、個々のタスクをそれぞれ別個のスレッドで処理することもできるでしょう。Windowsでは、多くの場合、好んでこういう方法がとられます。
しかし、私が開発を行う場合、それぞれのタスクを、それぞれプロセスにして、ソケットやパイプや、ときには共有メモリーを使って相互に交信するようにするというのが、通常のやり方です。こうすると、通常使用するコマンド・ライン・ツールがすべて使用でき、複数のプロセスを自動化できるため、単体テストを非常に簡単化することができます。1個のプロセスがクラッシュしたとしても、他のプロセスに害を及ぼすことがありません。性能は、通常、マルチスレッド化とほぼ同じで、具体的なハードウェアやプログラミング次第ですが、マルチスレッド化よりも高い性能になることもあります。
他の並行性モデル
このようなマルチプロセスの実装は、しばしば、イベント・ベースのプログラミングに依るところが大です。イベントというものは、I/Oや関連するマルチタスキングの処理を管理するのに都合のよい、並行性の明確なひとつのコンセプトです。イベントは、非同期的な「外部の出来事 (externalities)」を、プログラム化されたコールバックに対応付けます (シグナルとかバインディングなどとも言われるものです)。GUIのコントロール・パネルの例で考えてみると、Unixでこれをプログラミングして高い性能のものに仕上げる場合、システム・コールselect() が、データの到着を検出した場合にのみ表示を更新するようにすればよいのです。C指向のプログラマーは、よく、イベント・ベースの手法をselectの観点から捉えています。
みなさんは、「コルーチン」や「ジェネレーター」を、教室で取り上げられる風変わりな手法だと考えているかもしれませんが、これらのメカニズムは、ModulaやIconなどの言語の定義に組み込まれています。マルチタスキング・プログラミングの表現が強力になると同時に、理解しやすく、したがって問題を引き起こさないからです。複雑な性能要件が課せられている場合とか、何百個ものサブタスクを使うことでアプリケーションが最もうまくモデル化できる場合、あるいは、とくに、サーバー・ルームにかなりの数のマルチウェイのホストが収容されている場合には、もっといろいろな並行性モデルについて研究すべきです。それぞれのモデルに、理想的なアプリケーションがあることがわかります。それらのモデルのいくつかが、みなさんのニーズに合っているかもしれません。
また、Linuxで使いたいと思うモデルであれば、どれも、たぶんサポートされていることと思います。後出の参考文献一覧に、さまざまな並行性モデルの実装や実験への参照先を、他の参考文献とともに示しておきます。
マルチウェイの謎
最後にもう1つだけ注意: 多重処理のハードウェア (多くの場合、「シンメトリック・マルチプロセシング (SMP: symmetric multiprocessing)」)でマルチタスキング・ソフトウェアが有効に動作すると決めてかかってはなりません。とくに、Linuxの古いバージョンの時代に、よく、専門技術を駆使して、SMPマシンで有効な結果を上げることが試みられました。Linux 2.4のデフォルトのインストールでも、4個までの (ときには、もっと多くの) プロセッサーを、個々のプロセスに割り当てるという、なかなかの仕事を行っています。ただ、単一のプロセス中のスレッドが、1つのプロセッサーでボトルネックとなっている一方で、他のプロセッサーでは待機状態になっているといったことが起こりえます。他の並行性手法も、同様の問題を起こすことがあります。
このような資源の無駄を避けることは、個々のプラットフォームの具体的な仕様によります。Linux 2.4と一般的なマルチウェイのハードウェアの組み合わせでは、デフォルトの「カーネル・スレッド」(参考文献 のLinuxのスレッド化のFAQを参照) が、スレッドを適切にスケジュールしてくれる、すなわちスレッドを別々のCPUの間で共有するようにしてくれると考えて問題ありません。最良のシステム管理ツールなどを使って、スケジューリングが正しく働いているかどうかを確かめたり、実際のスレッド・スケジューリングについて、Linuxベンダーやユーザーズ・グループに具体的な質問を行ったりしてください。
みなさんのプログラミングの多くの部分は、自然に論理的なタスクに分解されていることと思います。並行性の基本的な考え方を明確に理解すれば、それを、みなさんの要求仕様に合わせて適用できるようになります。並行性は、表の側面と裏の側面の両面を備えていることを忘れないでください。「ユーザーの視点」と「プログラミング・モデル」は、アプリケーションにどのように取り組むかを左右します。「裏の側面」は、ハードウェアへのタスクの割り付けを管理します。機能的な要件と性能面での要件とは、厳密に区別する必要があります。最後に、並行性はスレッド化だけではないということを忘れないでください。明示的にマルチスレッド化を行わないモデルでプログラミングを行い、サーバーを最大限活用できるケースも多いのです。
参考文献
- 私は、comp.programming.threads FAQ で、スタイルについて論争しています。とはいっても、これが貴重な情報源であることに疑問の余地はありません。実際にスレッド・プログラミングを行わなければならない状況にすでにどっぷり浸かっているのであれば、なおさらです。
-
Linux Threads Home Page は、C/C++ 以外の言語もしっかり 認識しています。大きな問題は、数年間、ほとんど更新されていないという点です。とはいっても、ユーザー・レベルおよびカーネル・レベルのマルチスレッド化、協調的なスケジューリングとプリエンプティブなスケジューリング、などについて、貴重な議論を掲載しています。
-
IBM Linuxテクノロジー・センター のメンバーには、次世代型POSIXスレッド化 (Next Generation POSIX Threading) プロジェクトの開発に加わっている者が多数います。これについては、NGPTのホームページ に詳細が示されています。
- CameronがこれまでdeveloperWorks に寄稿したコラムは、以下のとおりです。
- その他、developerWorks には、いろいろな視点が紹介されています。
- developerWorks のLinuxゾーンには、他にもLinux関係の記事が多数掲載されています。
著者について  | 
|  | Cameronは、Phaseit, Inc. の常勤のコンサルタントです。オープン・ソースなどの技術的なトピックについて、数々の執筆や発言を行っています。Cameronのメール・アドレスはclaird@phaseit.net です。 |
記事の評価
|