トピック

はじめに ページの先頭へ

よい設計技法とは、要求のセットを満たす「最良の」方法を選択する技術です。よい並行システムの設計技法は、多くの場合、並行性のニーズを満たす最も簡単な方法を選択する技術です。設計者にとって最も重要な規則の 1 つは、車輪の再発明を避けることです。ほとんどの問題を解決できるように、優れた設計パターンや設計イディオムが開発されてきました。並行システムの複雑さを考慮すると、十分に証明されたソリューションを使用することと、設計の単純さを追及することだけが意味を持ちます。

並行性の方式 ページの先頭へ

完全にコンピュータの中だけで起きる並行活動のことを、実行スレッドと呼びます。すべての並行活動と同様に、実行スレッドも、時間の経過の中で発生するため、抽象的な概念です。実行スレッドを物理的にとらえる最良の方法は、特定の時間にその瞬間の状態を表現することです。

コンピュータを使って並行活動を表す最も直接的な方法は、各活動に専用のコンピュータを割り当てることです。ただし、これはあまりにも高価であり、常に衝突を解決する役に立つとは限りません。したがって、通常は、一種のマルチタスキングを通じて、同じ物理プロセッサ上で複数の活動をサポートします。この場合、プロセッサや関連するリソース (メモリやバスなど) は共有されます (ただし、このリソース共有によって、本来の問題には存在していなかった新たな衝突が発生する可能性があります)。

マルチタスキングの最も一般的な形態は、各活動への「仮想」プロセッサの割り当てです。この仮想プロセッサは、普通、プロセスタスクなどと呼ばれます。通常、各プロセスは、ほかの仮想プロセッサのアドレス空間とは論理的に異なる、独自のアドレス空間を持ちます。これは、プロセスが互いに衝突して、偶然に互いにほかのメモリを上書きしてしまうのを防ぎます。ただし、物理プロセッサをあるプロセスから別のプロセスに切り替えるために必要なオーバーヘッドは、多くの場合、たいへん大きなものです。これを行うには CPU 内でレジスタのセットを切り替える重大な作業 (コンテキスト切り替え) を伴うため、最新の高速プロセッサを使っても数百マイクロ秒を要します。

このオーバーヘッドを減らすため、多くのオペレーティング システムは、単一プロセス内に複数の軽量スレッドを含める能力を備えています。プロセス内のスレッドは、そのプロセスのアドレス空間を共有します。これにより文脈の切り替えに伴うオーバーヘッドは減りますが、メモリ衝突が起きる可能性は増えます。

高スループットのアプリケーションでは、軽量スレッド切り替えのオーバーヘッドでさえ、受け入れられないぐらいに大きなものになります。このような状況では、アプリケーションのある特殊な機能を活用することで可能となる、さらに軽量なマルチタスキング方式を使うのが普通です。

システムの並行性要求は、システムのアーキテクチャに劇的な影響を与えます。機能を単一プロセス アーキテクチャからマルチプロセス アーキテクチャに移行させる決断をすると、多くの次元からシステムの構造に重大な変更がもたらされます。付加メカニズム (リモート プロシージャ呼び出しなど) を導入する必要がある場合には、システムのアーキテクチャが大幅に変更される可能性があります。

追加プロセスと追加スレッドを管理する余分なオーバーヘッドだけでなく、システムの可用性要求も検討する必要があります。

アーキテクチャに関するほとんどの決定と同様に、プロセス アーキテクチャを変更する際にも、事実上、ある一連の問題を別の問題に置き換えることになります。

方式

利点

欠点

単一プロセス、スレッドなし
  • 単純である
  • プロセス内のメッセージ処理が速い
  • 作業負荷のバランスを取るのが難しい
  • マルチプロセッサに移行できない
単一プロセス、マルチスレッド
  • プロセス内のメッセージ処理が速い
  • プロセス間通信のないマルチタスキングである
  • 「重量」プロセスのオーバーヘッドがない優れたマルチタスキングである
  • アプリケーションは「スレッドセーフ」でなければならない
  • オペレーティング システムには、効率のよいスレッド管理機能が必要である
  • 共有メモリ問題を検討する必要がある
マルチプロセス
  • プロセッサを追加することで拡張できる
  • ノード間の分散が比較的容易である
  • プロセス境界に敏感である。プロセス間通信を多用すると性能に悪影響を及ぼす。
  • スワッピングとコンテキスト切り替えが高くつく
  • 設計が難しい

典型的な発展経過は、単一プロセス アーキテクチャから出発し、同時性を必要とする動作のグループにプロセスを追加していくというものです。このような広いグループの中で、さらに並行性の必要性について検討し、プロセス内にスレッドを追加して並行性を拡大します。

最初の出発点は、目的に合わせて作られたアクティブ オブジェクト スケジューラを使用して、オペレーティング システムの 1 つのタスクまたはスレッドに、多数のアクティブ オブジェクトを割り当てることです。これにより、通常は、非常に軽量な並行性のシミュレーションを達成できますが、オペレーティング システムの単一のタスクやスレッドを使用するので、複数 CPU によるメカニズムの利点を活用することはできません。独立したスレッド内で起きるブロッキング動作を切り離して、ブロッキング動作がボトルネックにならないようにすることが重要な決定となります。結果として、ブロッキング動作を伴うアクティブ オブジェクトを分離し、専用のオペレーティング システム スレッドにします。

問題点 ページの先頭へ

残念なことに、アーキテクチャ上の多くの決定と同様に、簡単な答えはありません。正しいソリューションには、注意深くバランスの取れた方法が必要です。アーキテクチャに関する小規模なプロトタイプを使用して、特定の選択の影響を調査できます。プロセス アーキテクチャのプロトタイプを作る場合には、プロセスの数をシステムに対する理論的な上限まで増やすことに焦点を当てます。以下の問題点を検討します。

  • プロセスの数を上限まで増やせるか。上限を超えてシステムをどこまで拡張できるか。成長の可能性に対する余裕はあるか。
  • 一部のプロセスを、共有プロセス アドレス空間で動作する軽量スレッドに変更すると、どのような影響があるか。
  • プロセスの数を増やすと、応答時間はどうなるか。プロセス間通信 (IPC: Inter-Process Communication) の量は増えるか。目立った性能劣化はあるか。
  • プロセスの結合や再編成で、IPC の量を減らすことができるか。このような変更を行うと、負荷分散が困難な大きい一枚岩的プロセスになってしまわないか。
  • IPC を減らすために共有メモリを使用できるか。
  • 時間リソースを割り当てるとき、全プロセスに「等しい時間」を割り当てる必要があるか。時間割り当てを実行することは可能か。スケジュール優先度の変更に、潜在的な問題点はあるか。

オブジェクト間通信 ページの先頭へ

アクティブなオブジェクト同士は、同期または非同に期通信を行うことができます。同期通信は、厳密に順序を制御することによって複雑なコラボレーションを単純化でき、便利です。つまり、あるアクティブなオブジェクトが、ほかのアクティブ オブジェクトを同期して呼び出す必要のある「RTC 列」を実行している間は、ほかのオブジェクトが開始したいかなる並行動作の相互作用も、そのシーケンスが完全に完了するまでは無視できます。

これは役に立つ場合もある一方で、より重要な高優先度のイベントが待たされることもある (優先度の逆転現象) ため、問題もあります。同時に呼び出されたオブジェクトそのものが、自分自身の同期呼び出しへの応答待ちでブロックされる可能性があるため、事態はさらに悪化します。これは、無制限な優先度逆転現象につながる可能性があります。最も極端なケースでは、同期呼び出しのチェーンに循環性があると、デッドロックにつながる可能性もあります。

非同期呼び出しでは、応答時間を制限できるようにすることで、この問題を回避しています。ただし、ソフトウェア アーキテクチャによっては、非同期通信はしばしばより複雑なコードにつながることがあります。これは、アクティブ オブジェクトは複数の非同期イベントにいつでも対応しなければならないためです (これらの各非同期イベントによって、ほかのアクティブ オブジェクトとの間で非同期反復の複雑な順序が強いられることがあります)。これは非常に困難で、実装時エラーの温床となります。

メッセージ配信が保証されている非同期メッセージング技術を使用すれば、アプリケーションのプログラミング作業が簡単になります。アプリケーションは、ネットワーク接続またはリモート アプリケーションが利用できなくなった場合でも、処理を続行できます。非同期メッセージングは、同期モードでもその使用を妨げられません。同期技術では、アプリケーションが利用可能なときには常に接続が利用可能でなければなりません。接続が存在することがわかっていれば、コミット処理がより簡単になります。

実際的な考慮 ページの先頭へ

アクティブ オブジェクトのコンテキスト切り替えのオーバーヘッドは非常に小さい場合がありますが、アプリケーションによっては、そのコストでさえ受け入れられない可能性があります。通常、これは、大量のデータ処理を高速に行う必要がある状況で発生します。このような場合には、受動オブジェクトや、セマフォのようなさらに伝統的な (ただしリスクの高い) 並列管理技法を使うような状態に、後退させられることがあります。

ただし、このような検討をしても、アクティブ オブジェクトのアプローチを完全に捨て去る必要があることを意味するわけではありません。このようなデータ中心のアプリケーションでも、システム全体から見れば、性能を気にする部分は相対的に小さい場合が多いものです。つまり、システムの残りの部分では、アクティブ オブジェクトのパラダイムをまだ活用できるということです。

システム設計という観点からみると、一般に、性能は設計基準の 1 つにしか過ぎません。システムが複雑になると、保守性、変更の容易さ、理解しやすさなどの別の基準も、より重要ではないにしても、同じ程度に重要です。アクティブ オブジェクトのアプローチは、明確に優れた点を備えています。このアプローチは、並行性と並行性管理の複雑さのほとんどを隠蔽する一方で、低レベルの技術特有のメカニズムではなく、アプリケーション固有の用語で設計を表現できるようにするからです。

発見的方法 ページの先頭へ

並行コンポーネント間の相互作用に注目するページの先頭へ

相互作用のない並行コンポーネントは、ほとんど取るに足らない問題です。ほとんどすべての設計上の問題が並行活動間の相互作用と関係しているため、まず、相互作用を理解することにエネルギーを集中させる必要があります。次のような質問をします。

  • 相互作用は、1 方向か、双方向か、多方向か
  • クライアント / サーバー関係またはマスター / スレーブ関係があるか
  • 何らかの形式の同期が必要か

相互作用について理解したら、これを実装する方法について考えることができます。実装方法としては、システムの性能目標に合致する最も単純な設計を産み出すものを選択する必要があります。一般に、性能要求には、全体的なスループットと、外部生成イベントに許容範囲内の遅れで対応できることが含まれます。

外部インターフェイスを分離してカプセル化する ページの先頭へ

外部インターフェイスに関する特定の仮定を、アプリケーションのあちこちに埋め込むのは悪い慣習です。また、複数の制御スレッドをイベント待ちにしてブロックするのは非効率的です。代わりに、イベント検出専用タスクを 1 つのオブジェクトに割り当てます。イベントが発生すると、そのオブジェクトは、イベントの発生を知る必要があるほかのオブジェクトに通知できます。この設計は、よく知られていて認知されている設計パターンである、「観察者」パターンに基づいています [GAM94]。これを、より柔軟性の高い「パブリッシャ - サブスクライバ パターン」に、簡単に拡張できます。ここで、パブリッシャ オブジェクトは、イベント検出者とイベントに興味のあるオブジェクト (「サブスクライバ」) との間の仲介者として働きます [BUS96]。

ブロッキング動作とポーリング動作を分離してカプセル化する ページの先頭へ

システムにおけるアクションは、外部で生成されたイベントの発生によってトリガされます。非常に重要な外部生成イベントの 1 つは単純な時間の経過で、クロックの時の刻みで表されます。その他の外部イベントは、ユーザー インターフェイス装置、プロセス センサー、ほかのシステムへの通信リンクなどのような外部ハードウェアに接続されている、入力デバイスからのものです。

ソフトウェアがイベントを検出するには、割り込み待ちにして動きを止めておくか、またはイベントが発生していないかどうかを確認するため、定期的にハードウェアをチェックする必要があります。後者の場合には、定期的なサイクルを短くして、短命なイベントの見落としや多重発生を防いだり、単にイベントの発生と検出の間の遅延を最小にしたりすることが、必要になる場合があります。

このことに関して興味深いのは、どんなにイベントの発生が稀であっても、イベントの発生を待機したり、頻繁にチェックしたりして、何らかのソフトウェアをブロックしなければならないことです。しかし、システムが処理する必要のあるイベントの多数 (ほとんどでない場合) は稀にしか発生しません。どんなシステムでも、大部分の時間は、重要なことはほとんど何も発生しません。

エレベータ システムは、このよい例を多数提供してくれます。エレベータの運転の中で重要なイベントには、エレベータ サービスの呼び出し、乗客による階の選択、乗客が手でドアを遮る動作、ある階から次の階への通過などがあります。これらのイベントの中には非常に俊敏な応答が必要なものがありますが、望ましい応答時間の規模と比べるとすべてがきわめて稀なイベントです。

1 つのイベントが多数のアクションを引き起こしたり、アクションが各種オブジェクトの状態に依存したりすることがあります。さらに、システムの構成が異なれば、イベントが同じでも使い方が異なる場合があります。たとえば、エレベータがある階を通過するときには、エレベータ ケージの中の表示を更新し、エレベータ自身が自分の場所を知って、新しい呼び出しがかかった場合や乗客が降りる階を選択した場合の応答方法を知る必要があります。各フロアには、エレベータの現在の場所を表示するものがある場合とない場合があります。

ポーリング動作より反応型動作を使用する ページの先頭へ

ポーリングは大きな代償を必要とします。これを実現するには、システムの一部の動作を定期的に停止して、イベントが発生しているかどうかをチェックする必要があります。イベントに迅速に応答する必要がある場合には、システムは非常に頻繁にイベントの到着をチェックする必要があり、システムが達成できる仕事量にさらに制約を加えます。

イベントに割り込みを割り当て、割り込みによってイベント依存のコードを起動させる方が、はるかに効率的です。割り込みも「高価」と考えられるので、割り込みを避ける場合がありますが、ほどよく割り込みを使う方がポーリングを繰り返すよりはるかに効率的です。

イベント通知メカニズムとして割り込みの方が好まれるケースとしては、イベントの到着がランダムでたまにしか起きず、ポーリング作業のほとんどがイベント未発生の結果に終わるような場合です。ポーリングの方が好まれるケースは、イベントが定期的かつ予想可能な形で起きる場合で、ポーリング作業のほとんどでイベント発生を見つけられる場合です。この中間で、ポーリングでも反応型動作でもどちらでもかまわない場合があります。どちらでもうまくいき、選択にあまり重要性がない場合です。ただし、実世界で起きるイベントのランダム性を考えると、ほとんどのケースでは反応型動作が適しています。

データのブロードキャストよりイベント通知を使用する ページの先頭へ

データのブロードキャストは (通常はシグナルを使う)、多くのリソースを必要とします。また、たいていは無駄が多くなります。そのデータに興味があるのはほんの一握りのオブジェクトですが、全部 (または多数) が作業を停めて、そのデータを調べる必要あります。よりよい、リソースの浪費の少ない方法は、あるイベントが発生することに興味があるオブジェクトだけに知らせる通知を使うことです。イベントのブロードキャストは、多数のオブジェクトの注意を促す必要のあるイベント (通常は、タイミング イベントまたは同期イベント) だけに限定します。

軽量メカニズムを多く使用し、重量メカニズムはあまり使用しない ページの先頭へ

より具体的には次のようになります。

  • 並行性ではなく、瞬間的な応答が必要な場合には、受動オブジェクトと同期方法の呼び出しを使用します。
  • アプリケーション レベルでの並行性の概念が必要なほとんどの場合には、アクティブなオブジェクトと非同期メッセージを使用します。
  • ブロッキング要素を切り離すには、OS のスレッドを使用します。アクティブ オブジェクトは、OS のスレッドにマップすることができます。
  • 最大限の分離を行うには、OS のプロセスを使用します。プログラムの起動と終了を独立して行う必要があったり、サブシステムを分散させる必要がある場合には、別々のプロセスが必要です。
  • 物理的な分散や強力なパワーが必要な場合には、使用する CPU を分けます。

おそらく、効率的な並行アプリケーションを開発するのに最も重要なガイドラインは、最軽量の並行メカニズムを最大限に使用することです。並行性をサポートする上で、ハードウェアとオペレーティング システム ソフトウェアは両方とも主要な役割を果たしますが、共にかなりの重量メカニズムであって、アプリケーション設計者がやるべきことがたくさん残ります。利用可能なツールと、並行アプリケーションのニーズとの間の大きなギャップを埋める必要があります。

アクティブ オブジェクトは、このギャップを次の 2 つの中心となる特性で埋める役目をします。

  • アクティブ オブジェクトは、OS または CPU が提供する基本的メカニズムのどれを使っても実装可能な並行性の基本単位 (制御スレッド) をカプセル化して、設計上の抽象概念を統合します。
  • アクティブ オブジェクトが 1 つの OS スレッドを共有する場合、アクティブ オブジェクトは非常に効率的で軽量な並行メカニズムとなります。これがなければ、アプリケーションで軽量な並行メカニズムを直接実装する必要があります。

アクティブ オブジェクトは、プログラミング言語が提供する受動オブジェクトにとっても理想的な環境です。プログラムやプロセスのような手続き的成果物を使わずに、並行オブジェクトの基本からシステム全体を設計すると、モジュール性が高く、まとまりがあり、理解しやすい設計につながります。

性能に対する偏狭な考え方を排除する ページの先頭へ

ほとんどのシステムでは、コード全体の 10% 未満が CPU サイクルの 90% 以上を使用します。

システム設計者の多くは、すべてのコード行を最適化する必要があるものと考えて行動します。そうではなく、最も頻繁に実行されたり、長い時間がかかっている 10% のコードを最適化するために、時間を費やす必要があります。その他の 90% については、理解可能性、保守性、モジュール性、実装の容易性を重視して設計します。

メカニズムの選択 ページの先頭へ

システムの機能外要求やアーキテクチャは、リモート プロシージャ呼び出しを実装するために使用するメカニズムの選択に影響を与えます選択肢の間のトレードオフの概要を以下に示します。

メカニズム 用途 コメント
メッセージング 企業サーバーへの非同期アクセス メッセージング ミドルウェアは、キューイング、タイムアウト、リカバリ / 再開状況を処理することで、アプリケーションのプログラミング作業を簡単にします。また、メッセージング ミドルウェアは疑似同期モードで使用することもできます。一般に、メッセージング技術では大きなメッセージ サイズをサポートできます。一部の RPC 方式ではメッセージ サイズに制限があり、大きいメッセージを処理するには追加のプログラミングが必要になります。
JDBC/ODBC データベースの呼び出し 同じサーバーまたは別のサーバーにあるデータベースへの呼び出しを行うために、Java サーブレット用またはアプリケーション プログラム用の、データベースに依存しないインターフェイスがあります。
ネイティブ インターフェイス データベースの呼び出し 多くのデータベース ベンダーは、ネイティブなアプリケーション プログラム インターフェイスを独自のデータベースに実装し、アプリケーションの移植性を犠牲にして、ODBC より優れた性能を提供しています。
リモート プロシージャ呼び出し リモート サーバー上のプログラムの呼び出し この処理を行うアプリケーション ビルダがある場合には、RPC レベルでのプログラムは不要です。
対話的 e-ビジネス アプリケーションではあまり使用されない 一般に、APPC や Socket などのプロトコルを使用した低レベルのプログラム間通信です。

まとめ ページの先頭へ

多くのシステムでは並行動作と分散コンポーネントが必要です。ほとんどのプログラミング言語は、このような問題のどれについてもほとんど助けてくれません。これまで、アプリケーションでの並行性のニーズと、それをソフトウェアで実装するオプションの両方を理解するための、よい抽象概念が必要なことを見てきました。また、逆説的になりますが、並行ソフトウェアは本質的に非並行ソフトウェアよりも複雑になる一方で、実世界の並行性に対処する必要があるシステムの設計を、大幅に単純化することが可能なことも見てきました。



Rational Unified Process   2003.06.15