: MSDN Magazine October 2007

October 2007
OCTOBER 2007 ボリューム 21 番号 10

• プールされたスレッド: 新しいスレッド プール API でスケーラビリティを強化する
Robert Saccone

• WPF のスレッド: Dispatcher を使用して応答性の高いアプリケーションを構築する
Shawn Wildermuth

• 並列 LINQ: マルチコア プロセッサでクエリを実行する
Joe Duffy および Ed Essey

• 並列パフォーマンス: マルチコア コンピュータ用にマネージ コードを最適化する
Daan Leijen および Judd Hall

• モバイル アプリケーション: 周りのノイズに応じて着信音の音量を調整する
Chris Mitchell

Code Download (1483 KB) HTMLHelp Format
コラム

• CLR 徹底解剖: IronPython と動的言語ランタイム
Bill Chiles

• 基本的な本能: Visual Basic 2008 における型の推定
Bill Horst

• データ ポイント: 共通テーブル式
John Papa

• Cutting Edge: AJAX アプリケーション アーキテクチャ、第 2 部
Dino Esposito

• テストの実行: Windows XP Embedded におけるテスト自動化
Dr. James McCaffrey および Mike Hall

• 基礎: WPF の 3D テキスト
Charles Petzold

• Windows と C++: タスク スケジューラ 2.0
Kenny Kerr

• Netting C++: EEK! のエコシステムを記述する
Stanley B. Lippman

• .NET の問題: デッドロック モニタ
Stephen Toub

• { End Bracket }: 結婚にまつわるマッシュアップ
Michael Richter

各出版物

• 編集者のメモ: 複雑さの複雑さ
Howard Dierking

• ツールボックス: データベース管理、手軽な FTP、およびクラスタ キャッシュ
Scott Mitchell


プールされたスレッド: 新しいスレッド プール API でスケーラビリティを強化する

プールされたスレッド

新しいスレッド プール API でスケーラビリティを強化する

Robert Saccone
この記事の一部は、Windows Server 2008 のプレリリース バージョンに基づいています。ここに記載されている詳細情報は、変更される場合があります。
この記事の内容 : :
  • スレッド プールの機能
  • 従来のスレッド プールの制限
  • 新しいスレッド プール API
  • スレッド プール オブジェクト
この記事は次のテクノロジを使用しています:
Windows Vista、Windows Server 2008
コンテンツ
スレッド プールの概要
スレッド プール オブジェクト
コールバック環境オブジェクト
作業オブジェクト
待機オブジェクト
タイマ オブジェクト
I/O 完了オブジェクト
クリーンアップ グループを使用してクリーンアップを単純化する
コールバック インスタンスの API
サンプル アプリケーションで実際に試してみる

先ごろリリースされた Windows Vista® と、間もなくリリースされる Windows Server® 2008 では、マネージ アプリケーション開発者のための新しいテクノロジが Windows® プラットフォームに数多く導入されています。その数があまりに多いために、ネイティブ Windows 開発者にとっても恩恵のある機能強化は見過ごされがちになっています。たとえば、Windows 2000 のリリース以降 Windows プラットフォームの一部となったスレッド プール コンポーネントでは、アーキテクチャの徹底的な見直しが行われています。この新しい実装に含まれる新しいスレッド プール API を使用すると、従来よりはるかに簡単に正しいコードを記述できます。既存のアプリケーションを引き続き実行できるよう、従来の API もまだサポートされていますが、これから説明するように、新しい API に移行すると数々のメリットがあります。

スレッド プールの概要

新しい API の説明に入る前に、スレッド プール コンポーネントによってどのような機能が提供されるのかをおさらいしておきましょう。一言で言えば、スレッド プールを使用すると、関数を周期的に呼び出したり、非同期に呼び出したり、カーネル オブジェクトがシグナル状態になったときに呼び出したり、非同期 I/O 要求が完了したときに呼び出したりすることができます。それらのコールバック関数を呼び出す必要が生じるたびに、コールバック関数を呼び出すための要求が、スレッド プールによって管理されている先入れ先出し (FIFO) キューの末尾に追加されます。その後、スレッド プールによって内部で管理されているワーカー スレッドがそのキューからアイテムを削除して、コールバック関数を呼び出します。ワーカー スレッドの有効期間はスレッド プールによって管理されるため、アプリケーション開発者がスレッドの有効期間を明示的に管理する必要はありません。したがって、TerminateThread 関数を呼び出したり、ワーカー スレッドで実行されている関数から ExitThread 関数を呼び出したりして、アプリケーションでスレッド プールのワーカー スレッドを終了するべきではありません。

長時間にわたってイベントを待機したり、周期的にアクティブになって何らかの状態をポーリングしたりする専用のスレッドをアプリケーションで作成する場合もよくあります。スレッド プールを使用すると、これをより効率的に行うことができます。多数のクライアント要求を処理するサーバー クラスのアプリケーションでスレッド プールを使用すると、Windows プラットフォームの優先サーバーの同時実行モデルを利用できます。このモデルでは、システムのプロセッサの数に基づいてできるだけ多くの要求が同時に処理されます。これに代わるモデルとして、対象となる各クライアントに 1 つずつ専用のスレッドを割り当てるという例も見られますが、このモデルではクライアントの数の増加に対応できません。多数のスレッドによって大量のリソースが消費されるだけでなく、スレッド間のコンテキスト切り替えのコストも大きくなるため、各クライアントに提供されるサービスの全体的な品質に影響が及びます。

新しいスレッド プール コンポーネントでは、従来のスレッド プールの制限の多くが対処されています。たとえば、新しいスレッド プールでは 1 つのプロセスで複数のスレッド プールを作成できます (以前のモデルでは 1 つしか作成できません)。これにより、アプリケーションが実行するタスクを何らかの基準に基づいて分離することができます。たとえば、スレッド プールを使用してネットワーク接続の非同期 I/O の完了を処理する分散サーバー アプリケーションを作成しているとします。このネットワーク接続には、クライアント アプリケーションからの接続と、別のコンピュータで実行されているサーバー アプリケーションの他のインスタンスからの接続の 2 種類があります。さらに、各サーバーはそれぞれ多数の同時クライアント接続をサポートしなければならず、クライアント要求の方がサーバー間接続よりはるかに数が多いがサーバー間接続の方が優先度が高いものとします。

クライアントとサーバーの両方のネットワーク接続を 1 つのスレッド プールで処理する場合は、すべての完了通知が 1 つのキューに登録されることになります。そのため、サーバー接続からの I/O の完了は、それより前にキューに登録された I/O の完了が (クライアントからの I/O の完了も含めて) すべて処理されるまで処理されないことになります。その結果、システムのクライアント負荷が高くなると、サーバー接続の処理が大幅に遅れる可能性があります。クライアント接続とサーバー接続にそれぞれ専用のスレッド プールを割り当てると、各スレッド プールに固有のキューとワーカー スレッドがあるため、それぞれを分離することができます。プロセッサは、オペレーティング システムのスケジューラにより、両方のプールのスレッドに平等に配分されます。ただし、むやみに多くのスレッド プールを使用しないようにすることが重要です。1 つのプロセスであまり多くのスレッド プールを作成すると、パフォーマンスが損なわれ、スループットが低下する可能性があります。

従来のスレッド プールでは、I/O と 非 I/O の 2 種類のワーカー スレッドが使用されていましたが、これが混乱の種になることもありました。開発者がこの違いを理解していないと、パフォーマンスの低下や間違った動作を引き起こす原因になります。さらに、2 つの異なるスレッド グループがあると、異なる種類のコールバック関数の間でスレッドを共有できないため、スレッド プールの実装そのものも効率が悪くなります。新しいスレッド プールではこの区別が取り除かれており、すべてのワーカー スレッドが同じになっています。

従来の API では、スレッド プールのワーカー スレッドで非同期に呼び出される関数をキューに登録するには QueueUserWorkItem が使用されていましたが、ワーカー スレッドでコールバック関数の実行が終了したときにそれをアプリケーションで特定するための手段は用意されていませんでした。そのため、コールバック関数が DLL に含まれている場合に、その DLL をアンロードしても安全であるという保証を得ることはほとんど不可能でした。その結果、DLL をアンロードするとホスティング プロセスがクラッシュすることもありました。また、スレッド プールのキューで実行を待機しているコールバックをキャンセルする手段もありませんでした。したがって、すべての要求が処理されるまで待つしかなく、そのために大幅な遅延が生じることもありました。

最後に、従来の API にはリソースの割り当てとリソースの使用の区別がありません。これらのリソースの割り当ては失敗する可能性があるため、従来の API では、信頼性が保証されたシステムを開発するのは困難です。新しいスレッド プール API ではリソースの割り当てと使用が明確に区別されているため、いったんリソースの割り当てに成功すれば、正しく記述されたコードでそれらのリソースの使用時にエラーが発生する可能性はほとんどありません。

新しいスレッド プール API はオブジェクト ベースであり、作成、クリーンアップ、およびプロパティの変更のための一連の関数がオブジェクトの種類ごとに用意されています。図 1 は、API によって公開されるオブジェクトの種類をまとめたものです。スレッド プールによって作成されるオブジェクトや、スレッド プールによって管理されるワーカー スレッドのすべてが、それらを使用するアプリケーション プロセスの一部になるということを理解しておくことが重要です。

Figure 1 スレッド プール オブジェクトの種類 (Close Figure 1)

オブジェクトの種類 | 説明
TP_POOL | コールバックを実行するために使用されるスレッドのプール。
TP_TIMER | 期限が来るとコールバック関数を呼び出します。
TP_WAIT | カーネル オブジェクトがシグナル状態になるか待機がタイムアウトになるとコールバック関数を呼び出します。
TP_WORK | コールバック関数を非同期に呼び出します。
TP_IO | 非同期 I/O が完了するとコールバック関数を呼び出します。
TP_CLEANUP_GROUP | 1 つ以上のスレッド プール コールバック オブジェクトを追跡します。
TP_CALLBACK_ENVIRON | スレッド プールをコールバック オブジェクトにバインドします。クリーンアップ グループにバインドすることもできます。

スレッド プール オブジェクト

スレッド プールを使用できるように準備するには、まず最初に、CreateThreadpool 関数 (図 2 を参照) を使用してスレッド プールを作成します。この関数は、NULL でなければならない予約済みパラメータを受け取ります。この関数が成功すると、新たに割り当てられたスレッド プールを表す PTP_POOL が返されます。失敗した場合は NULL が返され、GetLastError を使用して拡張エラー情報を取得できます。

Figure 2 スレッド プール オブジェクトの API (Close Figure 2)
PTP_POOL WINAPI CreateThreadpool(PVOID reserved);

BOOL WINAPI SetThreadpoolThreadMinimum(PTP_POOL ptpp,
                      DWORD cthrdMin);

VOID WINAPI SetThreadpoolThreadMaximum(PTP_POOL ptpp,
                      DWORD cthrdMost);

VOID WINAPI CloseThreadpool(PTP_POOL ptpp);

スレッド プールを作成したら、2 つの新しい API を使用して、プールで管理するスレッドの最小数と最大数を制御できます。既定では、スレッド プールの最小数は 0、最大数は 500 です。これらの数は、従来のスレッド プールとの下位互換性を維持するために選択されています。というのも、従来のスレッド プールを使用するアプリケーションの中には、多数のスレッドを必要とするものがあるためです。そのように多くのスレッドを使用するアプリケーションでは、多数のコンテキスト切り替えが発生するためにパフォーマンスが問題になることも多いため、これらの既定値は今後のバージョンの Windows で変更される可能性があります。

これらの API には、いくつかの興味深い特徴があります。まず、SetThreadpoolThreadMaximum では何も返されないのに対し、SetThreadpoolThreadMinimum ではブール値が返されます。これは、スレッドの最小数を設定した結果、その新しい最小数に合わせてプールのワーカー スレッドの数を増やさなければならなくなった場合に、それらのスレッドを割り当てている間にエラーが発生したら呼び出し元に報告できるようにするためです。一方、プールのスレッドの最大数を設定する際には、スレッド プールで作成できるスレッド数の上限が設定されるだけなので、リソースの割り当てが行われることはありません。スレッド プールでは、実際の作業負荷に基づいて、スレッドの数が最小値から最大値まで変化します。残念ながら、Windows Vista の SetThreadpoolThreadMinimum にはバグがあり、プールのワーカー スレッドの数を増やす必要が生じた場合に、関数が追加のスレッドをすぐに作成しないまま戻ってしまいます。このバグは Windows Vista Service Pack 1 で修正されます。また、Windows Server 2008 にはこの問題はありません。ただし (ワーカー スレッド作成エラーの問題がなければ)、現在の Windows Vista では、いったん十分な数の作業がキューに登録されて、指定した最小数のスレッドがスレッド プールに作成されると、スレッド プールはその最小数を遵守して、プールのスレッド数をその数以上に維持します。

スレッドの最小数と最大数を同じ値に設定すると、プールを閉じるまで終了しない永続スレッドのプールが作成されます。これは、永続スレッドから呼び出す必要がある関数 (RegNotifyChangeKeyValue など) でスレッド プールを使用する場合に便利です。RegNotifyChangeKeyValue の詳細については、msdn2.microsoft.com/ms724892 を参照してください。

現在の最大値より大きい最小値で SetThreadpoolThreadMinimum を呼び出すと、新しい最小値が設定されるだけでなく、最大値もその新しい最小値に設定されます。最小値より小さい最大値で SetThreadpoolThreadMaximum を呼び出すと、最小値が新しい最大値に設定されます。これらの関数では、スレッド数のパラメータが DWORD として指定されますが、DWORD の全範囲を使用できるわけではありません。この値は内部で LONG として扱われ、0 以上かどうかが検証されます。

ここで、スレッド プール API のエラー報告の基本方針を説明しておきます。失敗することが予期されている操作の場合は、呼び出された関数からのリターン コードによって結果が報告されます。一方、例外的なエラーは、構造化例外を使用して報告されます。たとえば、SetThreadpoolThreadMinimum 関数と SetThreadpoolThreadMaximum 関数の場合は、ワーカー スレッドの数として無効な値を渡したり、スレッド プールへのポインタとして NULL を渡したりすると、構造化例外が発生します。一方、SetThreadpoolThreadMinimum の呼び出しで新しいワーカー スレッドの作成に失敗した場合は、呼び出し元に FALSE として報告されます。これは、リソース割り当ての失敗は予期されているエラーだからです。処理されない例外によってアプリケーションが終了してしまわないように、スレッド プール API の呼び出しを構造化例外ハンドラでラップすることも考えられますが、それはお勧めできません。それらの例外をキャッチしてプログラムの実行を続行しても、アプリケーション自体の問題を隠すことにしかならず、アプリケーションが最終的にエラーになったときに問題の原因を診断するのが難しくなるだけです。

スレッド プールの同時実行モデルがどのように実装されているのかを理解しておくと、これらの API を正しく使用するうえで役に立ちます。このモデルは、システムの利用可能なプロセッサの数に基づいています。たとえば、それぞれが 2 つのコアを持つ 2 つの物理プロセッサがシステムにある場合、ほとんど常に実行可能なスレッドの最適な数は 4 つになります。スレッドの数をそれより多くすると、コンテキスト切り替えによるオーバーヘッドが発生する可能性があります。一方、アプリケーションで未処理のコールバックがほとんど常に 1 つしかない場合は、スレッド プールのスレッドの数を 1 つだけにしても問題ありません。2 つのコアを持つ 2 つのプロセッサの例で、アプリケーションに未処理のコールバックが多数ある場合は、各コアがそれぞれ 1 つのアイテムを処理できるように、少なくとも 4 つのワーカー スレッドが必要になります。最大限の同時実行を維持するためには、スレッド プールのワーカー スレッドの数をそれより多くする必要がある場合もあります。その理由を理解するために、4 つのコアのそれぞれが既にコールバック関数の実行でビジー状態になっていて、スレッド プールのキューに保留中のコールバックもある場合に、コールバックを実行しているワーカー スレッドのいずれかがブロックされたらどうなるかを考えてみましょう。可能性は 3 つあります。

  • 利用可能なスレッドがスレッド プールにある場合は、別のスレッドがディスパッチされて、そのスレッドがキューの次のアイテムを削除し、保留中のコールバック関数を呼び出します。
  • 利用可能なワーカー スレッドがスレッド プールになく、既に作成されたワーカー スレッドの数が最大スレッド数に達していない場合は、わずかな遅れの後に新しいワーカー スレッドが作成され、保留中のコールバック関数を実行するためにディスパッチされます。
  • 利用可能なワーカー スレッドがスレッド プールになく、既に作成されたワーカー スレッドの数が最大スレッド数に達している場合は、追加のスレッドは作成されません。保留中のコールバック アイテムは、以前にディスパッチされたワーカー スレッドがコールバック関数の実行を終えて、実行するアイテムがまだあるかどうかを確認するためにスレッド プールに戻るまで、キューにとどまります。

このような理由から、コールバック関数が実行中にブロックされる可能性がある場合は、ハードウェアでサポートされる最大限の同時実行を実現するために、スレッド プールの最大サイズをシステムの利用可能なプロセッサ数より大きくする必要があります。ただし、コールバック関数がブロックされなければそれに越したことはありません。コールバック関数がブロックされると、同時実行が減少するだけでなく、ワーカー スレッドの再利用のレベルも低下します。このため、コールバック関数がブロックされないようにしたり、ブロックされる時間を最小限に抑えたりする努力は、十分にする価値があります。コールバックがどこでブロックされているかがわかれば、その時間を短縮できます。たとえば、コールバックが同期 I/O を実行している場合は、スレッド プールで完了する非同期 I/O に変更することを検討します。また、コールバック関数の間で行われる同期も、ロックの競合によってブロックが発生する可能性があるため、最小限に抑えるようにします。

ほとんどのコールバック関数がブロックされない場合は、スレッド プールに含まれるスレッドの数を、利用可能なプロセッサ数より多い範囲で減らすことができます。ほとんどのコールバック関数が最終的にブロックされる場合は、利用可能なプロセッサ数よりはるかに多くのワーカー スレッドが含まれるようにスレッド プールのサイズを設定する必要があります。この場合は、ほとんどのスレッドが最終的に待機状態になります。コールバック関数が完了すると、スレッド プールは、全体的なスループットを最大化するために、ワーカー スレッドを引っ込めて、実行可能なスレッドの数をプロセッサ構成に対して最適な数にまで減らします。要するに、スレッド プールのサイズを設定する際には、実行するタスクと十分なプロセッサ帯域幅がある場合にプールで追加のスレッドを作成して処理を行えるように設定する必要があります。ただし、アプリケーションで実際に使用されるスレッドの数がむやみに多かったり (既定の設定の 500 など)、コンテキスト切り替えのコストが大きすぎたりする場合は、アプリケーションが適切に設計されていないため、パフォーマンス モニタやコード プロファイラを使用してコードを調べ、改善できる箇所を見つける必要があります。

スレッド プールは、使い終わったら CloseThreadpool 関数を使用して閉じる必要があります。バインドされている未処理のコールバック オブジェクトがない場合はすぐに閉じます。ある場合は、それらの未処理のオブジェクトが解放されるときに非同期に解放されます。

コールバック環境オブジェクト

スレッド プールの作成方法がわかったので、次に、コールバック環境オブジェクトの概要と使用方法について説明します。コールバック環境オブジェクトは、アプリケーションがスレッド プールで実際に処理を行うために作成するスレッド プール コールバック オブジェクトのインスタンスにスレッド プールのインスタンスをバインドするために使用されます。また、コールバック環境オブジェクトにクリーンアップ グループ オブジェクトを関連付けることもできます。クリーンアップ グループ オブジェクトは、スレッド プール コールバック オブジェクトのクリーンアップを単純化します。クリーンアップ グループについては後ほど詳しく説明します。コールバック環境の API を図 3 に示します。

Figure 3 コールバック環境の API (Close Figure 3)
VOID InitializeThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);
VOID DestroyThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);
VOID SetThreadpoolCallbackPool(PTP_CALLBACK_ENVIRON pcbe,
                    PTP_POOL ptpp);
VOID SetThreadpoolCallbackLibrary(PTP_CALLBACK_ENVIRON pcbe,
                    PVOID mod);
VOID SetThreadpoolCallbackRunsLong(PTP_CALLBACK_ENVIRON pcbe);

VOID SetThreadpoolCallbackCleanupGroup(PTP_CALLBACK_ENVIRON pcbe,
                    PTP_CLEANUP_GROUP ptpcg,
                    PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng);

コールバック環境を作成するには、まず最初に、TP_CALL_BACK_ENVIRON 構造体を静的ストレージまたはスタックで宣言するか、ヒープから割り当てます。次に、InitializeThreadpoolEnvironment 関数を使用してそれを初期化します。この関数は、TP_CALLBACK_ENVIRON へのポインタを受け取ります。最後に、SetThreadpoolCallbackPool を使用してスレッド プールをコールバック環境に関連付けます。スレッド プールをコールバック環境に関連付けない場合や、SetThreadpoolCallbackPool の呼び出しで NULL を指定した場合は、プロセスの既定のスレッド プールが使用されます。後ほど説明するように、コールバック環境は、さまざまなスレッド プール コールバック オブジェクトのインスタンスを作成するために使用されます。アプリケーションでコールバック環境のプロパティの変更が完了し、必要なコールバック オブジェクトのインスタンスをすべて作成できたら、DestroyThreadpoolEnvironment 関数を使用してコールバック環境を破棄する必要があります。

SetThreadpoolCallbackRunsLong API は、環境に関連付けられているコールバック関数がすぐには戻らない可能性があることをスレッド プールに伝えるために使用されます。後ほど、これが API 関数 CallbackMayRunLong の呼び出しから返される結果に与える影響について説明します。

先に述べたように、従来のスレッド プールでは、スレッド プールで実行されるコールバック関数を含む DLL をアンロードしても安全なときを特定する必要があるアプリケーションはサポートされません。新しいスレッド プール API には、DLL 内のコールバック関数が実行されている間はライブラリがアンロードされないことを保証するための SetThreadpoolCallbackLibrary 関数が用意されています。ここで行われる処理を大まかに説明すると、まず、コールバック関数が呼び出される前にオペレーティング システムのローダー ロック (DLL がロードされたりアンロードされたりする過程で常に保持されるロック) が取得され、DLL の参照カウントが増やされた後、ローダー ロックが解放されます。その後、コールバックの実行が完了すると、再びローダー ロックが取得されて、参照カウントが減らされます。これにより、コールバック関数の実行中に DLL がアンロードされる可能性がなくなります。ここで、コールバックがスレッド プールのキューで保留されている間は DLL の参照カウントが変わらないことに注意してください。つまり、コールバックが保留中の場合は DLL をアンロードできることになります。ただし、そのためには、作成する DllMain 関数で DLL_PROCESS_DETACH イベントを処理し、保留中のすべてのコールバックをキャンセルする必要があります。これについてはこの後すぐに説明します。

作業オブジェクト

作業オブジェクトは、スレッド プールのワーカー スレッドでコールバック関数が非同期に呼び出されるようにするために使用されます。作業オブジェクトを作成するには CreateThreadpoolWork 関数を使用します。この関数と作業オブジェクト関連のその他の API を図 4 に示します。

Figure 4 作業オブジェクトの API (Close Figure 4)
PTP_WORK WINAPI CreateThreadpoolWork(PTP_WORK_CALLBACK pfnwk,
                    PVOID Context,
                    PTP_CALLBACK_ENVIRON pcbe);
VOID WINAPI SubmitThreadpoolWork(PTP_WORK pwk);
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance,
                    PVOID Context,
                    PTP_WORK Work);
VOID WINAPI WaitForThreadpoolWorkCallbacks(PTP_WORK pwk,
                    BOOL fCancelPendingCallbacks);
VOID WINAPI CloseThreadpoolWork(PTP_WORK pwk);

BOOL WINAPI TrySubmitThreadpoolCallback(PTP_SIMPLE_CALLBACK pfns,
                    PVOID pv,
                    PTP_CALLBACK_ENVIRON pcbe);
VOID CALLBACK SimpleCallback(PTP_CALLBACK_INSTANCE Instance,
                    PVOID Context);

最初のパラメータである pfnwk は、ワーカー スレッドによって実行されるコールバック関数へのポインタです。2 番目のパラメータである Context は PVOID 型で、コールバック関数が必要とするアプリケーション固有のデータを渡すために使用できます。最後のパラメータは TP_CALLBACK_ENVIRON へのポインタで、作業オブジェクトをスレッド プールとクリーンアップ グループ (コールバック環境にクリーンアップ グループが関連付けられている場合) にバインドします。

作業オブジェクトを作成したら、SubmitThreadpoolWork 関数を使用してスレッド プールのキューに登録できます。最終的にはワーカー スレッドがその作業オブジェクトをキューから削除して、関連付けられているコールバック関数を呼び出します。SubmitThreadpoolWork を呼び出すたびに、作業アイテムのコールバック関数の呼び出しが 1 つ生成されます。

作業オブジェクトの WorkCallback 関数は 3 つのパラメータを受け取ります。最初のパラメータである Instance は、コールバック関数の実行の特定のインスタンスを識別するために使用されます。Instance が有効なのは、そのコールバック関数が実行されている間だけです。Context パラメータは、CreateThreadpoolWork の 2 番目のパラメータとして渡されたポインタです。最後のパラメータである Work は、コールバック関数が呼び出される作業オブジェクトのインスタンスです。

コールバック関数がスレッド プール内のどのワーカー スレッドによって呼び出されるかはわからないということを理解しておくことが重要です。大まかに言えば、コールバック関数では、実行されるワーカー スレッドについて前提を立てるべきではありません。また、ワーカー スレッドは、コールバック関数が呼び出される前と同じ状態にしておく必要があります。たとえば、コールバック関数で COM を使用する場合は、そのコールバック関数が呼び出されるたびに CoInitializeEx を呼び出す必要があります。また、ワーカー スレッドは異なるタスクのコールバック関数をディスパッチするために再利用される場合もあれば、スレッド プールに戻った時点で終了される場合もあるため、コールバック関数が戻る前に CoUninitialize を呼び出す必要もあります。CoUninitialize を呼び出すことにより、リソース リークが発生したり、状態情報が残ったままになったりするのを防ぐことができます。次のコールバック関数はスレッドの実行環境に対して別の要件を持っている可能性があるため、状態情報が残っていると、その実行に悪影響を与える可能性があります。アンマネージ コードの実行時検証ツールである Application Verifier は、通常のアプリケーションのテストでは特定が難しい見過ごしがちなプログラミング エラーの検出に役立ちます。このツールが強化されて、スレッド プール関連のプログラミング エラーを検出できるようになりました。たとえば、CoInitializeEx と CoUninitialize の呼び出しの不均衡、ワーカー スレッドをスレッド プールに戻す前に元に戻されなかったスレッドの優先順位や関係の変更、元に戻されていない偽装、クリティカル セクションの孤立などのエラーを検出できます。詳細については、Application Verifier と共にインストールされるドキュメントを参照してください。このドキュメントは、microsoft.com/downloads/details.aspx?familyid=bd02c19c-1250-433c-8c1b-2619bd93b3a2 からダウンロードできます。

WaitForThreadpoolWorkCallbacks 関数は、作業オブジェクトの未処理のコールバック関数の実行がすべて終了するまで呼び出し元のスレッドをブロックします。2 番目のパラメータは、保留中のコールバック (スレッド プールのキューに登録されたが、まだ実行のためにワーカー スレッドにディスパッチされていないコールバック) の実行を許可するかキャンセルするかを制御します。この API は、WorkCallback 関数の内部で使用するとデッドロックを引き起こす可能性があるため、使用する際には注意してください。

作業オブジェクト関連の最後の API は、CloseThreadpoolWork 関数です。未処理のコールバックがない場合は作業オブジェクトが直ちに解放されます。それ以外の場合は、未処理のコールバックが完了してから非同期に解放されます。その場合、スレッド プールのキューで実行を待機している保留中のコールバックはキャンセルされます。

図 4 の残りの関数である TrySubmitThreadpoolCallback は、作業オブジェクトを作成してスレッド プールに登録し、pfns パラメータで指定したコールバックの実行が完了したら閉じられるようにする、という処理と同等の処理を提供します。コールバック関数のシグネチャは、図 4 の SimpleCallback のシグネチャと一致している必要があります。これは、作業オブジェクトに関連付けられるコールバックとは多少異なります。作業オブジェクトの割り当てや解放はスレッド プールによって内部で管理されるため、コールバック関数に渡されるのは、コールバック関数のインスタンス ポインタと、TrySubmitThreadpoolCallback の Context パラメータで指定されたアプリケーション定義のコンテキストだけです。TrySubmitThreadpoolCallback は処理のためにリソースを割り当てるため、失敗する可能性があります。この関数がブール値のリターン コードを返すのはそのためです。

待機オブジェクト

待機オブジェクトは、カーネル オブジェクトがシグナル状態になるか、指定した待機期間がタイムアウトになったときに、コールバック関数を呼び出すために使用されます。待機オブジェクトを作成するには CreateThreadpoolWait 関数を使用します。この関数のパラメータは CreateThreadpoolWork と同じパターンに従っていますが、最初のパラメータで渡されるコールバック関数へのポインタは WaitCallback 関数のシグネチャ (図 5 を参照) と一致している必要があります。

Figure 5 待機オブジェクトの API (Close Figure 5)
PTP_WAIT WINAPI CreateThreadpoolWait(PTP_WAIT_CALLBACK pfnwa,
             PVOID pv,
             PTP_CALLBACK_ENVIRON pcbe);
VOID WINAPI SetThreadpoolWait(PTP_WAIT pwa,
             HANDLE h,
             PFILETIME pftTimeout);
VOID CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE Instance,
             PVOID Context,
             PTP_WAIT Wait,
             TP_WAIT_RESULT WaitResult);
VOID WINAPI WaitForThreadpoolWaitCallbacks(PTP_WAIT pwa,
             BOOL fCancelPendingCallbacks);
VOID WINAPI CloseThreadpoolWait(PTP_WAIT pwa);

待機オブジェクトを作成したら、次に、カーネル オブジェクトがシグナル状態になるのをスレッド プールが待機するようにします。そのために使用するのが SetThreadpoolWait 関数です。最初のパラメータである pwa は、設定する待機オブジェクトのインスタンスへのポインタです。2 番目のパラメータは、待機するカーネル オブジェクトへのハンドルです。最後のパラメータである pftTimeout は、カーネル オブジェクトがシグナル状態になるのをスレッド プールが待機する時間を示す FILETIME 構造体へのポインタです。待機する時間は、絶対時間で指定することも相対時間で指定することもできます (単位は 100 ナノ秒)。正の値を渡すと、1/1/1600 からの絶対時間になります。負の値を渡すと、関数が呼び出された時刻を基準とする相対時間になります。0 を渡すと待機がすぐにタイムアウトになり、NULL を渡すとタイムアウトにならずに無限に待機し続けます。

カーネル オブジェクトがシグナル状態になるか、待機がタイムアウトになると、スレッド プールが待機オブジェクトの WaitCallback 関数を呼び出します。最初の 2 つのパラメータは、既に説明した WorkCallback 関数とまったく同じです。Wait パラメータは、コールバックが呼び出される待機オブジェクトを示します。WaitResult は呼び出しの理由を示すために使用され、WAIT_ABANDONED_0、WAIT_OBJECT_0、または WAIT_TIMEOUT のいずれかになります。WaitResult が WAIT_OBJECT_0 に設定されている場合は、カーネル オブジェクトがシグナル状態になり、待機が満たされたことを表します。

WaitResult が WAIT_TIMEOUT に設定されている場合は、待機が満たされず、SetThreadpoolWait の呼び出しで指定されたタイムアウト間隔が経過したことを表します。結果が WAIT_ABANDONED_0 の場合は、指定したオブジェクトがミューテックスであり、所有されたスレッドによって終了前に解放されなかったことを表します。ただし、WaitCallback 関数を呼び出すワーカー スレッドは、ミューテックスの待機を実行したスレッドではないため、待機オブジェクトでミューテックスを使用するのは避けてください。スレッド プールでは、実際にカーネル オブジェクトの待機を実行するために別の種類のスレッド (待機スレッドと呼ばれます) が使用されます。ミューテックスを実際に所有するのはこの待機スレッドです。待機スレッドはアプリケーションに公開されないため、ミューテックスが待機スレッドに所有されると解放できなくなります。

待機オブジェクトに対して WaitCallback 関数が呼び出されたら、再び SetThreadpoolWait を呼び出す必要があります。これにより、待機オブジェクトが再利用されて、再び、カーネル オブジェクトがシグナル状態になるのをスレッド プールが待機するようになります。待機オブジェクトを再利用するために再度 SetThreadpoolWait を呼び出すときには、待機するカーネル オブジェクトのハンドルを再度指定することもできます。別のカーネル オブジェクトを待機する場合は、最初の SetThreadpoolWait の呼び出しで指定したハンドルを使用する必要はありません。残りの 2 つの待機オブジェクト API (WaitForThreadpoolWaitCallbacks と CloseThreadpoolWait) の動作は、作業オブジェクトの対応する API とまったく同じです。

タイマ オブジェクト

タイマ オブジェクトは、その期限に達したときにコールバック関数を呼び出すために使用されます。タイマ オブジェクトを作成するには CreateThreadpoolTimer 関数を使用します。この関数とタイマ オブジェクト関連のその他の API を図 6 に示します。この関数のパラメータもまた、CreateThreadpoolWork 関数 (図 4) と同じパターンに従っていますが、最初のパラメータで渡されるコールバック関数へのポインタは TimerCallback 関数のシグネチャ (図 6 を参照) と一致している必要があります。

Figure 6 タイマ オブジェクトの API (Close Figure 6)
PTP_TIMER WINAPI CreateThreadpoolTimer(PTP_TIMER_CALLBACK pfnti,
              PVOID pv,
              PTP_CALLBACK_ENVIRON pcbe);
VOID WINAPI SetThreadpoolTimer(PTP_TIMER pti,
              PFILETIME pftDueTime,
              DWORD msPeriod,
              DWORD msWindowLength);
VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance,
              PVOID Context,
              PTP_TIMER Timer);
VOID WINAPI WaitForThreadpoolTimerCallbacks(PTP_TIMER pti,
              BOOL fCancelPendingCallbacks);
BOOL WINAPI IsThreadpoolTimerSet(PTP_TIMER pti);
VOID WINAPI CloseThreadpoolTimer(PTP_TIMER pti);

タイマ オブジェクトを作成したら、次に、SetThreadpoolTimer 関数を使用してそれを設定します。pftDueTime パラメータは、タイマの最初の期限となる時間を設定するために使用します。この時間は、先ほど説明した SetThreadpoolWait 関数のタイムアウトと同じ方法で表されます。msPeriod パラメータは、名前からもわかるように、周期的に開始されるタイマ (単位はミリ秒) を設定します。したがって、pftDueTime によって表される最初の時間間隔が経過して、コールバックがスレッド プールのキューに登録された後は、msPeriod で定義した時間間隔が経過するたびに、また別のコールバックがキューに登録されます。

msWindowLength パラメータは、スレッド プールでタイマの期限を延期できる時間枠 (単位はミリ秒) を指定します。多数のタイマを使用していて、期限が厳密でなくてもかまわない場合は、このパラメータを使用することで効率を高めることができます。この時間枠は、そこに含まれるすべてのタイマの有効期限をシステムでまとめて処理できるようにするための一種の誤差であり、スレッドのアクティブ化、タイマの期限の到達、スリープ状態、スレッドのアクティブ化、別のタイマの期限の到達、スリープ状態、という繰り返しより効率的です。この延期が行われるのは、その時間枠に厳密な期限を持つタイマがない場合だけです。msWindowLength の使用方法の理解を深めるために、多数の受信クライアント接続があるサーバー アプリケーションで、リソースの使用量を減らすために、アクティブでない状態が 5 分間続いた接続を閉じる場合について考えてみましょう。このケースでは、無通信タイマに対して 0 以外の長さの時間枠を指定しても (タイマの期限を少し過ぎても接続が維持される可能性があります) 問題にはなりません。

SetThreadpoolTimer は、以前に設定したタイマの新しい期限、周期、および時間枠の長さを設定するために使用することもできます。pftDueTime パラメータが NULL の場合は、TimerCallback 関数へのコールバックのキューへの登録が中止されますが、既にキューに登録されているコールバックは実行されます。これにより、タイマ オブジェクトを閉じずにタイマをキャンセルできるため、タイマ オブジェクトを再利用できます。

タイマ オブジェクトの期限が来ると、コールバックを呼び出すための要求がスレッド プールのキューに登録されます。ワーカー スレッドは、その要求をキューから取り出して、SetThreadpoolTimer の呼び出しで渡された TimerCallback 関数を呼び出します。このコールバック関数のパラメータは、3 番目のパラメータが PTP_WORK ではなく PTP_TIMER である以外は、作業オブジェクトのコールバック関数について説明したものと同じです。

図 5 の残りのタイマ API 関数のうち、IsThreadpoolTimerSet 関数は、名前からもわかるように、タイマが設定されている場合は TRUE を返し、それ以外の場合は FALSE を返します。残りの 2 つの関数 (WaitForThreadpoolTimerCallbacks と CloseThreadpoolTimer) の動作は、作業オブジェクトの対応する関数とまったく同じです。

I/O 完了オブジェクト

スレッド プールによってサポートされるコールバック オブジェクトの最後の種類は、I/O 完了オブジェクトです。I/O 完了オブジェクトに関連するすべての API を図 7 に示します。I/O 完了オブジェクトを使用すると、ファイル ハンドルをスレッド プールにバインドして、非同期 I/O 完了通知がスレッド プールのキューに登録されてワーカー スレッドによって処理されるようにすることができます。CreateThreadpoolIo 関数のパラメータは、HANDLE パラメータが追加されている以外は他の create 関数とほぼ同じパターンに従っています。HANDLE は、重複 I/O 完了操作に対して開いている必要があります。

Figure 7 I/O 完了オブジェクトの API (Close Figure 7)
PTP_IO WINAPI CreateThreadpoolIo(HANDLE fl,
                PTP_WIN32_IO_CALLBACK pfnio,
                PVOID pv,
                PTP_CALLBACK_ENVIRON pcbe);
VOID WINAPI StartThreadpoolIo(PTP_IO pio);
VOID CALLBACK IoCompletionCallback(PTP_CALLBACK_INSTANCE Instance,
                PVOID Context,
                PVOID Overlapped,
                ULONG IoResult,
                ULONG_PTR NumberOfBytesTransferred,
                PTP_IO Io);
VOID WINAPI WaitForThreadpoolIoCallbacks(PTP_IO pio,
                BOOL fCancelPendingCallbacks);
VOID WINAPI CancelThreadpoolIo(PTP_IO pio);
VOID WINAPI WaitForThreadpoolIoCallbacks(PTP_IO pio,
                BOOL fCancelPendingCallbacks);
VOID WINAPI CloseThreadpoolIo(PTP_IO pio);

図 7 に示されているように、CreateThreadpoolIo の最初のパラメータは、完了通知を受け取るための HANDLE です。2 番目、3 番目、および 4 番目のパラメータは、それぞれ、呼び出されるコールバック関数へのポインタ、オプションのアプリケーション固有のコンテキスト、およびコールバック環境へのポインタを表します。CreateThreadpoolIo は、成功した場合には非 NULL ポインタを、それ以外の場合は NULL ポインタを返します。

I/O 完了通知がスレッド プールによって処理されるようにするには、ハンドルに対して非同期 I/O 要求操作を発行する前に StartThreadpoolIo 関数を呼び出す必要があります。これを怠ると深刻な結果を招きます。というのも、I/O の完了が発生してもスレッド プールによって無視されるため、メモリの破損を引き起こすからです。非同期 I/O 操作を開始するための呼び出しが ERROR_IO_PENDING 以外のエラーを返した場合は、関連する関数である CancelThreadpoolIo を呼び出す必要があります。CancelThreadpoolIo の呼び出しを省略してしまうと、スレッド プールでメモリ リークが発生します。

I/O の完了が発生すると、I/O 完了オブジェクトに関連付けられている IoCompletionCallback がワーカー スレッドによって呼び出されます。この関数のシグネチャは、他のすべてのコールバックと同じ規則に従っています。最初の 2 つのパラメータは、コールバック インスタンスへのポインタと、create 関数に渡されたコンテキスト ポインタです。3 番目のパラメータである Overlapped は、非同期 I/O 操作の開始時に渡された OVERLAPPED 構造体へのポインタです。4 番目の IoResult には、操作の結果が含まれています。操作が正しく完了した場合には NO_ERROR が含まれ、それ以外の場合にはシステム エラー コード (msdn2.microsoft.com/ms681381 を参照) が含まれます。5 番目のパラメータである NumberOfBytesTransferred には、I/O 操作の間に転送されたバイト数が含まれます。6 番目のパラメータは、I/O 完了オブジェクト自体へのポインタです。

図 7 の残りの関数 (WaitForThreadpoolIoCallbacks と CloseThreadpoolIo) の動作は、これまでと同様、作業オブジェクトの対応する関数と同じです。

クリーンアップ グループを使用してクリーンアップを単純化する

次に、アプリケーションで作成したスレッド プール コールバック オブジェクトのクリーンアップ プロセスがクリーンアップ グループによって単純化されるしくみを見てみましょう。スレッド プール コールバック環境にクリーンアップ グループを関連付けると、そのコールバック環境を使用して作成されたすべてのスレッド プール コールバック オブジェクトがクリーンアップ グループによって追跡されます。アプリケーションでそれらのコールバック オブジェクトを使い終わったら、関数を 1 回呼び出すだけで、各コールバック オブジェクトを閉じることができます。各オブジェクトに対して個別に呼び出しを行う必要はありません。実際、コールバック オブジェクトがクリーンアップ グループに関連付けられている場合は、オブジェクトの close API を呼び出すべきではありません。

クリーンアップ グループを使用するには、まず最初に、図 8 の CreateThreadpoolCleanupGroup 関数を使用してクリーンアップ グループ オブジェクトを作成します。

Figure 8 クリーンアップ グループの API (Close Figure 8)
PTP_CLEANUP_GROUP WINAPI CreateThreadpoolCleanupGroup(void);
VOID WINAPI CloseThreadpoolCleanupGroupMembers(PTP_CLEANUP_GROUP ptpcg,
                       BOOL fCancelPendingCallbacks,
                       PVOID pvCleanupContext);
VOID CALLBACK CleanupGroupCancelCallback(PVOID ObjectContext,
                       PVOID CleanupContext);
VOID WINAPI CloseThreadpoolCleanupGroup(PTP_CLEANUP_GROUP ptpcg);

次に、そのクリーンアップ グループを、使用するコールバック環境に関連付けます。そのためには、図 3 の SetThreadpoolCallbackCleanupGroup 関数を使用します。

この関数の最初のパラメータは、コールバック環境へのポインタです。2 番目のパラメータは PTP_CLEANUP_GROUP です。3 番目のパラメータは、CloseThreadpoolCleanupGroupMembers 関数が呼び出されたときに呼び出されるコールバック関数へのポインタです。

アプリケーションでは、クリーンアップ グループによって追跡されているスレッド プール コールバック オブジェクトを使い終わったら、CloseThreadpoolGroupMembers を呼び出すだけで済みます。この関数は、現在実行中のコールバック関数がすべて終了するまでブロックされます。fCancelPendingCallbacks が TRUE の場合は、スレッド プールのキューに登録されたがまだ実行が開始されていないコールバックはキャンセルされます。fCancelPendingCallbacks が FALSE の場合は、保留中のコールバック関数がすべてワーカー スレッドにディスパッチされて実行が完了するまで CloseThreadpoolCleanupGroupMembers は戻りません。pvCleanupContext パラメータは、SetThreadpoolCallbackCleanupGroup の呼び出しで指定されたオプションの cancel cleanup callback 関数にアプリケーション データを渡すために使用されます。cancel callback 関数は、クリーンアップされる各スレッド プール コールバック オブジェクトに対して 1 回ずつ呼び出されます。この関数のシグネチャは、図 3 の CleanupGroupCancelCallback と一致している必要があります。最初のパラメータである ObjectContext は、クリーンアップされるスレッド プール コールバック オブジェクトの creation 関数に渡されたオプション データです。2 番目のパラメータである CleanupContext は、呼び出し元によって CloseThreadpoolGroupMembers 関数に渡されたオプション データです。CloseThreadpoolCleanupGroupMembers を呼び出したら、CloseThreadpoolCleanupGroup 関数を使用してクリーンアップ グループを閉じて、関連付けられているリソースを解放します。CloseThreadpoolCleanupGroup は、クリーンアップ グループにメンバがある間は呼び出さないようにしてください。その間に呼び出すとリソース リークが発生する可能性があります。

コールバック インスタンスの API

図 9 は、ここで説明する最後の API セットです。これらの API はすべて、コールバック オブジェクトのコールバック関数を実行しているワーカー スレッドから使用するためのものであり、コールバック関数に PTP_CALLBACK_INSTANCE を渡す必要があります。最初の関数である CallbackMayRunLong は、コールバック関数の実行時間が長い場合にそれをスレッド プールに伝えるために使用されます。スレッド プールは、実行時間の長いコールバックを実行しているワーカー スレッドの数を追跡します。この記事の前の方では、渡されたコールバック環境に関連付けられているコールバック関数がすべて実行時間の長いコールバックであることをスレッド プールに伝えるために SetThreadpoolCallbackRunsLong 関数を使用しました。

Figure 9 コールバック インスタンスの API (Close Figure 9)

BOOL WINAPI CallbackMayRunLong(
                PTP_CALLBACK_INSTANCE Instance);
VOID WINAPI DisassociateCurrentThreadFromCallback(
                PTP_CALLBACK_INSTANCE Instance);
VOID WINAPI SetEventWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                HANDLE evt);
VOID WINAPI ReleaseSemaphoreWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                HANDLE sem,
                DWORD crel);
VOID WINAPI LeaveCriticalSectionWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                PCRITICAL_SECTION pcs);
VOID WINAPI ReleaseMutexWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                HANDLE mut);
VOID WINAPI FreeLibraryWhenCallbackReturns(
                PTP_CALLBACK_INSTANCE Instance,
                HMODULE mod);

CallbackMayRunLong の結果が TRUE の場合は、実行時間の長いコールバックを処理するために利用できるワーカー スレッドがスレッド プールにあることを示します。利用可能なワーカー スレッドの有無を検討する際にスレッド プールで考慮されるのは、関数が呼び出された時点で存在するワーカー スレッドの現在のセットだけです。リターン コードが FALSE の場合は、利用可能なすべてのワーカー スレッドが既に実行時間の長いコールバックを実行していることを示します。この場合、ワーカー スレッドが実行時間の短いコールバックを実行できるようにするには、実行時間の長い作業は延期にして、できるだけ早くコールバック関数を戻す必要があります。

図 9 の次の関数である DisassociateCurrentThreadFromCallback は、現在実行されているコールバック関数と、そのコールバックを開始したオブジェクトとの関連付けを解消します。現在のスレッドは、そのオブジェクトのためにコールバックを実行しているとは見なされなくなります。たとえば、作業オブジェクトのコールバック関数で DisassociateCurrentThreadFromCallback を呼び出すと、そのコールバック関数に渡された作業オブジェクトへのポインタを使用して WaitForThreadpoolWorkCallbacks を呼び出してもデッドロックが発生しなくなります。ただし、DisassociateCurrentThreadFromCallback では、現在実行されているコールバックのコールバック環境のクリーンアップ グループへの関連付けは維持されるため、別のスレッドが CloseThreadpoolCleanupGroupMembers を呼び出した場合は、コールバック関数を実行しているスレッドがスレッド プールに戻るまで CloseThreadpoolCleanupGroupMembers は待機することになります。これにより、DLL 内のコードを実行しているスレッドがまだある間に DLL がアンロードされる心配がなくなります。1 つ注意すべき点として、DisassociateCurrentThreadFromCallback を呼び出してオブジェクトをコールバック関数内で再利用する場合は (作業オブジェクトに対して SubmitThreadpoolWork などの関数を呼び出す場合など)、CloseThreadPoolCleanupGroupMembers の呼び出しとの同期が必要になります。これは、CloseThreadPoolCleanupGroupMembers の実行が開始された後にオブジェクトを再利用しようとすると、コールバック関数の内部で例外がスローされる可能性があるからです。

次の一連の関数は、コールバック関数の実行の完了を同期オブジェクトと連携させます。SetEventWhenCallbackReturns 関数は、現在のコールバックが完了したときにイベント オブジェクトをシグナル状態にするために使用されます。同様に、ReleaseSemaphoreWhenCallbackReturns、ReleaseMutexWhenCallbackReturns、および LeaveCriticalSectionWhenCallbackReturns の各関数はすべて、現在のコールバック関数が完了したときにさまざまな種類のロック オブジェクトを解放するために使用されます。これらの関数を使用すると、指定したロックをコールバック関数の結果に関係なく確実に解放できるため、プログラミング エラーの削減に役立ちます。今後のバージョンの Windows で、Slim Reader/Writer ロックをサポートする新しい関数が追加されることが期待されます。

FreeLibraryWhenCallbackReturns 関数を使用すると、指定したコールバック インスタンスの実行が完了したときに、指定したモジュール ハンドルに対してスレッド プールが FreeLibrary を呼び出すようにすることができます。アプリケーションの側で、コールバック関数が戻る前に、未処理のコールバックの実行がすべて完了し、スレッド プールのキューの保留中のコールバックがすべてキャンセルされるようにする必要があります。

この記事の前の方で、まだコールバック関数が DLL 内のコードを実行している間に DLL がアンロードされるのを防ぐ手段として SetThreadpoolCallbackLibrary 関数を紹介しました。しかし、この関数による保証にはかなりのコストが伴います。コールバック関数の呼び出しの前後に行われるローダー ロックの取得と解放のオーバーヘッドがあるからです。また、プロセスのローダー ロックは 1 つしかないため、激しい競合が発生する可能性もあります。つまり、スレッド プールのメカニズムを利用して DLL が使用中にアンロードされないようにする場合、アプリケーションのパフォーマンスに悪影響が生じる可能性があります。アプリケーションのシナリオによっては、ここで説明した関数と Window の同期プリミティブを組み合わせて、DLL を安全にアンロードするためのメカニズムをアプリケーションで独自に構築する方が効率的な場合もあります。

サンプル アプリケーションで実際に試してみる

これまで見てきたように、新しいスレッド プール コンポーネントは、信頼性と拡張性に優れたアプリケーションを簡単に作成できるように、さまざまな点で強化されています。特に重要なのは、1 つのプロセスで複数のスレッド プールをホストできるようになったことです。各スレッド プールはそれぞれ特性が異なるため、実行する作業の種類に基づいてプロセスを分割することができます。

この新しいスレッド プールの概要記事を読んで、実際にアプリケーションで試してみたいと思われた方もいるのではないでしょうか。そうした方のために、2 つのサンプル アプリケーションを用意しました。1 つ目の ThreadPoolDemo では、作業オブジェクト、待機オブジェクト、およびタイマ オブジェクトの動作を調べることができます。ThreadPoolDemo は、プロセスの既定のスレッド プールで実行することも、カスタム スレッド プールで実行することもできます。スレッドの最小数と最大数はコマンド ライン パラメータで指定できます。

-Work コマンド ライン オプションを指定して実行する作業オブジェクトのデモでは、作業オブジェクトが作成されます。コマンド ライン パラメータ -I を使用してカウントを指定すると、作業アイテムが、指定した回数スレッド プールに登録されます。-B オプションを使用すると、コールバック関数がブロックされる時間を定義できます。-E オプションを使用すると、コールバック関数の実行時間を指定できます。既定では、登録された作業アイテムがすべて実行されるまでプログラムが実行されます。コマンド ラインで –CC または –CW を指定すると、指定した回数作業アイテムが登録された後、すぐに CloseThreadpoolCleanupGroupMembers が呼び出されます。この 2 つのオプションの違いは、–CC ではまだ開始されていないコールバックがすべてキャンセルされるのに対し、–CW ではそれらが実行されるのを待ってから CloseThreadpoolCleanupGroupMembers の呼び出しから戻るという点にあります。

タイマ オブジェクトを試すこともできます。それぞれ固有の期限およびオプションの周期と時間枠を持つ複数のタイマ オブジェクトを構成できます。時間枠の長さが異なる複数のタイマを使用すると、システムでタイマの期限をまとめた場合の効果を確認できます。

最後の待機オブジェクトのデモでは、スレッド プールで待機するイベント指定を 1 つ以上定義できます。各イベント指定には、イベントがいつシグナル状態になるかを示す期限や、待機がいつ終了になるかを定義するタイムアウト間隔をオプションで含めることができます。これらのオプションを使用して、シグナル状態になるイベント、待機がタイムアウトになるイベント、シグナル状態にもタイムアウトにもならないイベントの組み合わせを試すことができます。

各コールバック オブジェクトのコマンド ライン パラメータの詳細については、オブジェクトの種類に応じて、「ThreadPoolDemo –Work ?」、「ThreadPoolDemo –Timer ?」、または「ThreadPoolDemo –Wait ?」のいずれかのコマンドをコマンド プロンプトで入力してください。

この記事の 2 つ目のサンプル アプリケーションは、CopyFile です。これは、基本的には、完了ポートの動作のデモである Windows SDK のファイル コピー サンプルの更新版です。CopyFile は、スレッド プールを使用してソース ファイルをターゲット ファイルにコピーし、I/O 完了オブジェクトがどのように動作するのかを示します。コマンド ラインで「CopyFile –Usage」と入力すると、パラメータの詳細がコンソール ウィンドウに表示されます。

筆者の質問に答え、洞察に満ちたフィードバックを与えてくれた Rob Earhart、Eric Li、および Sandeep Ranade に感謝します。また、記事の内容をレビューして貴重なフィードバックを提供してくれた Rob Shewan にも感謝します。

Robert Saccone は、Forefront Server Security グループの主席アーキテクトです。大規模ソフトウェアの設計、分散システム、オペレーティング システムの実装などの分野に関心を持っています。連絡先は、robsaccone@hotmail.com です。

© 2007 Microsoft Corporation and CMP Media, LLC. All rights reserved; 許可なしに一部または全体を複製することは禁止されています.


WPF のスレッド: Dispatcher を使用して応答性の高いアプリケーションを構築する

WPF のスレッド

Dispatcher を使用して応答性の高いアプリケーションを構築する

Shawn Wildermuth
この記事の内容 : :
  • WPF におけるスレッド
  • Dispatcher を使用する
  • UI 以外のスレッド処理
  • タイマを使用する
この記事は次のテクノロジを使用しています:
.NET Framework 3.0、WIndows Presentation Foundation
コンテンツ
スレッド モデル
DispatcherObject
Dispatcher を使用する
BackgroundWorker
DispatcherTimer

直感的かつ自然で見栄えもするインターフェイスを何か月もかけて作成したのに、ユーザーが応答を待たされる間、共有デスクを指でコツコツたたいてばかりいるとしたら、非常に残念なことです。実行時間の長いプロセスのせいでアプリケーションが固まってしまうのを眺めるのは、まったくの苦痛です。しかし、応答性の高いアプリケーションを作成するには、注意深い計画が必要です。つまり通常は、実行時間の長いプロセスを別のスレッドで動作させ、UI スレッドはユーザーのために空けておきます。

筆者にとって、応答性に関する最初の実体験は、Visual C++® と MFC で初めて記述したグリッドにまでさかのぼります。当時、複雑な処方に含まれるすべての薬物を表示するような、薬局用アプリケーションの作成を手伝っていました。問題は、30,000 種類の薬物があったことです。そこで、応答性が高く見えるように、最初の画面に収まるだけの薬物を UI スレッドで処理し (所要時間は約 50 ミリ秒)、次にバックグラウンド スレッドを使用して表示外の薬物の処理を完了することにしました (所用時間は約 10 秒)。このプロジェクトは成功し、ユーザーの感じ方が事実より大事な場合もあるという貴重な教訓となったのです。

Windows® Presentation Foundation (WPF) は魅力的なユーザー インターフェイスの作成に適したテクノロジですが、だからと言って、アプリケーションの応答性を考慮する必要がないわけではありません。実行時間の長いプロセスには、データベースからの大規模な結果取得、非同期 Web サービスの呼び出し、その他多数の潜在的に作業負荷が大きい操作などがありますが、その種類にかかわらず言えるのは、アプリケーションの応答性を高めることが、長い目で見て必ずユーザーの満足度向上につながるということです。しかし、WPF アプリケーションで非同期プログラミング モデルを使い始める前に、WPF のスレッド モデルを理解しておくことが重要です。この記事では、このスレッド モデルを紹介するだけでなく、Dispatcher ベースのオブジェクトの動作や、BackgroundWorker の使用方法について説明し、魅力的かつ応答性の高いユーザー インターフェイスを作成できるようにします。

スレッド モデル

すべての WPF アプリケーションは、レンダリング用とユーザー インターフェイス管理用の、2 つの重要なスレッドで開始されます。レンダリング スレッドは、バックグラウンドで実行される非表示のスレッドです。このため、通常、開発者が扱うのは UI スレッドだけです。WPF では、ほとんどのオブジェクトが UI スレッドに関連付けられています。これはスレッドの関係と呼ばれ、WPF オブジェクトは、そのオブジェクトを作成したスレッド上でのみ使用できます。他のスレッドで使用すると、ランタイム例外がスローされます。WPF スレッド モデルは、Win32® ベースの API との相互運用性に優れています。このため、HWND ベースの任意の API (Windows フォーム、Visual Basic®、MFC、Win32) を WPF でホストすることも、WPF をこれらの API でホストすることもできます。

スレッドの関係は、WPF アプリケーション用の優先順位付きメッセージ ループである Dispatcher クラスによって処理されます。通常、WPF プロジェクトには単一の Dispatcher オブジェクト (したがって単一の UI スレッド) があり、あらゆるユーザー インターフェイス作業がこれを通じて行われます。

一般的なメッセージ ループとは異なり、WPF に送信される各作業項目は、優先順位が付けられて Dispatcher 経由で送信されます。このため、項目を優先順位によって順序付けすることも、システムがその処理に時間を割けるようになるまで特定の種類の作業を遅延させることも可能です (たとえば、システムまたはアプリケーションがアイドル状態になるまで、一部の作業項目を遅延させることができます)。WPF では、項目の優先順位付けをサポートすることにより、スレッド上で特定の種類の作業に他の作業よりも多くのアクセスを (したがって時間も) 与えることができます。

後で示す例では、レンダリング エンジンによるユーザー インターフェイスの更新に、入力システムよりも高い優先順位を与えます。これは、マウス、キーボード、またはインク システムをユーザーがどのように操作しても、ユーザー インターフェイスのアニメーションが更新され続けることを意味します。こうすることで、ユーザー インターフェイスの応答性が高く見える場合があります。たとえば、音楽再生アプリケーション (Windows Media® Player のようなアプリケーション) を作成するとします。ほとんどの場合、ユーザーがインターフェイスを使用してもしなくても、音楽再生に関する情報 (進行状況バーなどの情報も含む) が表示されるようにするとよいでしょう。こうすると、ユーザーの最大の関心事 (ここでは音楽を聴くこと) に対し、インターフェイスの応答性を高く見せることができます。

すべての WPF オブジェクトは、Dispatcher のメッセージ ループを使用して作業項目をユーザー インターフェイス スレッドに送り込むだけでなく、オブジェクトがどの Dispatcher に関連付けられているのかを (したがって、どの UI スレッド上に存在するのかも) 認識しています。このため、それ以外のスレッドから WPF オブジェクトを更新しようとすると失敗します。これを担当するのが DispatcherObject クラスです。

DispatcherObject

WPF におけるクラス階層では、ほとんどのクラスが一元的に DispatcherObject クラスから (他のクラスを通して) 派生しています。図 1 に示すとおり、ほとんどの WPF クラスの階層において、DispatcherObject 仮想クラスが Object の直下に挟み込まれています。

図 1 Dispatcher から Object への派生
図 1 Dispatcher から Object への派生

DispatcherObject クラスには、主な役割が 2 つあります。オブジェクトに関連付けられた現在の Dispatcher にアクセスを提供すること、そして、スレッドからオブジェクト (DispatcherObject から派生) にアクセスできることを確認 (CheckAccess) および検証 (VerifyAccess) するメソッドを提供することです。CheckAccess と VerifyAccess の違いを挙げると、現在のスレッドでオブジェクトを使用できるかどうかを示すブール値を返すのが CheckAccess、スレッドからオブジェクトにアクセスできない場合に例外をスローするのが VerifyAccess です。すべての WPF オブジェクトにこの基本機能があるため、特定のスレッド、特に UI スレッドで使用可能かどうかを判断できるのです。コントロールなどの WPF オブジェクトを独自に記述する際は、使用するすべてのメソッドで、作業の実行前に VerifyAccess を呼び出すようにします。こうすると、作成したオブジェクトが UI スレッド上でのみ使用できるようになります (図 2 を参照)。

Figure 2 VerifyAccess および CheckAccess を使用する (Close Figure 2)
public class MyWpfObject : DispatcherObject
{
    public void DoSomething()
    {
        VerifyAccess();

        // Do some work
    }

    public void DoSomethingElse()
    {
        if (CheckAccess())
        {
            // Something, only if called
            // on the right thread
        }
    }
}

したがって、Control、Window、Panel など、DispatcherObject の派生オブジェクトを呼び出すときは、UI スレッドから行うようにしてください。UI 以外のスレッドから DispatcherObject を呼び出すと、例外がスローされます。UI 以外のスレッド上で作業している場合、DispatcherObject を更新するには Dispatcher を使用する必要があります。

Dispatcher を使用する

Dispatcher クラスは、WPF のメッセージ ポンプへのゲートウェイとなり、UI スレッドでの処理に作業をルーティングするメカニズムを提供します。スレッドの関係の要求を満たすためにはこれが必要です。ただし、Dispatcher で作業がルーティングされるたびに UI スレッドがブロックされるため、Dispatcher で行う作業のサイズを小さくして時間がかからないようにすることが重要です。ユーザー インターフェイスで行う大規模な作業は、複数の小さいブロックに分割して Dispatcher で実行することをお勧めします。UI スレッド上で行う必要がない作業は別のスレッドに移動し、バックグラウンドで処理されるようにします。

通常、作業項目を UI スレッドでの処理に送信するには、Dispatcher クラスを使用します。たとえば、Thread クラスを使用して別個のスレッド上で作業を行う場合、図 3 のように、作業を指定した ThreadStart デリゲートを新しいスレッド上に作成する方法が考えられます。

Figure 3 UI 以外のスレッドで UI を更新する (間違った方法) (Close Figure 3)
// The Work to perform on another thread
ThreadStart start = delegate()
{
    // ...

    // This will throw an exception
    // (it's on the wrong thread)
    statusText.Text = "From Other Thread";
};

// Create the thread and kick it started!
new Thread(start).Start();

statusText コントロール (TextBlock) の Text プロパティの設定が UI スレッド上で呼び出されていないため、このコードは失敗します。コードで TextBlock の Text を設定しようとすると、呼び出しが UI スレッドから行われているかどうかを確認する VerifyAccess メソッドが、TextBlock クラスの内部で呼び出されます。呼び出しが別のスレッドから行われていると判断されると、例外がスローされます。では、UI スレッド上で呼び出しを行うには、Dispatcher をどのように使用したらよいのでしょう。

Dispatcher クラスを使用すると、コードを UI スレッド上で直接呼び出すことができるようになります。図 4 では、Dispatcher の Invoke メソッドを使用して、TextBlock の Text プロパティを自動的に変更するためのメソッド SetStatus を呼び出しています。

Figure 4 UI を更新する (Close Figure 4)
// The Work to perform on another thread
ThreadStart start = delegate()
{
  // ...

  // Sets the Text on a TextBlock Control.
  // This will work as its using the dispatcher
  Dispatcher.Invoke(DispatcherPriority.Normal,
                    new Action<string>(SetStatus),
                    "From Other Thread");
};
// Create the thread and kick it started!
new Thread(start).Start();

Invoke 呼び出しは 3 つの情報を受け取ります。実行する項目の優先順位、実行する作業を記述するデリゲート、および 2 番目のパラメータで記述したデリゲートに渡すパラメータです。Invoke を呼び出すと、このデリゲートが UI スレッド上で呼び出されるよう、キューに登録されます。Invoke メソッドを使用すると、作業が UI スレッド上で実行されるまで確実にブロックできます。

Dispatcher を同期で使用する代わりに、Dispatcher の BeginInvoke メソッドを使用して、作業項目を非同期で UI スレッドのキューに登録することもできます。BeginInvoke メソッドを呼び出すと、DispatcherOperation クラスのインスタンスが返されます。このクラスには、作業項目の現在の状態や実行結果 (完了した作業項目の場合) など、作業項目の実行に関する情報が格納されます。BeginInvoke メソッドおよび DispatcherOperation クラスの使用方法を、図 5 に示します。

Figure 5 UI を非同期で更新する (Close Figure 5)
// The Work to perform on another thread
ThreadStart start = delegate()
{
    // ...

    // This will work as its using the dispatcher
    DispatcherOperation op = Dispatcher.BeginInvoke(
        DispatcherPriority.Normal,
        new Action<string>(SetStatus),
        "From Other Thread (Async)");

    DispatcherOperationStatus status = op.Status;
    while (status != DispatcherOperationStatus.Completed)
    {
        status = op.Wait(TimeSpan.FromMilliseconds(1000));
        if (status == DispatcherOperationStatus.Aborted)
        {
            // Alert Someone
        }
    }
};

// Create the thread and kick it started!
new Thread(start).Start();

Dispatcher は、メッセージ ポンプの一般的な実装とは異なり、作業項目の優先順位に基づくキューです。したがって、重要度の高い作業が重要度の低い作業より先に実行されるため、応答性が向上します。優先順位付けの性質は、DispatchPriority 列挙に指定される優先順位に典型的に示されています (図 6 を参照)。

Figure 6 DispatchPriority の優先順位付けレベル (優先順位の低い順) (Close Figure 6)

優先順位 | 説明
Inactive | 作業項目はキューに登録されますが、処理されません。
SystemIdle | システムがアイドル状態のときにのみ、作業項目が UI スレッドにディスパッチされます。実際に処理される項目としては、これが最低の優先順位です。
ApplicationIdle | アプリケーション自体がアイドル状態のときにのみ、作業項目が UI スレッドにディスパッチされます。
ContextIdle | 優先順位の高い作業項目が処理された後に、作業項目が UI スレッドにディスパッチされます。
Background | レイアウト、レンダリング、および入力の項目がすべて処理された後に、作業項目がディスパッチされます。
Input | ユーザー入力と同じ優先順位で、作業項目が UI スレッドにディスパッチされます。
Loaded | レイアウトとレンダリングがすべて完了した後に、作業項目が UI スレッドにディスパッチされます。
Render | レンダリング エンジンと同じ優先順位で、作業項目が UI スレッドにディスパッチされます。
DataBind | データ バインドと同じ優先順位で、作業項目が UI スレッドにディスパッチされます。
Normal | 通常の優先順位で、作業項目が UI スレッドにディスパッチされます。ほとんどのアプリケーション作業項目は、この優先順位でディスパッチされるようにしてください。
Send | 最高の優先順位で、作業項目が UI スレッドにディスパッチされます。

一般に、UI の外観を更新する作業項目の優先順位としては、常に DispatcherPriority.Normal を使用します (前に使用した例を参照)。しかし、他の優先順位を使用すべき場合もあります。特に興味深いのは、3 種類のアイドル優先順位 (ContextIdle、ApplicationIdle、および SystemIdle) です。これらの優先順位を使用すると、作業負荷が非常に低いときにのみ作業項目が実行されるよう指定できます。

BackgroundWorker

せっかく Dispatcher の動作について理解したのに驚かれることでしょうが、これにはあまり使い道がありません。Windows Forms 2.0 において、マイクロソフトはユーザー インターフェイス開発者用の開発モデルを簡素化するため、UI 以外のスレッド処理のためのクラスを導入しました。このクラスは BackgroundWorker と呼ばれています。図 7 に、BackgroundWorker クラスの一般的な使用方法を示します。

Figure 7 WPF で BackgroundWorker を使用する (Close Figure 7)
BackgroundWorker _backgroundWorker = new BackgroundWorker();

...

// Set up the Background Worker Events
_backgroundWorker.DoWork += _backgroundWorker_DoWork;
backgroundWorker.RunWorkerCompleted +=
    _backgroundWorker_RunWorkerCompleted;

// Run the Background Worker
_backgroundWorker.RunWorkerAsync(5000);

...

// Worker Method
void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    // Do something
}

// Completed Method
void _backgroundWorker_RunWorkerCompleted(
    object sender,
    RunWorkerCompletedEventArgs e)
{
    if (e.Cancelled)
    {
        statusText.Text = "Cancelled";
    }
    else if (e.Error != null)
    {
        statusText.Text = "Exception Thrown";
    }
    else
    {
        statusText.Text = "Completed";
    }
}

BackgroundWorker コンポーネントは WPF とうまく連動します。このコンポーネントの裏では AsyncOperationManager クラスが使用され、さらに AsyncOperationManager クラスにより SynchronizationContext クラスが使用されて、同期処理が行われるためです。Windows フォームでは、SynchronizationContext クラスから派生する WindowsFormsSynchronizationContext クラスが、AsyncOperationManager によって渡されます。同様に ASP.NET では、SynchronizationContext の別の派生クラス AspNetSynchronizationContext が処理されます。SynchronizationContext から派生するこれらのクラスは、メソッド呼び出しのスレッド間同期を処理できます。

WPF では、このモデルが DispatcherSynchronizationContext クラスで拡張されています。BackgroundWorker を使用すると、Dispatcher が自動的に使用され、複数スレッドにまたがるメソッド呼び出しが行われます。この共通パターンに既に精通している多くの読者にとって、新しい WPF プロジェクトでも引き続き BackgroundWorker を使用できることは朗報です。

DispatcherTimer
WPF スレッド関連リンク
  • Windows Presentation Foundation Virtual Labs
  • .NET Framework Developer Center の WPF
  • WPF Fundamentals: スレッド モデル (MSDN ライブラリ)
  • 『Windows Presentation Foundation プログラミング』Chris Sells、Ian Griffiths 共著 (O'Reilly、2005 年)

Microsoft® .NET Framework での開発において、コードの定期実行は一般的なタスクです。しかしこれまで、.NET でのタイマの使用は、よく言っても複雑でした。.NET Framework 基本クラス ライブラリ (BCL) で Timer クラスを探すと、3 つもの Timer クラスが見つかります。System.Threading.Timer、System.Timers.Timer、そして System.Windows.Forms.Timer です。これらのタイマはそれぞれに異なります。Alex Calvo による MSDN Magazine の記事では、これらの Timer クラスの使い分けについて説明されています (msdn.microsoft.com/msdnmag/issues/04/02/TimersinNET を参照)。

WPF アプリケーションのために、Dispatcher を利用する新しい種類のタイマ (DispatcherTimer クラス) が導入されました。他のタイマと同様に DispatcherTimer クラスでも、刻み間隔のほか、タイマのイベント発生時に実行するコードを指定できます。図 8 に、DispatcherTimer のごく一般的な使用方法を示します。

Figure 8 DispatcherTimer クラスの使用例 (Close Figure 8)
// Create a Timer with a Normal Priority
_timer = new DispatcherTimer();

// Set the Interval to 2 seconds
_timer.Interval = TimeSpan.FromMilliseconds(2000);

// Set the callback to just show the time ticking away
// NOTE: We are using a control so this has to run on
// the UI thread
_timer.Tick += new EventHandler(delegate(object s, EventArgs a)
{
    statusText.Text = string.Format(
        "Timer Ticked:  {0}ms", Environment.TickCount);
});

// Start the timer
_timer.Start();

DispatcherTimer クラスは Dispatcher に関連付けられているため、DispatcherPriority および使用する Dispatcher も指定できます。DispatcherTimer クラスは、優先順位 Normal を現在の Dispatcher として既定で使用しますが、これらの値はオーバーライドできます。

_timer = new DispatcherTimer(
    DispatcherPriority.SystemIdle, form1.Dispatcher);

応答性の高いアプリケーションを作成するためには、ワーカー プロセスを計画する努力を惜しまないでください。初期段階で調査を行えば、さらに満足のいく計画を実現できます。着手する前に、「WPF スレッド関連リンク」に記載されているサイトを参照することをお勧めします。この記事と併せてこれらのサイトを読むことにより、応答性の高いアプリケーションを開発するための十分な基盤が得られます。

Shawn Wildermuth は Microsoft MVP、MCSD.NET、MCT であり、Wildermuth Consulting Services の創立者でもあります。Shawn は『実践的 ADO.NET』(Addison-Wesley、2002 年) の著者であり、4 つのマイクロソフト認定トレーニング キットおよび近々発行される『規範データ アーキテクチャ』の共著者でもあります。Shawn の Web サイトは、wildermuthconsulting.com からご覧いただけます。


並列 LINQ: マルチコア プロセッサでクエリを実行する

並列 LINQ

マルチコア プロセッサでクエリを実行する

Joe Duffy and Ed Essey
この記事は、現在開発中の Parallel FX Library に基づいています。ここに記載されているすべての情報は、変更される場合があります。
この記事の内容 : :
  • PLINQ の基本
  • PLINQ プログラミング モデル
  • 同時実行例外
  • 出力順序
この記事は次のテクノロジを使用しています:
Parallel FX Library
コンテンツ
LINQ から PLINQ へ
PLINQ プログラミング モデル
クエリの出力を処理する
同時実行例外
出力結果での順序付け
副作用
PLINQ を使用する

マルチコア プロセッサについて説明します。マルチコア プロセッサは、主にサーバーやデスクトップ PC に普及した後、現在では携帯電話や PDA でも使用されており、電力消費の点でも非常に効率が高いプロセッサです。マルチプロセッサ プラットフォームの可用性の向上に対応して、PLINQ (Parallel Language Integrated Query) は、従来のマルチプロセッサ コンピュータやマルチコア プロセッサの最新技術など、並列ハードウェアを利用するための簡単な方法を提供します。

PLINQ はクエリ実行エンジンで、LINQ-to-Objects クエリまたは LINQ-to-XML クエリを受け取り、使用できる場合は、自動的に複数のプロセッサまたはコアを実行に使用します。プログラミング モデルの変更はわずかなので、PLINQ を使用するために同時実行のエキスパートになる必要はありません。実際に、動作を理解するために詳細な説明が本当に必要になるまで、スレッドやロックには触れません。PLINQ は、Microsoft® .NET Framework の次世代の同時実行サポートである Parallel FX Library の重要なコンポーネントです。

PLINQ のようなテクノロジの使用は、将来の並列マイクロプロセッサ アーキテクチャのソフトウェアのスケーラビリティを確保するうえでますます重要になります。クエリとして表すことができるデータ集約的な操作または計算集約的な操作を行う箇所など、現在のアプリケーション全体を通じての選択箇所で LINQ を利用すると、PLINQ が使用できるようになったときに、アプリケーションを実行しているコンピュータのプロセッサ数が 2 から 4、32、さらにそれ以上と増加しても、プログラムの各部分のパフォーマンスを継続的に向上できるようになります。そのコードをシングルプロセッサ コンピュータでのみ実行する場合でも、通常、PLINQ のオーバーヘッドは小さいので違いには気付きません。さらに、PLINQ のデータ並列の性質によって、データ セットのサイズの増大に合わせてプログラムを拡大し続けることができるようになります。

この記事では、PLINQ テクノロジの目標 (PLINQ テクノロジを広範な .NET Framework やその他の同時実行サービスに組み込む)、および PLINQ テクノロジが LINQ 開発者の観点からどのように見えるかについて説明します。最後に、PLINQ が既に非常に高い価値を示しているいくつかのシナリオ例を紹介します。

PLINQ を含む Parallel FX Library は現在開発中ですが、最初の Community Tech Preview (CTP) が 2007 年の秋に MSDN® で利用できるようになります。詳細については、blogs.msdn.com/somasegar を参照してください。

LINQ から PLINQ へ

PLINQ プロジェクトについて初めて耳にしたとき、ほとんどの人は、なぜ LINQ を並列化するのかとたずねます。簡単に答えると、計算を表現するための LINQ のセットアットアタイム プログラミング モデルでは、どのように行うかではなく、何を行う必要があるのかを指定することに重点が置かれているからです。LINQ を使用しない場合、どのように行うかについては、通常、ループや中間データ構造の使用によって表現されますが、特定の情報を大量にエンコードすることにより、コンパイラとランタイムは簡単には並列化できません。一方、LINQ の宣言型の性質には、並列化を使用して同じ結果を得る PLINQ のような優れた実装のための柔軟性がありません。

そこで、次の疑問が必然的に生じます。PLINQ が意味をなすように大量のデータにクエリを実行するのであれば、なぜ単にデータベースを使用しないのでしょうか。この疑問に対する答えは、次の修辞疑問となります。LINQ を使用していなかったら、どのようなコードを記述していたでしょうか。同じ量のデータ集約的および計算集約的な作業を行うことになると考えられますが、構造化されていない一連のループや関数呼び出しなどの内部でそれを捕らえていたでしょう。したがって、この疑問は、すべてのプログラムがデータベースに存在する必要があることを意味しますが、そのようなことに同意する人はほとんどいないはずです。

PLINQ を使用すれば、データベース サーバーの処理ロジック全体をクライアントのメモリ内の LINQ-to-Objects クエリに移動する必要はありません。代わりに、PLINQ には、既存の問題に対する既存のソリューションのための並列実行を利用する付加的な方法が用意されています。問題が十分にデータ集約的で、データベース ソリューションの追求が従来どおり必要とされる場合は、そのようなソリューションを引き続き追求する必要があります。PLINQ は、そのソリューションをほとんど変更しません。ただし、まとめてクエリする複数のデータ ソースがある場合は (場合によって、異種、スパニング データベース、XML ファイルなど)、データがクライアント側に渡った後、PLINQ が引き継いで並列化します。

当然、次のように質問されます。なぜ、単に LINQ-to-Objects 自体がクエリを並列に実行しないのでしょうか。並列実行は完全に隠され、開発者はコードを 1 行も変更することなくそのメリットを得ることができるでしょう。残念ながら、LINQ から PLINQ への移行に関するわかりにくい問題がいくつかあり、少なくとも PLINQ 1.0 では、この考え方に対する理解を妨げます。これについては、後ほど詳しく説明します。

PLINQ プログラミング モデル

PLINQ を使用することは、LINQ-to-Objects と LINQ-to-XML を使用することとほとんど変わりません。OrderBy、Join、Select、Where など、C# 3.0 および Visual Basic® 9.0 のクエリの包含構文や System.Linq.Enumerable クラスを通じて利用できる演算子をすべて使用できます。PLINQ は、言語の包含で使用できる演算子だけでなく、LINQ の演算子をすべてサポートします。また、System.Xml.Linq API を使用して読み込んだ XML ドキュメント以外に、T[]、List<T>、またはその他の種類の IEnumerable<T> など、すべてのメモリ内コレクションにクエリを実行できます。既に LINQ クエリが多数ある場合、PLINQ によってそれらを実行できます。

LINQ-to-SQL クエリと LINQ-to-Entities クエリは、引き続き各データベースとクエリ プロバイダによって実行されるので、PLINQ ではこれらのクエリを並列化する方法は提供しません。多数の異種クエリの出力の結合など、メモリ内にあるこれらのクエリの結果を処理する場合は、PLINQ がとても役に立ちます。

これまでと同じ方法で LINQ クエリを記述する以外に、PLINQ を使用するには、次の 2 つの追加手順が必要です。

  1. コンパイル時に System.Concurrency.dll アセンブリを参照します。
  2. System.Linq.ParallelEnumerable.AsParallel 拡張メソッドへの呼び出しによって、データ ソースを IParallelEnumerable<T> にラップします。

手順 2 で AsParallel 拡張メソッドを呼び出すと、C# コンパイラまたは Visual Basic コンパイラが、System.Linq.Enumerable ではなく、標準クエリ演算子の System.Linq.ParallelEnumerable バージョンにバインドするようになります。これによって、PLINQ が制御を引き継ぎ、クエリを並列に実行できるようになります。AsParallel は、任意の IEnumerable<T> を受け取るように定義します。

public static class System.Linq.ParallelEnumerable {
    public static IParallelEnumerable<T> AsParallel<T>(
        this IEnumerable<T> source);
    ... the other standard query operators ...
}

IParallelEnumerable<T> は、IEnumerable<T> から派生し、追加はほとんどありません。単に PLINQ の ParallelEnumerable クエリ プロバイダへのバインドを容易にするためにあり、C# 3.0 および Visual Basic .NET 9.0 の新しい拡張メソッドのサポートを利用します。インターフェイスは IEnumerable<T> から派生するので、引き続きインスタンスの繰り返し処理を行い、IEnumerable<T> を必要とする他の API に渡すことができます。ParallelEnumerable で定義される標準クエリ演算子は、Enumerable の標準クエリ演算子と同じです。唯一の違いは、IEnumerable<T> ではなく IParallelEnumerable<T> をその拡張ソース引数として受け取り、IEnumerable<T> ではなく IParallelEnumerable<T> を返し (単純型を返すだけの集計を除く)、当然ながらクエリの評価に並列実行を内部的に使用する点です。

例として、C# で定義した簡単な LINQ クエリを次に示します。

IEnumerable<T> data = ...;
var q = data.Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
foreach (var e in q) a(e);

この場合、次のように、AsParallel への呼び出しをデータに追加するだけで、PLINQ を使用できます。

IEnumerable<T> data = ...;
var q = data.AsParallel().Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
foreach (var e in q) a(e);

これは、C# のクエリの包含構文を使用してより簡潔に記述できます。この場合、PLINQ バージョンは次のようになります。

IEnumerable<T> data = ...;
var q = from x in data.AsParallel() where p(x) orderby k(x) select f(x);
foreach (var e in q) a(e);

この変更を行うと、PLINQ では従来のデータ並列評価技法を使用して、利用できるすべてのプロセッサで Where、OrderBy、および Select を透過的に実行します。PLINQ では、LINQ と同様に遅延実行を使用します。したがって、繰り返し処理を行うか、GetEnumerator を直接呼び出すか、ToList や ToDictionary など他の API によって結果をリストに強制するまで、クエリの実行は開始されません。クエリが実行されると、PLINQ が、複数のスレッドの隠された使用によって、利用できるプロセッサで実行するクエリの部分を準備します。これがどのように実行されるかを理解する必要もなく、パフォーマンスの向上と利用できるプロセッサの活用を体験できます。

明確ではありませんが、q の推定される型は、前に説明した通常の LINQ-to-Objects クエリと PLINQ クエリとで異なります。最初の例では、q は IEnumerable<U> として型指定されています。この場合の U は、Select 演算子に渡される f メソッドが返す型です。一方、2 番目の例では、q は IParallelEnumerable<U> として型指定されています。これは、通常は問題ではありません。たとえば、明示的に型 IEnumerable<U> を持つように q を宣言している場合、IParallelEnumerable<U> は IEnumerable<U> から派生するので、AsParallel への変更は引き続き機能します。ただし、q のそれ以降の使用は、IParallelEnumerable<U> と同様に扱われることになります。たとえば、その後にクエリを実行すると、PLINQ がクエリ プロバイダとして選択されます。

LINQ の演算子の一部はバイナリで、2 つの IEnumerable<T> を入力として受け取ることに注意してください。Join はそのような演算子の例として最適です。この場合、最も左側のデータ ソースの型によって、LINQ または PLINQ のいずれを使用するかが決まります。したがって、次のように最初のデータ ソースで AsParallel を呼び出すだけで、クエリを並列に実行できます。

IEnumerable<T> leftData = ..., rightData = ...;
var q = from x in leftData.AsParallel()
        join y in rightData on x.a == y.b
        select f(x, y);

この説明では、拡張メソッドを使用してクエリを記述していると想定しています。代わりに、Enumerable クラスのメソッドを直接呼び出すようにしている場合は、PLINQ に移行するには、もう少し作業を行う必要があります。AsParallel への呼び出しに加えて、ParallelEnumerable 型も参照する必要があります。たとえば、上記のクエリが、次のように Enumerable への直接呼び出しによって記述されているとします。

IEnumerable<T> data = ...;
var q = Enumerable.Select(
            Enumerable.OrderBy(
                Enumerable.Where(data, (x) => p(x)),
            (x) => k(x)),
        (x) => f(x));
foreach (var e in q) a(e);

PLINQ を使用するには、クエリを次のように書き換える必要があります。

IEnumerable<T> data = ...;
var q = ParallelEnumerable.Select(
            ParallelEnumerable.OrderBy(
                ParallelEnumerable.Where(data.AsParallel(), (x) =>    p(x)),
            (x) => k(x)),
        (x) => f(x));
foreach (var e in q) a(e);

明確な理由により、包含および拡張メソッドを使用すると、クエリを記述するのが容易になるだけでなく、PLINQ への移行もはるかに簡単になります。

これで作業は完了です。これらの変更を行うだけで、LINQ-to-Objects の代わりに PLINQ を使用できます。LINQ-to-XML は XML ドキュメントを IEnumerable<T> データ構造として公開するため、これまでのすべての説明は、XML コンテンツに対するクエリの実行にも当てはまります。

クエリの出力を処理する

既に述べたように、遅延評価のため、クエリの出力の処理を開始するまで並列実行は導入されません。IEnumerable<T> に慣れている場合、これは GetEnumerator メソッドを呼び出すことと同じです。PLINQ クエリを処理できる基本的な方法は 3 つあり、それぞれ、並列実行の少しずつ異なるモデルになります。

最初のモデルは、パイプライン処理です。この場合、列挙を行うスレッドはクエリの実行用のスレッドから独立しています。PLINQ は、クエリを実行するためのスレッドを多数使用しますが、列挙スレッドへの干渉がないよう、スレッドごとの並列実行の程度を軽減します。たとえば、利用できるプロセッサが 8 つある場合、要素が使用可能になったときに、そのうちの 7 つのプロセッサが PLINQ クエリを実行し、その間、残りの 1 つのプロセッサは PLINQ クエリの出力で foreach ループを実行します。これにより、さらなる出力の増分処理が可能になるため、結果を保持するのに必要なメモリ要件が小さくなります。ただし、多数のプロデューサ スレッドと 1 つだけのコンシューマ スレッドを使用すると、作業配分が偏ることが多くなり、プロセッサの使用が非効率になる場合があります。

2 番目のモデルは、ストップアンドゴー処理です。このモデルでは、列挙を開始するスレッドはクエリを実行するその他のすべてのスレッドに結合されています。すべてのスレッドが出力の完全なセットの作成を終了すると、列挙スレッドが出力の列挙を開始します。このモデルの利点は、すべての処理能力ができるだけ早く出力を作成するのに向けられることです。また、実装では増分同期オーバーヘッドが少ないため、パイプライン処理より若干効率的です。PLINQ がこれらの点についてより適切に処理できるのは、出力データの行き先とそのアクセス方法を正確に認識しているからです。コンシューマとプロデューサの間で作業配分に偏りがある場合、通常、これは消費のより適切なメソッドになります。

最後のモデルは、反転列挙です。PLINQ にはラムダ関数が用意されており、この関数は、出力の要素ごとに一度に並列に実行されます。PLINQ で処理される並列実行は非常に多く、実装は複数のスレッドからの出力を結合するなどの負荷の高い操作を避けるため、これは最も効率的なメカニズムです。ただし、次の欠点があります。foreach ループを単に使用することができず、特殊な ForAll API を使用する必要があり、またラムダが共有状態に依存しないことに注意する必要があります。そうしない場合、並列実行を導入するとクエリが正しくなくなり、予期しないクラッシュやデータの破損が発生する可能性があります。この API の観点から問題を表すことができる場合は、常に推奨されるメソッドです。

これらの 3 つのモデルのうちどれを使用するかは、クエリ結果で何を行うかによります。既定は、最初のパイプライン処理です。結果のクエリ列挙子で MoveNext が呼び出されると、直ちに追加のワーカー スレッドのセットがクエリを実行し、使用できるようになったときに、この呼び出しおよびすべての後続の MoveNext 呼び出しから結果が返されます。MoveNext への呼び出しが行われた場合に、クエリのプロデューサ スレッドからの使用できる出力がないときは、要素が使用できるようになるまで、呼び出し元のスレッドがブロック (待機) します。単に foreach を使用して PLINQ クエリの出力を処理する場合は、次のようなコードにします。

var q = ... some query ...;
foreach (var e in q) {
    a(e); // this runs in parallel with the execution of 'q'
}

IParallelEnumerable<T> インターフェイスは、パイプラインというブール引数を受け取る GetEnumerator のオーバーロードを提供するので、代わりにストップアンドゴー処理を選択できます (true はパイプラインを意味し、false はストップアンドゴーを意味します)。MoveNext への最初の後続の呼び出しでは、クエリ全体が実行され、すべての出力が使用できる場合はその呼び出しから制御が返されるだけです。MoveNext への後続の呼び出しは、出力を格納しているバッファを列挙します。

var q = ... some query ...;
using (var e = q.GetEnumerator(false)) {
    while (e.MoveNext()) {
        // after the 1st call, the query is finished executing
        // we then just enumerate the results from an in-memory list
        a(e.Current);
    }
}

ストップアンドゴー処理が既定として使用される特殊なケースがいくつかあります。ToArray メソッドまたは ToList メソッドを使用している場合、これらの演算子は内部的にストップアンドゴー処理を強制します。クエリに並べ替えがある場合、並べ替えの出力のパイプライン処理は無駄が多いため、代わりにストップアンドゴーが使用されます。並べ替えでは遅延が非常に大きくなるので (1 つの出力要素を作成する前に、通常は入力全体を並べ替える必要があるため)、PLINQ では、できるだけ早く並べ替えを完了することにすべての処理能力が優先的に向けられます。

反転列挙を使用するには、次のように、別の PLINQ 固有の API を使用する必要があります。

public static class System.Linq.ParallelEnumerable {
    public static void ForAll<T>(
        this IParallelEnumerable<T> source, Action<T> action);
    ... the other standard query operators ...
}

ForAll API の使用は、上で示した foreach ループの使用によく似ています。

var q = ... some query ...;
q.ForAll(e => a(e));

同時実行例外
魅力的なシナリオ

この記事を読んでいる間に、 独自のアプリケーションで PLINQ を使用する方法を既にいくつか考え始めているのではないでしょうか。おそらく、既に LINQ を使用しており、複数のプロセッサまたは複数のコアを備えるコンピュータでのアプリケーションのスケーラビリティを改善しようと考えているでしょう。もちろん、PLINQ によって現在のプログラムの実行速度を向上できますが、より速い速度でデータ ストリームを処理しながら、より多くの計算作業を行ったり、同じ時間でより大きなデータ サイズを操作したりすることもできるようになります。このすべてを行うとき、この新しい PLINQ テクノロジによって、今までに試みることができなかったアプリケーションの新たな可能性が広がるのです。

マルチコアと PLINQ によって新たな可能性が広がるシナリオについて、いくつか説明しましょう。サウンド スタジオで作業している音楽プロデューサーが、生の楽器音に一連のエフェクトを適用して、もっと芸術的に洗練された製品品質のマスター トラックを作ろうとしているとしましょう。彼が使用するミキシング ソフトウェアを提供する会社は、PLINQ を使用してこれらのエフェクトを適用することもできます。これらのエフェクトは、通常は大きなデータ ストリーム (生の音楽) に対するフィルタと投影で構成されます。PLINQ は、作成時間を大幅に短縮し、使用できるようになったときに、より強力なハードウェアを活用できます。このアプローチでは、完全な作成後の処理を行う代わりに、ほぼリアル タイムで音楽を変換することもできます。

同様に、外国為替トレーダーが、利益を得るために裁定取引の状況 (市場の非効率) を探しているとします。このような変動はごく小さく、市場は絶えず均衡に達するので消えてしまいます。したがって、非常にすばやい取引が必要とされます。PLINQ による並列実行を使用して株式取引情報をクエリすると、大量のデータや、複雑な分析と計算に基づいて、ほぼリアル タイムでの意思決定を行えるようになります。

これらは、マルチコア ハードウェアで PLINQ によって提供される速度の向上が、ビジネスに利益をもたらすごくわずかな例です。医療、経済、地質モデリング、科学計算、交通量の制御とシミュレーション、ゲーム、人工知能、機械学習、言語分析など、その他の分野でも同様の機会が提供されます。

PLINQ の並列化プロセスは完全に透過的であるという上記の説明はほぼ事実ですが、並列実行の使用が、上で示した単純な抽象化を通じてリークする可能性のある場所がいくつかあります。これらは既に触れた問題です。さいわいなことに、これらのほとんどは重要ではありませんが、いずれにしても注意が必要です。

例外をスローするラムダまたはクエリ演算子は、順次 LINQ クエリの実行を直ちに停止します。これは、クエリの実行に使用されるプロセッサが 1 つだけの場合、要素が順番に処理されるためです。演算子が要素の 1 つで失敗すると、直ちに例外がスローされ、後続の要素は無視されます。このことは PLINQ には当てはまりません。

例として、次の (人為的な) クエリを見てみましょう。

object[] data = new object[] { "foo", null, null, null };
var q = data.Select(x => x.ToString());
foreach (var e in q) Console.WriteLine(e);

LINQ を使用してこれを実行するたびに、最初の配列要素の ToString の実行は成功し、2 番目の ToString の呼び出しの試行は NullReferenceException で失敗します。3 番目や 4 番目の要素には達しません。ただし、PLINQ の場合と同様、複数のプロセッサが関与するときは、複数の例外が並列に発生する可能性があります。PLINQ による問題の細分化の決定方法によって、1、2、3、すべて同時、またはそれらの任意の組み合わせ (場合によって 3 は含むが、1 または 2 は除く) のエラーが発生することもあります。

この問題に対処するため、PLINQ では、LINQ とは少し異なる例外モデルを使用してエラーを伝えます。PLINQ スレッドのいずれかで例外が発生すると、システムは、まずその他のすべてのスレッドの実行をできるだけ早く停止しようとします。このプロセスは、完全に透過的に行われます。ただし、他の例外が同時に発生することを回避できる場合とできない場合があり、PLINQ が関与するまでに、既に他の例外が発生していることもあります。すべてのスレッドがシャットダウンされると、発生した例外の完全なセットが新しい System.Concurrency.MultipleFailuresException オブジェクトに統合され、その新しい統合例外オブジェクトが再度スローされます。発生した各例外には、影響を受けないスタック トレースを含め、InnerExceptions プロパティ (型 Exception[]) を通じて後でアクセスできます。

実際に、処理不能な例外によってクエリの実行が終了すると、スローされた例外が 1 つだけの場合でも、PLINQ は常に 1 つの MultipleFailuresException をスローします。前の例では、それは、PLINQ が常に NullReferenceExceptions を MultipleFailuresException にラップすることを意味します。ラップせずに特定の種類の例外をキャッチする場合は、複数の catch 句を記述する必要があります。通常では、特定の種類の例外をキャッチすることは明らかにありませんが、必要な場合は、次のように記述して多数のロジックを繰り返す必要があります。

try
{
    // query...
}
catch (NullReferenceException)
{ ... }
catch (MultipleFailuresException)
{ ... }

これは気が利かないだけでなく、開発者がどちらかの句を忘れやすいので、一部の状況や構成でのみ起こるバグの原因になります。

残念ながら、これによってデバッグがさらに難しくなる可能性があります。例外が処理不能になる場合に、デバッガをアタッチすると、例外の発生元ではなく、まず GetEnumerator への呼び出しに割り込みます (クエリ結果の foreach を呼び出している場合)。これは、現在の .NET Framework の非同期プログラミング モデル (BeginXX/EndXX) で起こることに類似しています。さいわいにも、PLINQ は元のスタック トレースを保持するので、MultipleFailuresException オブジェクトを拡張し、その InnerExceptions プロパティを参照する場合、利用できる完全な元のスタック トレースを使用して例外の完全なセットを見つけることができます。

出力結果での順序付け

LINQ で次のコードを記述したとします。

int[] data  = new int[] { 0, 1, 2, 3 };
int[] data2 = (from x in data select x * 2).ToArray();

data2 の内容を予測できるでしょうか。この質問は単純すぎて考えるまでもないように思われます。誰もが、{ 0, 2, 4, 6 } と答えるでしょう。しかし、次のようにコードを変更するだけで、data2 の考えられる内容が異なります。

int[] data  = new int[] { 0, 1, 2, 3 };
int[] data2 = (from x in data.AsParallel() select x * 2).ToArray();

この場合、{ 0, 2, 4, 6 } は確実に考えられますが、{ 6, 0, 2, 4 } やこれらの 4 つの数字のその他のどの順列も可能性があります。

これは、foreach を使用してクエリを繰り返し処理しているか、ToArray を使用して結果を配列にマーシャリングしているかに関係なく、PLINQ がクエリを並列に実行し、利用可能になるとすぐに結果を使用できるようにするためです。LINQ の順序付けは、単に、その実装が入力を順番に処理することによる副産物です。一方、PLINQ の順序付けは、作業の並列単位の非確定的スケジュールによって決定されます。これは、ある実行からその次の実行で大幅に変わる可能性があります。

これは、PLINQ チームが行った明示的な設計上の決定でした。クエリは順序付けについて何も保証されてはいません。たとえば、SQL Server™ を見てみると、クエリ テキストで order by 句を指定していない限り、順序付けは、クエリでインデックスが使用されるかどうかや、ディスクのレコードのレイアウトなど、多くの要素によって決まります。実際には、SQL Server でもクエリの評価で並列実行を使用できるため、非確定的であることもあります。

順序付けを維持する必要がよくあるため、また LINQ から移行しようとしているユーザーにとってのいくつかの小さな問題を軽減するため、PLINQ には順序保持を利用する方法が用意されています。順序保持は、介在する並べ替え操作がないときに、出力要素の相対的な順序付けが入力要素の相対的な順序付けに厳密に関連付けられるようにします。上記のクエリの出力が常に { 0, 2, 4, 6 } になるようにするには、代わりに次のクエリを使用します。

int[] data  = new int[] { 0, 1, 2, 3 };
int[] data2 = (from x in data.AsParallel(QueryOptions.PreserveOrdering)
               select x * 2).ToArray();

順序保持には欠点もあります。実際には、それ以降のパフォーマンスやクエリのスケーリングに影響する可能性があります。これは、PLINQ が最後に並べ替え操作を論理的に挿入し、その並べ替えが、プロセッサ数の増加に完全には比例しない演算子であるためです。この意味を理解するため、前のクエリと論理的に同じクエリを次に示します。

int[] data  = new int[] { 0, 1, 2, 3 };
int[] data2 = data.AsParallel().
                  Select((x, i) => new { x, i }). // remember indices
                  Select((x) => new { x * 2, i }). // (in original)
                  OrderBy((x) => x.i). // ensure order is preserved
                  Select((x) => x.x). // get rid of indices from output
                  ToArray(); // (in original)

順序保持をサポートするために PLINQ によって導入された手順は、赤で示しています。ご覧のとおり、かなりの作業です。PLINQ は、このバージョンのクエリを記述した場合より、明らかにずっと効率的な方法でこの機能を実装します。ただし、手順は論理的に同じです。

副作用
PLINQ リソース
  • PLINQ に関する Joe Duffy のブログ記事
  • Channel 9 の LINQ に関する Anders Hejlsberg へのビデオ インタビュー
  • .NET Framework Developer Center の The LINQ Project
  • LINQ の進化の過程と C# のデザインへの影響 (MSDN Magazine の 2007 年 6 月号の Anson Horton の記事)
  • MSDN フォーラムの LINQ についてのディスカッション

PLINQ は、統計的純度と呼ばれるものに依存します。LINQ クエリの大部分は、ほとんどの場合、データ構造を変化させたり、不純な操作を実行したりしません。つまり、LINQ クエリの大部分は厳密に機能するということです。LINQ クエリは、データを入力として受け取り、計算を実行し、特定の変更を加えて、そのデータのまったく別のコピーを出力として作成します。ただし、この最善の使用方法が、コンパイラまたはランタイムによって強制されることはありません。この一般的な使用パターン以外のクエリを記述している場合は、PLINQ への移行がかなり複雑になる可能性があります。

例として、次の LINQ クエリを考えてみましょう。

var q = from x in data where (x.f-- > 0) select x;

where 句の述語がオブジェクトのフィールドを変更していることに注意してください。これは、競合状態や同時実行のバグを公開しないと、クエリを安全には並列に実行できない可能性があることを意味します。既定では、このようなクエリは安全でなく、PLINQ では実行できないことを前提とする必要があります。では、どのような場合に安全なのでしょうか。競合が発生するのは、述語が同じオブジェクトに対して複数のスレッドで実行されるときだけで、これは、データに重複するオブジェクトが含まれている場合にのみ起こる可能性があります。したがって、データが 1 つのセットの場合はまったく問題ありません。

この他にも、静的変数が使用される場合など、常に安全でないケースがあります。

class C { internal static int s_cCounter; }
// elsewhere...
var q = from x in data where (x.f == C.s_cCounter++) select x;

このクエリを並列に実行すると、がっかりすることでしょう。多数のオブジェクトが重複する f フィールド値を持つことになる可能性があり、これは競合状態によってのみ発生します。注意が必要なのは、この問題の解決に対して最初に思いつくアプローチが C.s_cCounter++ を Interlocked.Increment(ref C.s_cCounter) に置き換えることである点です。このアプローチは正しいソリューションになりますが、いずれかの種類の同期がクエリのスケーラビリティを大幅に低下させます。原則として、可変性や共有状態へのすべての依存をクエリから排除するよう努める必要があります。

Windows® プラットフォームはスレッド中心のプラットフォームで、多くのシステム状態がコード自体の一部を実行しているスレッドに割り当てられることにもなります。これは、スレッド ローカル記憶域、偽装などのセキュリティ情報、COM アパートメント (特にシングル スレッド アパートメント)、Windows フォームや Windows Presentation Foundation などの GUI フレームワークといった形式になります。これらのすべては、LINQ から PLINQ に移行すると、問題が発生する可能性があります。

LINQ モデルでは、クエリのすべてのコードはクエリを開始した同じスレッドで実行されますが、PLINQ では、クエリのコードは多数のスレッドに分散されます。残念ながら、.NET Framework の多数のサービスが本質的および透過的にそれらに内部的に依存するので、スレッドの関係への依存がある場合は必ずしも明確ではありません。考えられる最も良い方法は、上に示した同時実行の障害の原因に注意し、できる限りクエリ内からそれらを除外することです。

PLINQ を使用する

PLINQ を使用すると、LINQ 開発者は、同時実行やデータの並列化について理解していなくても、パフォーマンスの向上や、本質的にスケーラビリティの高いソフトウェアの構築など、並列ハードウェアを活用できるようになります。プログラミング モデルは単純で、より多くの開発者がより多くの並列ハードウェアを活用できるようになります。詳細については、「PLINQ リソース」のリファレンスを参照してください。

異種データ ソースの結合に LINQ-to-Objects、LINQ-to-XML、または LINQ を既に使用している場合は、既存のクエリが PLINQ を使用するようにするのは簡単なプロセスです。このテクノロジの考えられるその他の使用法については、「魅力的なシナリオ」を参照してください。まだ LINQ を使い始めていない場合は、並列速度向上がその使用を推奨するもう 1 つの理由です。PLINQ はまだ利用できませんが、プログラムで LINQ を使用すれば、将来への投資になります。

Joe Duffy は、マイクロソフトの Parallel FX チームの開発担当者で、ブログを www.bluebytesoftware.com/blog で定期的に公開しています。Joe は、Addison-Wesley から刊行される書籍『Concurrent Programming on Windows』の著者です。

Ed Essey は、マイクロソフトで 5 年間にわたりプログラム マネージャを務めており、並列コンピューティング モデルとインフラストラクチャを担当しています。


並列パフォーマンス: マルチコア コンピュータ用にマネージ コードを最適化する

並列パフォーマンス

マルチコア コンピュータ用にマネージ コードを最適化する

Daan Leijen and Judd Hall
この記事の内容 : :
  • Task Parallel Library
  • Parallel.For と ThreadPool
  • 静的な作業配分
  • フューチャ
この記事は次のテクノロジを使用しています:
Parallel FX Library
コンテンツ
TPL の概要
構造化並列処理
並列処理を過度に公開する
レイ トレーサの例
動的な作業配分
集計
fork と join の並列実行
タスクとフューチャ
複製可能タスク
タスク マネージャ

マルチプロセッサ コンピュータが標準になりつつある一方で、シングルプロセッサの需要は落ち込んできています。このため、複数のプロセッサでプログラムを並列に実行することがパフォーマンスを向上させる鍵といえます。ただし、そのような複数のプロセッサを実際に活用するアルゴリズムを作成するのは非常に難しいのが現状です。実際、多くのアプリケーションはシングルコアだけを使用し、マルチコア コンピュータで実行しても処理速度の向上は見られません。このため、独自のプログラムを新しい方法で作成する必要があります。

TPL の概要

Task Parallel Library (TPL) は、自動的に複数のプロセッサを使用できるマネージ コードを簡単に作成できるように設計されています。このライブラリを使用すると、既存の逐次コードで潜在的な並列処理を簡単に表現でき、公開された並列タスクは使用可能なすべてのプロセッサで同時に実行されます。通常、これにより処理速度が大幅に向上します。

TPL は、Microsoft® Research、マイクロソフトの共通言語ランタイム (CLR) チーム、および並列コンピューティング プラットフォーム チームが共同で作成しています。TPL は、Microsoft .NET Framework の次世代の同時実行サポートである Parallel FX Library の主要コンポーネントです。TPL のバージョン 1.0 はまだリリースされていませんが、最初の Parallel FX Communicty Tech Preview (CTP) が 2007 年の秋に MSDN® で利用できるようになります。詳細については、http://blogs.msdn.com/somasegar を参照してください。TPL は言語拡張が不要で、.NET Framework 3.5 以上で使用できます。

Visual Studio® 2008 は完全にサポートされ、すべての並列処理は通常のメソッド呼び出しを使用して表現されます。たとえば、1 つの配列の要素を二乗する次の for ループがあるとします。

for (int i = 0; i < 100; i++) {
  a[i] = a[i]*a[i];
}

反復はそれぞれ別個に行われる (後続の反復は前の反復で行われた状態の更新を読み込まない) ため、次のように TPL で並列 for メソッドを呼び出して潜在的な並列処理を表すことができます。

Parallel.For(0, 100, delegate(int i) {
  a[i] = a[i]*a[i];
});

Parallel.For は、3 つの引数を使用する標準の静的メソッドです。この最後の引数は、デリゲート式になります。このデリゲートは、前の例の変更されていないループ本体をキャプチャします。これにより、特にプログラムへの同時実行の導入を簡単に行うことができるようになります。

このライブラリには、動的な作業配分のための優れたアルゴリズムが含まれており、作業負荷や特定のコンピュータに自動的に適応します。一方、ライブラリのプリミティブは潜在的な並列処理を表すだけで、実際に並列処理は生成しません。たとえば、シングルプロセッサ コンピュータでは、並列 for ループは順番に実行され、厳密な逐次コードのパフォーマンスとかなり近いものになります。一方、デュアルコア コンピュータでは、ライブラリは 2 つのワーカー スレッドを使用して、作業負荷と構成に応じてループを並列に実行します。つまり、現時点でコードに並列処理を導入しておくと、複数のプロセッサが使用可能になった時点で、アプリケーションでは自動的にそれらのプロセッサが使用されます。同時に、コードは古いシングルプロセッサ コンピュータでも正しく実行されます。

ただし、ライブラリは共有メモリを使用する並列コードを正しく同期することはできません。特定のコードを安全に並列処理できるようにするのはプログラマの役目です。共有メモリに対する同時変更を保護するためには、ロックなどの他のメカニズムも必要です。ただし、TPL は同期に役立ついくつかの抽象化を備えています (これについてはこの後すぐに取り上げます)。

構造化並列処理

並列プログラミングの中で最も重要な抽象化の 1 つは並列ループです。たとえば、次の (未熟な) 行列乗算ルーチンについて考えてみましょう。

void SeqMatrixMult(int size, double[,] m1, double[,] m2, double[,] result)
{
  for (int i = 0; i < size; i++) {
    for (int j = 0; j < size; j++) {
      result[i, j] = 0;
      for (int k = 0; k < size; k++) {
        result[i, j] += m1[i, k] * m2[k, j];
      }
    }
  }
}

この例では、外側の反復は互いに独立し、潜在的に並列処理できるようになっています。この潜在的な並列処理を TPL を使用して公開するのは簡単です。まず、コンパイル時に System.Concurrency.dll アセンブリを参照します。次に、using ステートメントを使用してライブラリをコードにインポートします。

using System.Concurrency;

名前空間が使用可能になったら、行列乗算の外側の for ループを、静的な Parallel.For メソッドへの呼び出しに置き換えるだけです。

void ParMatrixMult(int size, double[,] m1, double[,] m2, double[,] result)
{
  Parallel.For( 0, size, delegate(int i) {
    for (int j = 0; j < size; j++) {
      result[i, j] = 0;
      for (int k = 0; k < size; k++) {
        result[i, j] += m1[i, k] * m2[k, j];
      }
    }
  });
}

Parallel.For コンストラクタは、3 つの引数を使用する標準の静的メソッドです。最初の 2 つの引数で、反復の制限 (0 ～サイズ) を指定します。最後の引数は、各反復で呼び出されるデリゲート関数です。このデリゲートは反復のインデックスを最初の引数として受け取り、前の例の変更されていないループ本体を実行します。デリゲートによってループ本体の自由変数が自動的にキャプチャされるため (result と m1 のように)、元のループ本体に変更を加える必要はありません。デリゲート式の詳細については、msdn.microsoft.com/msdnmag/issues/06/00/C20 を参照してください。

最後に、いずれかの反復で例外がスローされた場合は、すべての反復が取り消され、最初にスローされた例外が呼び出し元のスレッドで再度スローされます。これにより、例外は正しく伝達されて失われることはありません。

TPL を使用しない場合、このループで潜在的な並列処理を表すのはかなり難しくなります。.NET ThreadPool クラスを使用する場合でも、同期のコストや作業区分を考慮する必要があります。図 1 に、スレッド プールを使用して並列処理される行列乗算ルーチンを示します。

Figure 1 静的な作業配分と明示的な同期 (Close Figure 1)
void ThreadpoolMatrixMult(int size, double[,] m1, double[,] m2,
    double[,] result)
{
  int N = size;
  int P = 2 * Environment.ProcessorCount; // assume twice the procs for
                                          // good work distribution
  int Chunk = N / P;                      // size of a work chunk
  AutoResetEvent signal = new AutoResetEvent(false);
  int counter = P;                        // use a counter to reduce
                                          // kernel transitions
  for (int c = 0; c < P; c++) {           // for each chunk
    ThreadPool.QueueUserWorkItem(delegate(Object o)
    {
      int lc = (int)o;
      for (int i = lc * Chunk;           // iterate through a work chunk
           i < (lc + 1 == P ? N : (lc + 1) * Chunk); // respect upper
                                                     // bound
           i++) {
        // original inner loop body
        for (int j = 0; j < size; j++) {
          result[i, j] = 0;
          for (int k = 0; k < size; k++) {
            result[i, j] += m1[i, k] * m2[k, j];
          }
        }
      }
      if (Interlocked.Decrement(ref counter) == 0) { // use efficient
                                                     // interlocked
                                                     // instructions
        signal.Set();  // and kernel transition only when done
      }
    }, c);
  }
  signal.WaitOne();
}

この例は既に高度な内容になっています。作業項目にスレッド プールを使用し、単一の待機ハンドルと共にカウンタを使用してカーネル遷移の数を最小化しています。さらに、プロセッサの数に基づいてループを静的にチャンクに分割し、必要な数の 2 倍のチャンクを作成して、動的な作業負荷に適応できるようにしています。ただし、Parallel.For とは異なり、図 1 のアプローチはループ本体で例外を伝達せず、取り消すことができません。

このコードが Parallel.For メソッドよりも作成が難しく、間違いを起こしやすいのは明らかです。また、手動でチューニングしたり、最適に近い作業区分を使用しているにもかかわらず、スレッド プール アプローチでは一般的に Parallel.For メソッドよりもパフォーマンスが低下します。図 2 に、いくつかのテスト例の結果を示します。この結果は、750x750 要素を使用した行列乗算の外側のループを並列処理するときの相対的な処理速度の向上を表しています。つまり、1 は標準の for ループでの実行時間を表しています。テストは、3 GB のメモリが搭載され、Windows Vista® Ultimate を実行している 4 ソケットのデュアルコア コンピュータで行われました。シングルコア コンピュータでは、Parallel.For バージョンは実際には直接の for ループと同じように実行されます。

図 2 Parallel.For と ThreadPool のパフォーマンスの比較
図 2 Parallel.For と ThreadPool のパフォーマンスの比較

並列処理を過度に公開する

次のように 2 番目の for ループを並列処理すると、さらに多くの並列処理を公開できます。

Parallel.For( 0, size, delegate(int i) {
  Parallel.For( 0, size, delegate(int j) {
    result[i, j] = 0;
    for (int k = 0; k < size; k++) {
      result[i, j] += m1[i, k] * m2[k, j];
    }
  });
});

並列ループを入れ子にするのは問題がありませんが、一般的にこの方法では 2 つの理由でパフォーマンスが低下します。1 つは、この例の場合、使用可能なコアの数は一般に行列のサイズよりもはるかに少ないため、外側のループが既に十分すぎるほどの並列処理を行う機会を公開しているという点です。もう 1 つは、すべてのデリゲート式が自由変数を格納するためのメモリを割り当てているという点です。最初の例で、コストが反復で補われる割り当てが 1 つしかなかったのはこのような理由からです。新しいコードでは、内側の Parallel.For は外側のループの各反復でヒープ割り当てを実行します。CLR では割り当ては非常に効率よく行われますが、それでも反復ごとに実行される作業量と比較すると、はるかに大きなコストになります。

それらの反復は独立していないため、内側のループを並列処理できません。特に、すべての反復が result[i,j] の場所に追加されるため競合が発生します。このループを並列処理すると、2 つの反復によって現在の値がレジスタに同時に読み込まれ、追加されて結果が書き戻されます。このため、1 つの追加が失われます。内側のループを並列処理する唯一の方法は、ロックを使用して追加のループを正しく保護することです。もちろん、実際にこの方法を行うことはお勧めできません。しばらくの間追加の割り当てを無視した場合でも、同時実行の各反復は同じロックについて競合するため、パフォーマンスに大きく影響します。この点については、集計操作を説明する際に簡単に触れます。競合とロックの詳細については、msdn.microsoft.com/msdnmag/issues/05/08/Concurrency/default.aspx を参照してください。

レイ トレーサの例

レイ トレーシングは、写実的なレンダリングを簡単に生成できる優れた方法です。ただし、この技法では、計算の作業負荷が非常に大きくなります。各レイを並列に計算できるので、レイ トレーシングはこのライブラリの優秀な候補といえます。ここでは、Luke Hoban が作成した既存のレイ トレーサ (blogs.msdn.com/lukeh/archive/2007/04/03/a-ray-tracer-in-c-3-0.aspx を参照) を、TPL を使用して並列で実行できるように変更します。レイ トレーサは、図 3 に示すような画像を生成し、Parallel FX CTP でサンプルとして使用できます。元のレイ トレーサのコア ループは、結果の画像のすべてのピクセルを反復処理します。

図 3 並列レイ トレーサ
図 3 並列レイ トレーサ(画像を縮小するには、ここをクリックします)
図 3 並列レイ トレーサ
図 3 並列レイ トレーサ (画像を拡大するには、ここをクリックします)

void Render(Scene scene, Color[,] rgb)
{
  for (int y = 0; y < screenHeight; y++)
  {
    for (int x = 0; x < screenWidth; x++) {
      rgb[x,y] = TraceRay(new Ray(scene,x,y));
    }
  }
}

各レイは個別に追跡できるため、元のコードを 1 行変更するだけで並列処理できます。

void Render(Scene scene, Color[,] rgb)
{
  Parallel.For(0, screenHeight, delegate(int y)
  {
    for (int x = 0; x < screenWidth; x++) {
      rgb[x,y] = TraceRay(new Ray(scene,x,y));
    }
  });
}

8 コアのコンピュータで、元のコードは、350x350 ピクセルの画像に対して毎秒 1.7 個のフレームを生成できます。一方、並列バージョンを同じ 8 コア プロセッサ コンピュータで実行すると、毎秒 12 個のフレームが生成されます。これは 8 コア プロセッサ コンピュータで 7 倍の処理速度です。若干の変更だけでこのようなすばらしい結果になります。1 秒あたり 12 個のフレームが生成される場合、床の上をはずむボールの滑らかなアニメーションを作成するのに十分な速さです。これは非常に単純なレイ トレーサであるため、さらに最適化してより滑らかなアニメーションを作成することもできます。

動的な作業配分

スレッド プールを使用してループを手動で並列処理する場合、開発者は最終的に作業を静的に分割することがよくあります。たとえば、レイ トレーサでは、画像が偶数のパーツに分割されることがよくあります。各パーツは別々のスレッドで処理されます。実際の作業負荷は不均等に分割される可能性があるため、一般的にこの方法は適切とはいえません。たとえば、反射を表現するために画像の下側の計算に 2 倍の時間がかかる場合、画像の上側を処理するスレッドは、そのほとんどの時間を下側のスレッドの完了を待機するのに費やします。作業が均等に分割された場合でも、同時に実行されているシステムでのページのエラーやその他のプロセスにより、この状況が発生する場合があります。

複数のプロセッサで適切に機能するために、TPL は Work-stealing (ワーク スティーリング) という方法を使用して動的に適応し、ワーカー スレッドに作業項目を配分します。既定では、ライブラリにはプロセッサごとに 1 つのワーカー スレッドを使用するタスク マネージャがあります。これにより、OS によって最小のスレッド切り替えが確実に行われます。各ワーカー スレッドは、実行する作業の独自のローカル タスク キューをそれぞれ持っています。各ワーカーは通常新しいタスクを独自のキューにプッシュし、タスクが実行されるたびに作業をポップします。そのローカル キューが空の場合、ワーカーは作業そのものを探して、他のワーカーのキューにある作業を "盗もう" とします。

この方法の利点は、作業キューが分散され、ほとんどの操作がワーカーにローカルであるため、ワーカー間で同期がほとんど行われないという点です。これはスケーラビリティの点で重要です。さらに Work-stealing には、実証された適切なキャッシュの局所性および作業配分プロパティがあります。たとえば、作業負荷が不均等である場合、1 つのワーカーが特定のタスクの実行に長時間かかる可能性がありますが、他のワーカーがそのキューから作業を盗むので、すべてのプロセッサは常にビジー状態になります。通常のアプリケーションでは、タスクにどのくらいの時間がかかるのかを予測するのが難しいため、動的な作業配分は重要です。これは、プロセッサを多くの異なるプロセスで共有したり、ワーカー スレッドが取得するタイム スライスを予測できないようなデスクトップ システムに特に当てはまります。

図 4 に、4 つのスレッドを使用するアクションでの動的な作業配分を示します。この図は 図 3 と同じレイ トレースの画像を示していますが、ここでは各ワーカー スレッドが異なる色を使用してそのピクセルを表示しています。ライブラリがワーカー スレッドに均等に作業を配分し、作業負荷に動的に適応しているのがわかります。

図 4 ワーカー スレッドでの作業の配分
図 4 ワーカー スレッドでの作業の配分(画像を縮小するには、ここをクリックします)
図 4 ワーカー スレッドでの作業の配分
図 4 ワーカー スレッドでの作業の配分 (画像を拡大するには、ここをクリックします)

動的な作業配分を実行するだけでなく、ライブラリはワーカーがブロックされた場合にワーカー スレッドの数を動的に調整します。操作をブロックする例として、ファイルの読み取り、キーが押されるまでの待機、ユーザー名の取得 (これによってドメイン上のネットワークにアクセスできるため) などがあります。タスクが知らないうちにブロックされると、同時実行レベルが低下するのに伴いパフォーマンスも低下することがあります (その場合でもプログラムは正しく動作します)。パフォーマンスを改善するために、ライブラリはワーカー スレッドがブロックされているかどうかを自動的に追跡して確認し、必要に応じて追加のワーカー スレッドを投入して同時実行レベルを維持します。操作がブロック解除されたら、スレッド切り替えの影響を軽減するために一部のワーカーを終了させることもできます。

集計

ドメインで反復処理を実行して値を 1 つの結果に集計するために for ループを使用することがよくあります。たとえば、100 未満の素数を合計する反復処理の例を次に示します。

int sum = 0;
for(int i = 0; i < 100; i++) {
  if (isPrime(i)) sum += i;
}

あいにく、並列処理によってデータの競合が発生するためこのループは並列処理できません。各反復は、共有の sum 変数をロック保護を使用せずに変更します。2 つの同時反復が同時に sum の値を増やすと、それら両方がレジスタ内の同じ値を読み込み、追加し、それらの結果を書き戻す可能性があります。その結果 1 つの追加が失われます。次のようにロックを使用して追加を保護するのが正しい方法です。

int sum = 0;
Parallel.For(0, 100, delegate(int i) {
  if (isPrime(i)) {
    lock (this) { sum += i; }
  }
});

ただし、すべての並列反復が同じロックと同じメモリの場所 (sum) の両方を取得しようとして競合が起こるため、パフォーマンス上の問題が発生します。そのため、各ワーカー スレッドでスレッド ローカルの sum を維持し、ループの最後でグローバルな sum のみに追加した方が良い結果になります。このパターンは Parallel.Aggregate 操作でキャプチャされるので、次のように書き換えることができます。

int sum = Parallel.Aggregate(0, 100, // iteration domain
              0,                     // initial value
              delegate(int i) { return (isPrime(i) ? i : 0) }, // apply
                                                      // on each element
              delegate(int x, int y) { return x+y; }  // combine results
          );

集計操作では 5 つの引数を受け取ります。最初の 2 つの引数で、反復ドメインを指定します。これは列挙子にすることもできます。次の引数は、結果の初期値です。その後の 2 つの引数はデリゲート関数です。最初の関数は各要素に適用され、それ以外の関数は要素の結果を組み合わせるために使用されます。

ライブラリはスレッド ローカル変数を自動的に使用して、ロックを使用せずにスレッド ローカルの結果を計算します。ロックを使用するのは最後のスレッド ローカルの結果を組み合わせるときだけです。集計が並列処理される場合、要素は逐次集計とは異なる順序で組み合わされる可能性がある点に注意してください。したがって、デリゲート関数の組み合わせは関連するものにする必要があり、初期値は単位要素にする必要があります。

fork と join の並列実行

もう 1 つの一般的な並列パターンとして、fork と join の並列実行があります。例として、次の逐次クイックソート実装について考えてみましょう。

static void SeqQuickSort<T>(T[] domain, int lo, int hi) where T : IComparable<T>
{
  if (hi - lo <= Threshold) InsertionSort(domain, lo, hi);

  int pivot = Partition(domain, lo, hi);
  SeqQuickSort(domain, lo, pivot - 1);
  SeqQuickSort(domain, pivot + 1, hi);
}

このアルゴリズムは、要素型 T のジェネリックであり、必要なのは T インスタンスを比較できることだけです。特定のしきい値では、このアルゴリズムは挿入ソートに頼ることになります。挿入ソートを使用すると、少数の要素の場合にパフォーマンスが向上します。それ以外の場合は、入力配列を 2 つの部分に分割し、各部分を個別にクイックソートします。各並べ替えは配列のそれぞれ異なる部分で動作するため、これらの 2 つの並べ替えは並列で実行できます。これは、Parallel.Do メソッドを使用して簡単に表すことができます。

static void ParQuickSort<T>(T[] domain, int lo, int hi) where T : IComparable<T>
{
  if (hi - lo <= Threshold) InsertionSort(domain, lo, hi);

  int pivot = Partition(domain, lo, hi);
  Parallel.Do(
    delegate { ParQuickSort(domain, lo, pivot - 1); },
    delegate { ParQuickSort(domain, pivot + 1, hi); }
  );
}

Parallel.Do メソッドは、2 つ以上のデリゲートを引数として受け取り、それらを並列で実行する可能性のある静的メソッドです。クイックソートは再帰的であり、すべての呼び出しで追加の並列タスクが導入されるため、多くの並列処理が公開されます。また、ライブラリでは並列実行が保証されないため、ほとんどのタスクは実際には順番に実行されます。これは最適なパフォーマンスには欠かせない条件です。

タスクとフューチャ

これまでのすべての例は、並列コードのスコープがレキシカル スコープによって判断される構造化並列処理を示していましたが、すべての並列アルゴリズムがこの方法で表現できるわけではありません。さいわいなことに、ライブラリでも一般的な並列タスクがサポートされます。

class Task
{
  Task( Action action );

  void Wait();
  void Cancel();
  bool IsCompleted { get; }
  ...
}

潜在的に並列実行が可能な、関連付けられたアクションを指定してタスクを作成します。アクションは、タスクを作成してから最初に Wait メソッドを呼び出すまでのいずれかの時点で実行されます。関連付けられたアクションを別のスレッドで並列に実行することはできますが、アクションがスレッド間で移行されることはありません。これにより、プログラマは何の心配もせず、LeaveCriticalSecion を EnterCriticalSection と異なるスレッドで実行する場合など、Windows クリティカル セクションのようにスレッド アフィン変換の抽象化を使用できるので便利です。タスクが既に完了している場合は、Wait からすぐに制御が戻ります。

関連付けられたアクションで発生した例外はタスクに格納され、Wait が呼び出されるたびに繰り返し発生します。同様に、Parallel.For および Parallel.Do 関数はスローされたすべての例外を累積し、すべてのタスクの完了時に再度発生します。これにより、例外は失われることなく、依存対象に正しく伝達されます。

最後に、Cancel を呼び出して該当のタスクと関連付けられたアクションで作成されたすべてのタスク (子タスク) を取り消すことができます。取り消しは事前に準備されないため、実行中のタスクはライブラリに呼び戻すことで協調して作業を終了する必要があります。これは、たとえば新しいタスクを作成したり、Wait メソッドを呼び出したりすることで実行できます。親タスクが取り消されると、これらのライブラリの呼び出しによって (同期の) OperationCanceled 例外が発生し、アクションが停止します。

タスクは、作業項目が取り消しまたは待機が可能なハンドルを返し、例外が伝達される改善されたスレッド プールと見なすことができます。また、関連付けられたアクションによって結果が計算される、フューチャ (Future) と呼ばれるタスクのバリエーションもあります。

class Task<T> : Task
{
  Task ( Func<T> function );
  T Value { get; }            // does an implicit wait
}

フューチャは、結果を計算するタスクであり、標準のアクションではなく結果を返すアクションで構築されます。この結果は、Func<T> 型のデリゲートになります。ここで T はフューチャ値の型を表します。

フューチャの結果は、Value プロパティを通じて取得されます。Value プロパティは Wait を内部で呼び出して、タスクが完了していること、および結果値が計算されていることを確認します。Wait が呼び出されているため、Value を呼び出すと値の計算中に発生した例外がスローされます。これを確認する 1 つの方法は、フューチャを計算で確定された値または例外値のいずれかを含むものとして考えることです。

フューチャは、MultiLisp で既に実装されている古い概念です。ただし、ここに示すフューチャの概念は、プログラマが共有メモリを正しくロックすることに責任を持つという点で "安全" ではありません。これは、フューチャのアクションがメモリ トランザクションに自動的にラップされるアプローチとは対照的です。

フューチャ抽象化は、ループほど構造化されていないシンボリック コードで適切に動作します。たとえば、次のバイナリ ツリー ノードとリーフの定義について考えてみましょう。

class Node : Tree {
  int   depth;  // The depth of the tree
  Tree  left;   // The left sub tree
  Tree  right;  // The right sub tree
  ...
}

class Leaf : Tree {
  int value;  // values are stored in the leafs
  ...
}

この例では、リーフのすべての値を合計する仮想 Sum メソッドをツリーで定義するとします。リーフは単にその値を返します。ノードは、そのサブツリーの合計を追加します。

override int Sum() {
   int l = left.Sum();
   int r = right.Sum();
   return (r + l);
}

この場合、各子の計算は互いに独立しているため、並列に実行できます。この例の並列処理はレキシカルにスコープが設定されており、Parallel.Do を使用することができますが、デモンストレーションのためにフューチャを使用します。

override int Sum()
{
   Task<int> l = new Task<int>( left.Sum );
   int r       = right.Sum();
   return (r + l.Value);
}

左側の各サブツリーに対して、int 型の新しいフューチャを作成し、デリゲートをコンストラクタ引数として渡します。この例では、左側の子 left.Sum の Sum メソッドを呼び出さずに渡します。右側のサブツリーの合計を計算して続行します。フューチャを作成することで、他のプロセッサは左側のサブツリーの合計の評価を並列に開始できます。最後に、Value プロパティを使用してフューチャの値を要求します。

タスクが別のワーカー スレッドによって既に計算済みの場合、この呼び出しによって結果の値がすぐに返されます。タスクが別のワーカー スレッドで実行中の場合は、結果が使用可能になるまでブロックします (ただし、同時実行レベルを維持するために別のワーカー スレッドがスケジュールされます)。

タスクが全く開始されないという非常に一般的なシナリオをもう 1 つ示します。この例では、Value を呼び出すと呼び出し元のスレッドでタスクが直接実行されます。これは、間接的なメソッドの呼び出しを行うだけなので、一般的で非常に効率が良い方法です。一方、OS が提供するシグナルを待機している場合、シグナルを発生させることは不可能で、唯一実行できる操作は呼び出し元のスレッドをブロックすることですが、これは一般的にパフォーマンスを低下させます。上記の例では、値の計算方法が明確であり、ライブラリはスレッドをブロックするのではなく、関連付けられたアクションを直接実行します。

リーフで実行される作業量は非常に小さいため、特定のツリーの深さで順番に合計処理を実行して各タスクで実行される作業のサイズを増やす方がよい場合があります。たとえば、逐次 Sum メソッドの SeqSum を使用する場合は、次のように記述できます。

override int Sum()
{
   if (depth < 10) return SeqSum();

   Task<int> l = new Task<int>( left.Sum );
   int r         = right.Sum();
   return (r + l.Value);
}

一般的に、正しいしきい値制限は、タスクのアクションで実行される作業量とタスク オブジェクトを割り当てるコストとを比較して決定されます。経験から言うと、割り当ての負荷はかなり低く、しきい値の制限は通常約 100 の浮動小数点乗算になります。

フューチャは実際に優れた値であるため、フューチャを使用して 1 つのプログラムの論理的に区別された部分間に並列処理を導入できます。たとえば、別の異なるフェーズが実際にフューチャの値を要求するデータ構造にそれらのフューチャを保存できます。これに適するのは、ゲーム アプリケーションです。たとえば、1 つのフェーズですべてのキャラクタの新しい状態をフューチャとして計算でき、他のフェーズではそれらの状態のフューチャの値を後から使用します。マルチコア コンピュータでは、これらのフューチャは、各フェーズで実行されている作業と並列に計算できます。

複製可能タスク

ライブラリは、2 つの基本概念 (タスクと複製可能タスク) に基づきます。フューチャや並列 for ループなどの他の抽象化は、すべてこれら 2 つの基本概念の観点から表されます。これにより、操作は一貫したセマンティクスによって通常どおりに動作します。たとえば、例外は常に正しく伝達され、すべての抽象化は取り消すことができます (並列 for ループを含む)。

複製可能タスクは、実際には TPL が提供する標準の抽象化を拡張するライブラリ作成者向けであり、通常のコードで使う場合は慎重に使用する必要があります。複製可能タスクは、次のように通常のタスクから直接派生します。

class ReplicableTask : Task
{
  ReplicableTask( Action action );
}

複製可能タスクは、同時に複数のスレッドによって自身を実行できるタスクを表し、作業の動的配分から抽出している間に、遍在的にすべてに適用される同時実行パターンをキャプチャします。コンストラクタは、別のスレッドで並列実行され、複数のスレッドによって同時に実行される可能性のあるアクション デリゲートを受け取ります。それらの実行のいずれかで例外が発生した場合は、いずれか 1 つの例外だけが格納され、Wait によって再びスローされます。

複製可能タスクを使用できるのは、他のスレッドが作業の実行に参加する可能性がある場合です。Parallel.For と Parallel.ForEach のすべてのバリエーションは、複製可能タスクを使用して実装されます。たとえば、基本的な Parallel.For を次のように単純に実装できます。

static void For( int from, int to, Action<int> body )
{
  int index = from;
  ReplicableTask rtask = new ReplicableTask( delegate {
    int i;
    while ((i = Interlocked.Increment(ref index)-1) < to) {
      body(i);
    }
  });
  rtask.Wait();
}

すべての複製可能タスクはインデックス変数を共有するため、複製可能タスクのコンストラクタに渡される実際のアクション デリゲートは、必要な数のスレッドによって実行できます。実装では、これを使用してアイドル状態のプロセッサを作業に参加させることができます。

この実装では、各ワーカー スレッドは一度に 1 つのインデックスを要求します。これは、OpenMP の dynamic(1) に対応し、通常各インデックスの作業負荷が大幅に異なる場合に適切に機能します (詳細については、msdn.microsoft.com/msdnmag/issues/05/10/OpenMP を参照してください)。ただし各インデックスの作業負荷が小さい場合は、この方法によってキャッシュの競合が発生することがあります。その場合は、すぐにインデックスのストライドを処理することをお勧めします。OpenMP の dynamic(n) に対応し、ストライドを引数として受け取る次の Parallel.For を見てみましょう。

static void ForWithStride( int stride, int from, int to, Action<int> body )
{
  int index = from;
  if (stride <= 0) stride = 1;
  ReplicableTask rtask = new ReplicableTask( delegate {
    int i;
    while ((i = Interlocked.Add(ref index,stride)-stride) < to) {
      int end = Math.Min(i+stride, to);
      do {
        body(i);
        i++;
      }
      while (i < end)
    }
  });
  rtask.Wait();
}

複製可能タスクは、さまざまな並列反復法を実装するための強力な抽象化ですが、経験から言うと、標準の Parallel.For 実装および Foreach 実装は多くのシナリオで十分に機能するので、複製可能タスクの追加表現は実際にはそれほど必要にならないでしょう。

タスク マネージャ

タスク マネージャに属するすべてのタスクは、その名前が表すとおり、タスクを管理し、タスクを実行するワーカー スレッドを監視します。使用可能な既定のタスク マネージャは常に存在しますが、アプリケーションで明示的にタスク マネージャを作成することもできます。タスク マネージャのインターフェイスは次のように定義されます。

class TaskManager : IDisposable
{
  TaskManager();
  TaskManager( int maxConcurrentThreads );

  static TaskManager Current     { get; }
  int    MaxConcurrentThreads    { get; }
  ...
}

class Task
{
  Task( TaskManager taskManager, Action action )
  ...
}

タスク マネージャには、MaxConcurrentThreads プロパティから返される、関連付けられた同時実行レベルがあります。これにより、タスクを実行する理想的なスレッド数が常時マネージャに指定されます。これは単に手掛かりであるため、先の処理を行うために追加のスレッドを使用する必要がある場合、マネージャは動的にこの作業を行います。タスク マネージャを作成するときに、この数を明示的に指定できます。既定では、プロセッサの数と同じです。

一般的に、既定のタスク マネージャが常に使用できるため、タスク マネージャを明示的に作成する必要はありません。ただし、各タスク マネージャの同時実行レベルが異なる場合や、各タスク マネージャが別個のタスクのセットを処理する場合は、複数のタスク マネージャを使用することもあります。その場合は、新しいタスク マネージャを作成し、タスク マネージャを最初の引数として受け取る特殊な Task コンストラクタを使用して、そのタスクと、そのタスクマネージャのすべての子タスクを実行します。たとえば、次のコードについて考えてみましょう。

using (TaskManager tm = new TaskManager(2)) {  // only use 2 worker
                                               // threads for tasks
  new Task( tm, delegate {
    // tasks created in this delegate use the tm task manager by default
    ...
    // finally, show some statistics
    Console.WriteLine( "statistics: " + tm );
  }).Wait();
}

タスク マネージャ インターフェイスのもう 1 つの重要な点は、単一のワーカー スレッドを使用してすべてのコードを順番に実行することです。つまり、すべてのタスクと並列 for ループは順番に実行されます。これは、マルチプロセッサ コンピュータで並列実行する前に、コードを順番に実行したときにコードが正しく機能するかどうかを確認できるので、デバッグ目的で使用すると非常に便利です。

Daan Leijen は、Microsoft Research の調査員です。彼が現在関心を持っているのは、宣言型と関数型のプログラミング、型推論システム、および並列コンピューティングです。

Judd Hall は、マイクロソフトのプログラム マネージャ II です。


モバイル アプリケーション: 周りのノイズに応じて着信音の音量を調整する

モバイル アプリケーション

周りのノイズに応じて着信音の音量を調整する

Chris Mitchell
この記事の内容 : :
  • Pocket PC 開発
  • ラウドネスを測定する
  • カスタム インストーラ
  • P/Invoke を使用する
この記事は次のテクノロジを使用しています:
.NET Compact Framework
コードのダウンロード : : NoiseDetection2007_10.exe (193KB)
コンテンツ
アプローチ
参照値
実装
インストールの処理方法
結果

ある晴れた日の午後、近所のカフェの一角に座って静かで穏やかなひとときを過ごしていると、電話がかかってきました。いつものように音量を最大に設定していたためにとても大きな着信音が鳴り響き、危うくコーヒーをこぼしてしまうところでした。そこで、音量を下げました。

その後バーに行き、友人を待っていましたが、彼の姿はどこにも見あたりません。友人は待ち合わせがどのバーだったかを忘れてしまっていて、居場所を確認しようと電話をかけてきました。残念なことに、その着信音は聞こえず、電話に気付きませんでした。問題は、着信音の音量を下げたままだったために、騒々しいバーでは聞こえなかったということです。

これは、現在の携帯電話の処理能力が、マルチチャンネル オーディオの録音に最初に使用した PC と変わらないことを考えると、かなりおかしなことです。さまざまな機能があり、大量の情報にアクセスできる今日のインテリジェントなモバイル デバイスがあれば、この問題は発生するはずなどないかのように思われます。しかし、携帯電話を使用している誰もが、何らかの形でこの問題を経験しています。

この問題を解決する明白な方法は、音量設定の変更を忘れないことです。着信音の音量を変更できるのは、さまざまな環境の音響の違いに対応するためだと思われますが、実際はそのようには機能しません。デバイスは、もう少し常識的に動作できるはずです。簡単なことです。携帯電話が、静かな環境では小さな音で鳴り、騒々しい場所では聞こえるように鳴ればいいだけです。

この記事では、現在の環境で周囲のノイズに基づいて Pocket PC の着信音レベルを自動的に調整する Windows Mobile® アプリケーションの可能性と実用性について説明します。このアプリケーションの機能によって、電話が適切な音量で鳴るようになり、気まずい思いをすることも、電話を受け損ねることもなくなります。この記事では、Visual Studio® 2005 および C# を使用した Windows Mobile 開発の入門として、この問題を扱います。

電話の着信音が大きすぎて困ったことはありませんか。

アプローチ

携帯電話のセンサを使用して現在の環境をより適切に認識できるようにする、というプランです。マイクを使用して周囲のノイズを測定し、それに応じて着信音レベルを調整します。

これを機能させるため、図 1 に示すように、一連の特定の問題に対処する必要があります。どのような方法で、デバイスに内蔵されているマイクからオーディオ サンプルを取得するのでしょうか。ラウドネスの定義には、どのような基準を使用するのでしょうか。得られた情報に応じて、モバイル デバイスの着信音レベルをどのように変更するのでしょうか。まず、システムの最初の部分、つまりマイクから周囲の情報を収集する部分について説明しましょう。ただし、ラウドネスの基準を先に定義する必要があります。

図 1 着信音の音量を調整する高度な手順
図 1 着信音の音量を調整する高度な手順

ラウドネスは主観的なものであり、通常は等ラウドネスのグラフで表されます (音の主観的知覚の詳細については、心理音響学の研究に関する記事を調べることをお勧めします)。このグラフは、一方の軸に周波数を、もう一方の軸には音圧の単位 (通常はデシベル (dB)) を示しています。このグラフは、周波数と等ラウドネスの知覚の間の関係を示します。

関係値を決定するため、この研究では、個々の参加者がさまざまな周波数で 10 dB ずつ増加させた音を聞きました。参加者は、その音が 1 KHz の参照信号と比較して等しいラウドネスになった時点を判断するよう求められました。これらの関係は、表の作成と等ラウドネスのグラフの描画に使用されました。これらを電話のマイクから取得した周波数情報と共に使用すれば、ラウドネスの基準を作成できます。

音圧レベルの基準を参照値と比較して、大きな変化があった場合に着信音レベルを調整するアプローチもあります。変化の構成内容は、電話のプロファイルで定義できます。これにより、機種が異なる場合のマイクとスピーカーの違いに関する問題を解決できます。参照値を変更するだけで、このソフトウェアをさまざまな機種の電話で効果的に動作させることができます。また、マイクからの周波数情報をシステムで取得する必要がなくなるため、貴重なバッテリ電源の節約にもなります。

システムの設計の複雑さが、周波数の分割と dB 測定の冗長性によって簡略化されます。ただし、このアプリケーションには相対的なラウドネスの基準が必要です。オーディオ信号の処理では、二乗平均平方根 (RMS) をラウドネスの基本的な基準として使用できます (RMS の詳細については、実装段階を述べる際に説明します)。周波数属性は知覚されるラウドネスになおも影響しますが、RMS 値は精度のニーズを満たすことができます。これは、Pocket PC の音量設定が 5 段階しかないことを考慮すると、特にそうだと言えます。RMS 関数を使用すると、着信音量の調整に必要な値を取得できます。

参照値

マイクからサンプルを取得し、RMS 関数に入力したら、その出力を 5 段階の音量のいずれかにマップする必要があります。マッピング関数とは何でしょうか。また、どのようにして、既に説明した参照値に設定を関連付けるのでしょうか。参照値は各デバイスに固有の値で、携帯電話の着信音量を変更するタイミングを定義します。たとえば、環境がカフェや騒々しいバーと同じであることを RMS 関数が示したら、着信音レベルを適切に調整する必要があります。

このアプリケーションを複数のデバイス プロファイルをサポートするようにスケール アップする場合は、アプリケーションをセットアップするためのインターフェイスが必要です。しかし、この記事では、そのようなインターフェイスは実装しません。それは他の方に委ねましょう。

デバイス プロファイル用の参照値を生成するためのプロセスは、それほど複雑ではありません。まず、アプリケーションの RMS レポート機能を使用して、ニーズに合った複数の参照状況での値を取得します。ここでは、夜の住宅地域 (45 dB)、静かな屋内レストランやオフィス (55 dB)、一般的な屋内レストラン (65 dB)、交通量の多い道路から約 16 フィート離れた場所 (75 dB) の 4 つの一般的な環境を使用しました。これらの値は、Pocket PC の音量の 5 段階の状態にマップするための参照になります (もちろん、これらの場所でさまざまな RMS 値の取得に使用できるツールが必要です)。

すべての参照場所の平均値を取得したら、これらの値をアプリケーションのコードで上限値および下限値として使用します。

実装

統合モバイル開発プラットフォームのため、Visual C#® 2005 で今回のソリューションを構築します。Pocket PC の開発は Visual C# 2005 によってとても簡単になりましたが、デスクトップから Pocket PC アプリケーションを展開できるデスクトップ インストーラを作成するための、組み込みのウィザードはまだありません。インストーラを作成できる便利な無料ツールがいくつかあり、独自のカスタム インストーラを作成するためのメソッドもあります (カスタム インストーラの構築については、この記事の後半で説明します)。

アプリケーションの説明に戻りましょう。アプリケーションには、TooLoud という名前を付けます。このアプリケーションの作成には、次の 3 つの部分があります。

  1. オーディオを取得および処理するため、Microsoft® .NET Compact Framework の P/Invoke サービスを使用します。
  2. RMS 値を計算する関数を記述します。
  3. 平均 RMS 値を取得し、Pocket PC のレジストリを使用して音量にアクセスして設定する関数を記述します。

マイクにアクセスして Pocket PC デバイスからのオーディオを録音するために、さまざまなライブラリと ActiveX® コントロールを使用できます。この例は、デバイスのマイクにアクセスするための WaveIn クラスが用意されている P/Invoke ライブラリを基に構築します (msdn2.microsoft.com/aa446550.aspx を参照)。WaveIn クラスには、設定したレートおよびビット解像度でサンプルを保存できる構造があります。P/Invoke ライブラリには、Wave というヘルパ クラスも用意されています。このヘルパ クラスは、.wav オーディオ 形式に対する共通インターフェイスです。上記のリンクでは、実装されているすべてのクラスと元の WaveIn クラスおよび Wave クラスの関数の一覧を参照できます。

このソリューションに含まれているバージョンの P/Invoke ライブラリにあるクラスは、WaveIn、Memory、および Registry だけです。簡潔にするため、その他のクラスはすべて削除しています。図 2 に示すライブラリは、2 つの追加の関数を用意することで拡張されています。1 つは WaveIn (TooLoud) の AutoSetVolume で、もう 1 つは Wave の CalcRMS です。

図 2 TooLoud のクラス ダイアグラムで表示した P/Invoke ライブラリの一部
図 2 TooLoud のクラス ダイアグラムで表示した P/Invoke ライブラリの一部(画像を縮小するには、ここをクリックします)
図 2 TooLoud のクラス ダイアグラムで表示した P/Invoke ライブラリの一部
図 2 TooLoud のクラス ダイアグラムで表示した P/Invoke ライブラリの一部 (画像を拡大するには、ここをクリックします)

アプリケーションは、音量を設定するプロシージャの一部として CalcRMS を呼び出す AutoSetVolume を実行します。TestProc は、ユーザー インターフェイスで選択したモードに応じて、繰り返し呼び出すことも、一度だけ呼び出すこともできます。TestProc は、次の 6 つの関数を呼び出します。

  • AutoSetVolume は、最終目的 (音量の自動設定) を達成するために使用されます。
  • NumDevices は、入力デバイス数を取得します。
  • GetDeviceName は、指定された録音デバイスの名前を取得します。
  • Preload は、ミリ秒単位で、オーディオの指定されたサイズおよび最大量のバッファを事前に読み込みます。Start は録音時間を開始し (現在は 1 秒に設定されています)、Stop は現在の録音を終了します。
  • AutoSetVolume は、新しい音量設定を計算します。
  • Dispose は、バッファをクリアします。

Preload は、パルス符号変調 (PCM) 形式のタグ、8 ビットのサンプル解像度、1 つのチャネル、および 11,025 Hz のサンプリング レートを指定して、waveformatex (データを説明するヘッダー) のインスタンスを設定します。

特定の時間のオーディオ データのブロックを取得できるようになったので、このデータを使用して RMS 値を計算できます。二乗平均平方根の計算の定義を、図 3 に示します。

図 3 ラウドネス計算の一部
図 3 ラウドネス計算の一部(画像を縮小するには、ここをクリックします)
図 3 ラウドネス計算の一部
図 3 ラウドネス計算の一部 (画像を拡大するには、ここをクリックします)

マイクからのサンプル値を使用して RMS 値を計算するメソッドを定義するため、対応するコードを図 4 に示します。サンプル値はバイト形式 (0 ～ 255) で保存されているので、-127.5 ～ +127.5 の正規化したオーディオ信号を用意するには、中間値の 127.5 を引きます。

Figure 4 RMS 計算 (Close Figure 4)
public double CalcRMS(MainTest.DisplayLineDelegate ShowLine)
{
    byte[] data = new byte[dwBytesRecorded];
    Marshal.Copy(lpData, data, 0, data.Length);
    double temp = 0, sum = 0;
    int j = 0;

    ShowLine(data.Length.ToString());

    for (int i = 0; i < data.Length; i++)
    {
        temp = (double)data[i] - 127.50;
        sum = sum + (temp * temp);
        j++;
    }
    return Math.Sqrt(sum / j);
 }

マッピング関数で RMS 値を取得し、Pocket PC の現在の着信音レベルの設定に使用する値を定義します。もっと経験的に行うこともできますが、デモンストレーションのため、携帯電話の機能の簡単なプロファイルで行います。

既に説明したように、この技法の欠点は、スピーカー機能とマイク機能に大きな違いがある携帯電話の場合はおそらくその機種ごとにプロファイルが必要になるだろうということです。この問題は、さまざまな場所の参照値をユーザーが設定できるようにする構成インターフェイスを作成することで解決できます。または、各種のプロファイルを提供する Web サイトを用意することもできます。ここでは、参照値の 1 つのセットを入力する方法についてだけ説明します。ただし、これに配慮して、任意の電話のプロファイルに対してマッピング値を再現できるように、それらの値を取得する方法を示します。

RMS 値は、特定の dB レベルに関連付けられているさまざまな一般的な音環境を録音して生成しました。できれば、録音は十分な時間にわたって行い (カフェでサンプリングを行っているちょうどそのときに、誰かが皿を落とす場合もあるでしょう)、携帯電話を使用すると思われる典型的な場所を含める必要があります。各環境から平均値を読み取れば、それを音量設定の上限値および下限値として使用できます。

参照値を計算したら、適応性のあるプロファイルを作成する必要があります。これは、携帯電話で参照値に応じて着信音レベルを調整する方法を定義するだけです。調整には、さまざまな方法が考えられます。たとえば、非常に騒々しい環境では、携帯電話のバイブレーション機能をオンにすることもできます。この実装では、単純に、参照値によってマイク入力を 5 段階の音量にマップします。

Pocket PC の着信音レベルは、レジストリを使用して設定できます。レジストリ値は ControlPanel\SoundCategories\Ring の HKEY_CURRENT_USER で、変更する値は InitVol です。InitVol の値の範囲は、0 ～ 5 です。

図 5 のコードは、レジストリの標準書き込み関数を使用して着信音の音量値を設定するために使用できます。マッピングはいくつかの条件ステートメントとして実装されており、指定された上限値と下限値に基づいて着信音の正しい値を決定します。

Figure 5 現在の着信音レベルの設定 (Close Figure 5)
public Wave.MMSYSERR AutoSetVolume(MainTest.DisplayLineDelegate ShowLine)
{
    double AverageRMS = 0.00;
    if (!m_inited)xreturn Wave.MMSYSERR.ERROR;

    if (m_recording) Stop();

    try
    {
        // Write the data recorded to each buffer.
        // Get RMS value for each block and take the average
        // across all blocks.
        for (int i = 0; i < m_numBlocks; i++)
        {
            if (m_whdr[i] != null)
            {
                AverageRMS = AverageRMS + m_whdr[i].CalcRMS(ShowLine);
            }
        }

        AverageRMS = AverageRMS / m_numBlocks;
        ShowLine("Average RMS: " + AverageRMS);

        // Select volume based on reference values.
        uint Volume = 0;
        if (AverageRMS > 35.00)
        {
            //Range of this value for the volume control is from
            //0x0000 (min) non to 0x0005 (max).
            Volume = 5;
        }
        else if ((AverageRMS <= 35.00) && (AverageRMS > 30.00))
        {
            //Range of this value for the volume control is from
            //0x0000 (min) non to 0x0005 (max).
            Volume = 4;
        }
        else if ((AverageRMS <= 30.00) && (AverageRMS > 20.00))
        {
            //Range of this value for the volume control is from
            //0x0000 (min) non to 0x0005 (max).
            Volume = 3;
        }
        else if ((AverageRMS <= 20.00) && (AverageRMS > 10.00))
        {
            //Range of this value for the volume control is from
            //0x0000 (min) non to 0x0005 (max).
            Volume = 2;
        }
        else
        {
           //Range of this value for the volume control is from
           //0x0000 (min) non to 0x0005 (max).
            Volume = 1;
        }

        ShowLine("Set Volume to: " + Volume.ToString());
        PInvokeLibrary.Registry.CreateValueDWORD(
            "ControlPanel\\SoundCategories\\Ring", "InitVol",
            Volume);

        return Wave.MMSYSERR.NOERROR;
    }
    catch { return Wave.MMSYSERR.ERROR; }
    finally { FreeWaveBuffers(); }
}

インストールの処理方法

TooLoud アプリケーションのコードを作成したので、Pocket PC 用のインストーラを作成するためのさまざまなオプションから選択する必要があります。私もそうでしたが、せっかちですぐにでも使用したい方は、Windows Vista® 用 Mobile デバイス センターを使用してアプリケーションを転送できます。ただし、これはエンド ユーザーにとってはあまり適切ではありません。CAB ファイルを作成し、PC から実行して Pocket PC にアプリケーションをセットアップできる Microsoft インストーラ (MSI) のインストーラを作成するアプローチもあります。CAB ファイルが生成されたら、使用許諾契約書を備える完全なセットアップ ファイルをすばやく生成できるようにするプログラムを使用できます。デスクトップから携帯電話への展開を処理するカスタム インストーラを構築して、さらに高度な技法を提供することもできます。最後に、Visual C# 2005 を使用して、カスタム インストーラ、スマート デバイス CAB ファイル、および展開プロジェクトを作成する方法について説明します。Visual Studio 2003 を使用している場合は、msdn2.microsoft.com/ms838273 で、同様の機能を実装するためのガイドを参照できます。

プロジェクト全体は、MSDN® Magazine Web サイトからダウンロードできます。このプロジェクトには、TooLoud アプリケーション、SmartDeviceCab ビルダ、CustomInstaller、および Setup インストーラの 4 つのメイン コンポーネントが含まれています。

Visual Studio 2005 では、スマート デバイス CAB を簡単に作成できます。スマート デバイス CAB プロジェクトをソリューションに追加するには、ソリューション エクスプローラでソリューションをクリックし、[追加] をポイントして [新しいプロジェクト] をクリックし、[その他のプロジェクトの種類]、[セットアップと配置] の順に選択し、[スマート デバイス CAB プロジェクト] テンプレートを選択して、[OK] をクリックします。次にソリューション エクスプローラで、追加したスマート デバイス Cab プロジェクトを選択し、TooLoud アプリケーションのプライマリ プロジェクト出力を追加します。

SmartDeviceCab プロジェクトには、設定できるプロパティがいくつかあります。図 6 は、Visual Studio 2005 のプロパティ ページを示しています。これらのプロパティには、簡単なものとそうでないものがあります。たとえば、OSversionMin プロパティは、4.21 に設定した場合、アプリケーションで画面の向きの認識をサポートするかどうかを定義します。ただし、この値によって、Windows Mobile 2003 以前に基づく Pocket PC にアプリケーションをインストールすることができなくなります。Windows® 2003SE 以前に基づく Pocket PC ソリューションの場合、Compress プロパティと NoUninstall デバイス展開プロパティには、False を指定する必要があります (.NET Compact Framework 2.0 をインストールしている場合は、後者のプロパティは True に設定してもかまいません)。

図 6 SmartDeviceCab のプロパティ
図 6 SmartDeviceCab のプロパティ(画像を縮小するには、ここをクリックします)
図 6 SmartDeviceCab のプロパティ
図 6 SmartDeviceCab のプロパティ (画像を拡大するには、ここをクリックします)

CE Setup DLL は、レジストリ設定を構成したり、アプリケーションをサービスとしてインストールしたりするなどのポストインストール操作を実行するファイルを指定します。後者は、アプリケーションの実行タイミングについて高度な判断を行うサービスとしてバックグラウンドでアプリケーションを実行する場合などに役に立ちます。

CAB ファイルを作成したら、そのファイルを使用して携帯電話にアプリケーションを展開できますが、携帯電話にコピーしてインストールする CAB ファイルを起動する方法についての問題がまだあります。そのため、CAB ファイルをシステムのデスクトップにインストールしてアプリケーション マネージャ (図 7 を参照) を起動する展開プロジェクトを作成します。このアプリケーション マネージャは、携帯電話にインストールするアプリケーションを制御するデスクトップ上のプログラムで、Windows Mobile デバイス センターで実行します。

図 7 Mobile デバイス センターとアプリケーション マネージャ
図 7 Mobile デバイス センターとアプリケーション マネージャ(画像を縮小するには、ここをクリックします)
図 7 Mobile デバイス センターとアプリケーション マネージャ
図 7 Mobile デバイス センターとアプリケーション マネージャ (画像を拡大するには、ここをクリックします)

この展開プロジェクトは、Visual Studio テンプレート内に含まれている、デスクトップの展開プロジェクトと同じです。ただし、機能を拡張して、CAB ファイルをインストールするためにアプリケーション マネージャを起動するタスクを組み込む必要があります。これは、カスタム インストーラ クラスを作成して標準のインストーラを拡張することで行います。カスタム インストーラ クラスは基本クラスの AfterInstall と AfterUninstall の 2 つのメソッドをオーバーライドして、アプリケーション マネージャを起動するメソッドを追加します。アプリケーション マネージャの場所を見つけるには、デスクトップ コンピュータのレジストリ値にアクセスします。

図 7 に、カスタム インストーラで起動したアプリケーション マネージャ (前面のウィンドウ) を示します。カスタム インストーラのコードを図 8 に示します。

Figure 8 カスタム インストーラのコード (Close Figure 8)
private void Installer_AfterInstall(
    object sender, System.Configuration.Install.InstallEventArgs e)
{
    // Get fullpath to .ini file.
    string arg = Path.Combine(
        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
        "Setup.ini");

    // Run WinCE App Manager to install .cab file on device.
    RunAppManager(arg);
}

private void Installer_AfterUninstall(
    object sender, System.Configuration.Install.InstallEventArgs e)
{
    // Run app manager in uninstall mode (without any arguments).
    RunAppManager(null);
}

private void RunAppManager(string arg)
{
    // Get path to the application manager.
    const string RegPath = @"Software\Microsoft\Windows\" +
        @"CurrentVersion\App Paths\CEAppMgr.exe";
    RegistryKey key = Registry.LocalMachine.OpenSubKey(RegPath);
    string appManager = key.GetValue("") as string;

    if (appManager != null)
    {
        // Launch the application manager.
        Process.Start(string.Format("\"{0}\"", appManager),
            (arg == null) ? "" : string.Format("\"{0}\"", arg));
    }
    else
    {
        // Could not locate application manager.
      MessageBox.Show(
          "Could not launch the WinCE Application Manager :-(");
    }
}
図 9 ソリューション全体
図 9 ソリューション全体

標準のデスクトップのセットアップ プロジェクトをソリューションに追加する通常の方法で、展開プロジェクトを作成しました。展開テンプレートを追加した後、custominstaller.dll と smartdevicecab.cab をセットアップ プロジェクトに追加します。カスタム インストーラを参照するには、セットアップ プロジェクトを右クリックし、[表示]、[カスタム動作] の順に選択します。次に、custominstaller.dll を展開プロジェクトの [インストール] カスタム動作と [アンインストール] カスタム動作に追加します。

展開プロジェクトには、最後にもう 1 つ、次の setup.ini ファイルが必要です。

[CEAppManager]
Version      = 1.0
Component    = App

[App]
Description  = TooLoud Pocket PC
    application.
CabFiles     = SmartDeviceCab.cab

このファイルは、読み込む CAB をアプリケーション マネージャに通知します。図 9 に、ソリューションのレイアウトを示します (ビルドの順序は、TooLoud、SmartDeviceCab、CustomInstall、Setup の順に設定する必要があります)。これで、アプリケーション マネージャを備えたデスクトップ コンピュータを使用して、プロジェクトを構築および展開できるようになりました。

結果

この記事では、.NET Compact Framework 1.x を使用して Visual Studio 2005 用に記述した具体例についてまとめています。この例で示す基本的な原則と一般的な技法は、Windows Mobile デバイスのハードウェアを使用して周辺環境を監視し、それに応じて着信音の音量を自動的に調整するアプリケーションの設計に関連するものです。このコードはもちろん、.NET Compact Framework 2.0 などの最新テクノロジにリファクタできます。

このアプリケーションが適切に動作するかどうかを確認するため、研究室でのテストと実環境でのテストの両方を行いました。研究室でのテストでは、電話の着信音が周囲の環境より 2 ～ 5 dB だけ大きくなることが確かめられました (6 dB で約 2 倍の音量になります)。dB 値は、ある音をテスト信号として使用して dB メーターで測定しました。実環境でのテストは、ロサンゼルスのカフェと空港の 2 か所で行いました。このテストの測定では、とても科学的な方法を採用しました。私の携帯電話が鳴ったときに振り向いた人の数を数えたのです。

このアプリケーションでは、音を利用して携帯電話の着信音レベルを監視および調整します。すべてのアプリケーションと同様に、このアプリケーションにも制約があります。たとえば、携帯電話の機種ごとに参照値を構成する必要があります。これは対応できないタスクではありませんが、現在のユーザー インターフェイスではサポートしていません。もう 1 つの主な制約は、このアプローチは電源を大量に消費することです。ただし、携帯電話が鳴るときだけプロセスをアクティブにするなどして、この問題は簡単に解決できます。

これらの問題は別にして、このアプリケーションは、着信音レベルを変えるには手動で設定を変更する他に手段のない現在のアプローチよりずっと便利だと思います。改善すべき点もまだありますので、自由にコードを調整して、役に立つと思われる機能を追加してください。ここでは説明されていない多くの機能を思いつくことができるでしょう。

この記事の冒頭のイラストを提供していただいた Brian Richardson に感謝します。

Chris Mitchell は、機械学習と音楽信号および音声信号処理の博士号を取得した後、現在はカウフマン財団と NCGE に所属しています。Chris は、ネットワーク コンサルティングのほか、英国のケンブリッジにあるアングリア ラスキン大学で非常勤講師として勤務し、彼自身の研究費の補助としていました。連絡先は、c.mitchell@anglia.ac.uk です。彼は音楽テクノロジへの情熱からオーディオ プログラミングの基礎研究に進んでおり、この分野に関することであればいつでも喜んで話を聞くことでしょう。


編集者のメモ: 複雑さの複雑さ

編集者のメモ
複雑さの複雑さ
Howard Dierking

開発者にとって、 複雑さをもたらす新たな要素にうまく対処しなければならないというプレッシャーは増す一方のように思えます。開発者には、次第に複雑さを増すビジネス上のルールとプロセスに対応するシステムを作成し、保守する役割が求められます。また、さまざまなメディアにわたる動的で対話的な統合されたユーザー エクスペリエンスを作り出すように求められます。そして開発者は常に、最新の最も優れたハードウェア設計の微妙な差異を理解して活かさなければならないというプレッシャーにさらされています。

このような複雑さの増大に対処するには、どうすればよいでしょうか。歴史から教訓を得られるとすれば、新たなメタファ (比喩) と新たな抽象化の考案が解決策になると言えます。メタファを使用すると、特殊な対象を既存の言語と経験に即して一般的な方法で伝達することができます。同様に、抽象化を使用すると、さまざまな要素の効果的な識別を妨げかねない詳細情報が覆い隠され、設計の質を高めやすくなります。最終的に、抽象化はテクノロジ スペシャリストが他の要素への依存を最小限に抑えてソフトウェア要素を作成する手段となり、一方のメタファは、それらの要素の検討と適用を行うさまざまな分野のゼネラリストとスペシャリストの的確な判断を支え、スマートな解決策を生み出せるようにします。

私が MSDN® Magazine に携わるようになったのは最近で、それ以前はキャリアの大半をさまざまな業界向けのソフトウェアの設計と開発に費やしてきました。ご想像いただけると思いますが、マガジン制作のプロセスには多くの変動要素があり、その複雑さに新参者は圧倒されがちです。

しかしながら、開発者時代と同じように、仕事を体系的に学ぶには抽象化とメタファが非常に重要であることに気付きました。たとえば、編集と制作のプロセスの詳細は、抽象化によって相互から効果的に分離することができます。編集者はレイアウト作成の手順をすべて知る必要はなく、制作担当者は原稿の入手と加工の方法を気にする必要はありません。メタファの例としては、編集したテキストをレイアウトするプロセスを示す "流し込み" などがあります。全体として考えれば、開発者として培ったスキルがマガジンの仕事にも少しは役立つことがわかって安心しました。

今月号の MSDN Magazine では、新しいツールとメタファを通じて日々のソフトウェア開発タスクに伴う複雑さを解消しようとする取り組みを紹介します。Bill Horst の記事では、複雑さを増すビジネス上の要件や要望への対応策として、型の推定によってコードの抽象化レベルを上げて CLR の型システムからビジネス ドメインに近づける方法を説明します。Bill Chiles の記事では、動的言語ランタイム (DLR) の考察を通じて、このプログラミングの志向性の概念をさらに一歩進めます。ASP.NET AJAX に関するコラムの第 2 部となる Dino Esposito の記事では、サービスのメタファを Web プレゼンテーションのパターンに拡張し、ユーザー エクスペリエンスの充実を図ります。Daan Leijen と Judd Hall の記事では、ハードウェアに関する新たな技術革新 (複数のプロセッサやマルチコア プロセッサなど) の利用方法をめぐり、特定の低レベルの詳細を Parallel FX Library によって捨象する方法を説明します。続く Joe Duffy と Ed Essey の記事では、Parallel LINQ でこのような抽象化をサポートするために既存言語のメタファがどのように拡張されているかを説明します。

結局のところ、新たなメタファや抽象化の考案は確実に新たな複雑さを招き、また新たなメタファや抽象化が必要になります。しかしながら、新たな抽象化の層を 1 つずつ積み重ねるにつれて、ソフトウェアで解決できる問題の規模が大きくなります。この観点からすると、MSDN Magazine のビジョンについても何かメタファを考える必要がありそうです。いくつか候補を挙げるとすれば、ロック コンサート、ハンマー、鏡、ミラー ボールといったところでしょうか。

皆さんのお考えはいかがですか。ご意見やアイデアを電子メールでお寄せください。ことによると、皆さんのアイデアが今後数年間にわたるマガジンのビジョン形成の助けになるかもしれません。 £Howard

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Mahesh Prakriya、Kit George、Umachandar Jayachandran、Rob Earhart、Eric Li、Sandeep Ranade、Rob Shewan、Judd Hall、Ed Essey、Vance Morrison、JJ Johnston、Daniel Moth、Mike Hall、Jordan Parker、David Aiken、Joe Duffy、および Stephen Toub です。


ツールボックス: データベース管理、手軽な FTP、およびクラスタ キャッシュ

ツールボックス
データベース管理、手軽な FTP、およびクラスタ キャッシュ
Scott Mitchell

コンテンツ
データベース プロジェクトを効率的に管理する
FTP を手軽に利用する
キャッシュ処理のレベルアップを図る

データベース プロジェクトを効率的に管理する

データベースの管理、スキーマの調査と変更、クエリの作成と実行に使用できるツールは数多く存在します。そうした市販ツールの中で検討に値する製品の 1 つに、Altova の DatabaseSpy 2007 があります。DatabaseSpy は Microsoft® Access®、Microsoft SQL Server™、Oracle、MySQL、Sybase などの多種多様なデータ ストアに接続することができ、データベースに関係なく同じユーザー エクスペリエンスを実現できます。

データベースに初めて DatabaseSpy 経由で接続すると、新しいプロジェクトが自動的に作成されます。プロジェクトは、接続の詳細、資格情報、よく使用するクエリなどのリポジトリとして機能します。プロジェクトの内容はすばやく検索でき、よく使用する要素は Favorites フォルダに保存できます。さらに、プロジェクト マネージャのメニューとツール バーはカスタマイズできるようになっており、外部ツールを起動するように構成することができます。

DatabaseSpy には、SQL Editor という最新のクエリ エディタも搭載されています。まず第一に、このエディタには IntelliSense® と同様の動作をするオートコンプリート機能があり、クエリの入力時には、使用できる SQL キーワード、関数、およびテーブル名のドロップダウン リストが表示されます。クエリの実行時には、結果がグリッドに表示されます。SQL Server Management Studio の結果グリッドとは異なり、DatabaseSpy のグリッドは列ヘッダーをクリックすると並べ替えることができます。SQL Editor には、XML、CSV、HTML、Excel® などのさまざまな形式でデータをエクスポートするためのツールもあります。

さらに、DatabaseSpy には多彩でグラフィカルな Design Editor も組み込まれており、開発者は数回のポイントとクリックでテーブルの作成や変更を行うことができます。テーブルの作成や変更はデータベース構造変更スクリプトに反映され、画面上に表示されます。この情報は、どのような変更が加えられたのかを具体的に確認し、同じ変更を別のデータベースに適用するために役立ちます。

データベースの管理とクエリの実行に適した最新鋭のツールをお探しなら、DatabaseSpy 2007 のようなサードパーティ製のツールも選択肢に入れて検討してみてください。

価格 : 129 ドル

www.altova.com/DatabaseSpy

テーブルの作成と変更に役立つ DatabaseSpy の Design Editor
テーブルの作成と変更に役立つ DatabaseSpy の Design Editor (画像を縮小するには、ここをクリックします)
テーブルの作成と変更に役立つ DatabaseSpy の Design Editor
テーブルの作成と変更に役立つ DatabaseSpy の Design Editor (画像を拡大するには、ここをクリックします)

FTP を手軽に利用する

FTP (File Transfer Protocol) は、よく知られているオープン プロトコルで、サーバーとクライアント間のネットワーク経由でのファイル交換に使用されます。Web ブラウザは、現在最も普及しているユビキタスな FTP クライアント アプリケーションの 1 つですが、FTP の機能とユーザー エクスペリエンスは簡単なものであり、大多数のスタンドアロン FTP クライアント アプリケーションに見られる高度な機能は備えていないのが一般的です。

高度な機能を備えたスタンドアロン FTP クライアント アプリケーションの 1 つに、Tim Kosse が立ち上げた FileZilla (バージョン 2.2) というオープンソースのプロジェクトがあります。FileZilla を起動すると、FTP サーバーのアカウント情報を保管する Site Manager が表示されます。ここでは、よくアクセスする FTP サーバーの追加や分類を行い、認証資格情報や接続オプションなどの構成情報を保存することができます。FileZilla では、ネイティブな FTP 実装 (資格情報をプレーン テキストのままネットワーク経由で送信するため安全ではありません) と、標準化された 2 種類の安全な実装 (FTP over SSL と SFTP) の両方がサポートされています。

FTP サイトへの接続が完了した FileZilla のユーザー インターフェイスは、4 つのペインで構成されます。1 つ目のペインには、FTP サーバーに送信された FTP コマンドと、そのコマンドに対する応答が一覧表示されます。この低レベルの情報は、開発者を始めとする経験豊富なユーザーがトラブルシューティングを行う際にとても役立ちます。2 つ目のペインにはローカル コンピュータ上のファイルとフォルダが一覧表示され、3 つ目のペインには FTP サーバー上のファイルとフォルダが一覧表示されます。ファイルやフォルダを一方の場所からもう一方の場所にアップロードする操作は、一方のペインで項目を選択してもう一方のペインにドラッグするだけで実行できます。現在のアクティビティと保留中のアクションの一覧を示す転送キューは、下部のペインに表示されます。

FileZilla プロジェクトには、クライアント アプリケーションに加え、オープンソースの FTP サーバーの実装も含まれています。

価格 : 無料 (Visual C++ のソース コードを入手可能)

sourceforge.net/projects/filezilla

診断ツールとして FTP コマンドと応答を一覧表示する FileZilla
診断ツールとして FTP コマンドと応答を一覧表示する FileZilla (画像を縮小するには、ここをクリックします)
診断ツールとして FTP コマンドと応答を一覧表示する FileZilla
診断ツールとして FTP コマンドと応答を一覧表示する FileZilla (画像を拡大するには、ここをクリックします)

キャッシュ処理のレベルアップを図る

キャッシュは、使用頻度の高いデータを低速なデータ ソースから高速なデータ ソースにコピーすることによってアプリケーションのパフォーマンスを高める一般的な手法です。データドリブン アプリケーションの場合、通常はデータベースまたは Web サービスからローカル コンピュータのメモリに取り込まれたデータがキャッシュされることになります。

キャッシュの実装が最も簡単なのは、キャッシュが各アプリケーションに固有の場合です。これに対し、複数のアプリケーションで共通のキャッシュを使用する必要がある場合には、キャッシュの実装ははるかに難しくなります。たとえば、大規模な Web サイトでは通常、同じコンテンツを提供する複数のコンピュータから成るサーバー ファームを使用します。各要求は到着した時点でファーム内のいずれかのコンピュータに割り当てられますが、情報がファーム内の 1 台のコンピュータのメモリにキャッシュされていると、その情報に別のコンピュータのキャッシュからアクセスすることはできないため、キャッシュの有効性が低下してしまいます。この問題を解決する 1 つの方法は、キャッシュをデータベースなどの集中型データ ストアに移動するか、ファーム内のサーバーの 1 つをキャッシュ サーバーとして指定し、そのサーバーにキャッシュの唯一のコピーを保存することです。この集中型キャッシュ ストアで問題となるのは、単一障害点が発生することになり、ボトルネックになる可能性がある点です。

単一の集中型キャッシュ ストアのこのような制約を克服する手段として、Alachisoft の NCache 3.1 が候補に挙げられます。NCache は、複数のコンピュータに分散されているキャッシュをシームレスに管理するクラスタ キャッシュ フレームワークです。管理者はまず、NCacheManager ツールを使用してクラスタ トポロジとキャッシュ ポリシーを指定します。管理者は、キャッシュの保存先 (メモリまたはディスク)、削除ポリシー、最大キャッシュ サイズなどを設定することができます。これらの設定とトポロジは実行時に変更できます。さらに、NCacheManager には各種統計が揃っており、キャッシュの状態の監視に利用できるほか、キャッシュ設定を微調整してパフォーマンスを最適化するための指標としても利用できます。

キャッシュ クラスタを定義する際は、キャッシュをクラスタ全体に複製するかパーティション分割するかを指定できます。複製キャッシュ クラスタでは、クラスタ内の各コンピュータに同じ内容のキャッシュが保存されます。キャッシュを複製すると単一障害点がなくなるため信頼性が高まりますが、キャッシュの更新時にはクラスタ全体への複製が必要になります。したがって、複製クラスタは読み取り専用キャッシュや小規模クラスタに適していると言えます。一方、キャッシュをパーティション分割すると、キャッシュの内容がクラスタ内の複数のコンピュータに分けられるため、更新や大規模クラスタで高いパフォーマンスが得られます。

NCache のようなフレームワークを評価する際に筆者がいつも注目するのは、その学習曲線と統合のしやすさです。NCacheManager ツールを習得し、さまざまな構成設定を完全に理解するには確かに時間がかかりますが、NCache API は難なくアプリケーションに組み込むことができます。NCache API のクラスは Microsoft .NET Framework のキャッシュ API のクラスと密接に対応しており、その内部には、ASP.NET アプリケーションのアップグレードを構成ファイルの更新と同じくらい簡単に実行できるようにする HTTP モジュールが組み込まれています。NCache には、マイクロソフトの Caching Application Block (CAB) に統合するためのプロバイダも組み込まれています。

つまり、アプリケーションのキャッシュ機能を強化するプロセスは比較的簡単であり、既存のコードにほとんど、またはまったく手を加えることなく NCache 用にアップグレードできます。

価格 : Standard Edition - 1 CPU あたり 995 ドル

www.alachisoft.com

NCacheManager でのキャッシュ ポリシーの指定
NCacheManager でのキャッシュ ポリシーの指定 (画像を縮小するには、ここをクリックします)
NCacheManager でのキャッシュ ポリシーの指定
NCacheManager でのキャッシュ ポリシーの指定 (画像を拡大するには、ここをクリックします)

ご意見やご質問は、

Scott toolsmm@microsoft.com.

まで英語でお送りください。
Scott Mitchell は、多数の著書を出版し、4GuysFromRolla.com の創設者でもあります。また、1998 年からは Microsoft MVP としてマイクロソフトの Web テクノロジに携わっています。現在は、フリーのコンサルタント、トレーナー、およびライターとして活躍しています。Scott の連絡先は、Mitchell@4guysfromrolla.com です。また、ScottOnWriting.NET (英語) にブログを公開しています。

記載価格は印刷開始時点のものであり、変更されている可能性があります。このコラムに記載されている見解は、執筆者の個人的な見解であり、必ずしもマイクロソフトの見解を反映するものではありません。

MSDN Magazine does not make any representation or warranty, express or implied with respect to any code or other information herein. MSDN Magazine disclaims any liability whatsoever for any use of such code or other information.


CLR 徹底解剖: IronPython と動的言語ランタイム

CLR 徹底解剖
IronPython と動的言語ランタイム
Bill Chiles

コンテンツ
IronPython の Hello World
ipy.exe を起動する
AST の概要
IronPython の AST を見てみる
DLR の AST を見てみる
生成されるコードを見てみる
動的サイトについて理解する
DynamicSite オブジェクトの機能のしくみ
大きなメリット

Microsoft® .NET Framework は、共通言語ランタイム (CLR) という単一のランタイム上でさまざまなプログラミング言語をサポートするために作られました。CLR はそれらの言語に、ガベージ コレクション、ジャスト イン タイム (JIT) コンパイル、サンドボックス セキュリティ モデル、ツールの統合のサポートなどの共有サービスを提供します。このようにこれらの機能を共有することには、言語実装者にとって大きなメリットが 2 つあります。まず、低レベルのエンジニアリング作業の大半が既に行われているため、言語を実装するのが容易になります。また、CLR 上で実行される言語はシームレスに統合されるため、ライブラリやフレームワークを共有することで、新たに CLR に実装される言語で他の言語のそれまでの成果を活用できます。

CLR には動的言語のサポートも含まれています。その実例が IronPython 1.0 です (www.codeplex.com/ironpython)。2006 年 10 月号の「CLR 徹底解剖」 (msdn.microsoft.com/msdnmag/issues/06/10/CLRInsideOut) で James Schementi が説明しているように、動的プログラミング言語はプログラマに強力な機能と生産性の向上をもたらします。Microsoft は現在、動的言語のニーズに対応するための一連のサービスを CLR に追加する動的言語ランタイム (DLR) を開発しています。DLR によって追加される機能には、共有される動的型システム、標準のホスティング モデル、高速な動的コードや高速なシンボル テーブルを簡単に生成できるようにするためのサポートなどがあります。これらの追加機能により、.NET 用の高品質な動的言語実装を従来よりはるかに簡単に構築できるようになります。さらに、これらの機能を使用すると、DLR 上に構築された動的言語で、他の動的言語や CLR ベースの静的言語で作成されたライブラリを共有できます。

DLR では、好みの動的言語を使用して、いつもと同じようにコーディングを行うことができます。また、すばらしいツール、優れたパフォーマンス、共有ライブラリなどのメリットも享受できます。DLR を使用することによって標準の機能を共有できるため、より多くの時間を言語独自の機能に充てられるようになります。たとえば、ガベージ コレクション、コード生成、メソッド キャッシュなどの機能は、独自に実装する必要はありません。

DLR は、アダプティブ メソッド キャッシュを使用して高速な動的プログラムを生成します。DLR 上の言語実装はすべてその恩恵を受けられます。これにより、クラス優先順位リストが何度も検索されなくなるため、コードの実行速度が向上します。.NET メソッドをオブジェクトに対して呼び出すたびにオーバーロードが解決されることもなくなります (メソッド foo をオブジェクト o に対して呼び出す場合など)。

IronPython と DLR のソース コードは、Berkeley Software Distribution (BSD) スタイルの Microsoft Permissive License の下に Codeplex で公開されています (2.0 alpha リリースを参照)。これはごく初期のリリースであり、まだ多くの作業を必要としています。たとえば、低レベルのメカニズムの設計、パフォーマンス強化機能の実装、ドキュメントの作成などがこれから行われます。この記事が読まれる頃には、IronPython の Codeplex プロジェクトに加えて、DLR コード自体の Codeplex プロジェクトや、IronRuby の Codeplex プロジェクトが公開されていることでしょう。DLR JScript® や DLR VBX の実装の作業も行われていますが、これらはバイナリ形式のみの提供になるかもしれません。

IronPython の Hello World

ここでは単純な Python プログラム (Hello World にひと工夫加えたもの) を使用して、それを ipy.exe で実行するとどうなるのかを説明します。使用するプログラムは、以下に示す msdnmag.py です。

def yo(yourname):
   text = "hello, "
   return text + yourname

print yo("bill")

ここでは、IronPython の実装の興味深い側面について説明するために、古典的な "print hello" からは少し逸脱した機能を追加しています。これにより、たとえば、識別子の参照のための名前のバインドや、yo 関数の "+" の意味のキャッシングなどについて見ることができます。

ここでは IronPython 2.0 alpha を使用します。実際に試すことができるように、先に進む前にここでダウンロードしてください。

まず、Visual Studio® 2005 で ironpython.sln ファイルを開き、デバッグ ソリューション構成でソリューションをビルドします。次に、IronPythonConsole プロジェクトを右クリックし、左のナビゲーション バーで [デバッグ] タブを選択します。このプロジェクトをスタート プロジェクトとして設定します ([開始動作] の下の 1 つ目のオプション ボタンのセット)。次に、[開始オプション] で、コマンドライン引数として「msdnmag.py」と入力します。最後に、上の msdnmag.py の Python コードを、ソリューションをビルドすると作成される \bin\debug\ ディレクトリの msdnmag.py というファイルにコピーします。

ipy.exe を起動する

では、ipy.exe がどのように起動するのか見てみましょう。DLR には、言語実装者が簡単にインタープリタを実行できるように、既定のコンソール クラスがいくつか用意されています (実装する言語のパーサーとランタイム実装は記述する必要があります)。これらのコンソール クラスは、入力と出力、コマンドライン スイッチ、および DLR によって実装されるいくつかの共有診断スイッチを処理します。これにより、DLR で最初のコードを実行する際の手間が省けるほか、テスト ツールも利用できます。

まず、<installdir>\Src\IronPython\Compiler\Generation\PythonScriptCompiler.cs を開きます。次に、ParseFile 関数の "using (Parser parser = ...." という行にブレークポイントを設定します。

F5 キー ([デバッグ] の [デバッグ開始]) を押してプログラムを実行します。Python の実行エンジンを初期化するコード スニペットの解析中に、最初のブレークポイントにヒットします。このコード スニペットは、コンピュータ固有の初期化のために Python の site.py ファイルを読み込みます。では、どのようにしてここに到達したのでしょうか。コール スタックを確認すると、PythonConsoleHost の Main 関数で ipy.exe が起動していることがわかります。この関数は、DLR の共有コンビニエンス コードを呼び出します。IronPython 固有のコードに実行が戻り、コマンド ラインが処理されると、msdnmag.py ファイルが実行されます。

PythonConsoleHost クラスはごく小さなクラスで、LanguageProvider (および実行エンジン) を IronPython の実装に設定する非常に短い Initialize メソッドが含まれています。PythonConsoleHost の Main メソッドは、DLR によって提供される既定の ConsoleHost クラスを呼び出すだけです。

ConsoleHost は、言語実装者が cmd.exe スタイルの対話シェルをすばやく作成できるようにするためのヘルパ クラスで、LanguageProvider から CommandLine ヘルパ オブジェクトを取得します。ConsoleHost が CommandLine オブジェクトを実行すると、CommandLine オブジェクトの Initialize メソッドが呼び出されて、PythonCommandLine オブジェクトでコードの実行が開始されます。

PythonCommandLine オブジェクトの Initialize メソッドでは、モジュールを読み込むための Python パスが設定され、いくつかの組み込みモジュールが設定されて、IronPython の site.py ファイルがインポートされます。これらはすべて標準の Python の動作です。PythonCommandLine は、"import site.py" というコード スニペット文字列で IScriptEngine.Execute を呼び出して site.py ファイルをインポートします。

最初のブレークポイントがヒットするのは、PythonScriptCompiler.ParseFile の呼び出しです。これは、ここで "import site.py" が解析されるためです。ただし、まだ msdnmag.py が実行されていないので先に進みましょう。

AST の概要

ipy.exe によって msdnmag.py が実行されると、IronPython の実装と DLR が連携してファイル内のコードをコンパイルします。基本的なレベルで見ると、コンパイラは、コードを表すデータ構造を生成するパーサーと共に起動するパイプラインです。このデータ構造は、抽象構文ツリー (コンパイラ用語では AST) です。コンパイラはコードを段階的に分析しながら、AST を変換したり、新しいデータ構造を作成したりします。そして結果の構造に基づいて、マシン コードか、CLR のような仮想マシンのための中間言語 (IL) コードを生成します。

IronPython のコードでは、まず IronPython 固有の AST が生成され、そのツリーが DLR の AST にマップされます。AST 上で分析を実行したりツール (コード エディタ) をサポートしたりできるようにするための独自の中間ツリーを持つ言語もあります。ツールは、ユーザーがエディタで入力したソース コードに近いツリーを必要とします。多くの言語の AST は DLR の AST に似たものになりますが、DLR の AST では、セマンティクスに関する情報がより多く明示的に表現されます。DLR の AST は、コンパイラの後の段階で必要となるさまざまな変換の 1 つと見なすことができます。

興味深い点として、DLR では、AST の一部がコードの実行時に遅延コンパイルされます。これは、CLR で JIT コンパイルがコードの実行時にのみ使用されるのに似ています。コードが初めて実行されるときにはパフォーマンスのコストがやや高くなりますが、その後の実行は高速になります。後ほど、コードの特定の行が実行されるまでコンパイルされない AST の部分の例を紹介します。

IronPython の AST を見てみる

では、プログラムの実行に戻って、IronPython の AST を見てみましょう。まず、F5 キー ([デバッグ] の [続行]) を押します。IronPython エンジンが site.py の内容を実行のために解析するときに、再びブレークポイントで停止します。Visual Studio の [ローカル] ウィンドウで "cc" を展開し、"SourceUnit" を展開して、Name または DisplayName を確認すると、ParseFile が何を処理しているのかがわかります。

再び F5 キーを押します。前回はこのブレークポイントで停止しました。ここで [ローカル] ウィンドウを確認すると、cc.SourceUnit.DisplayName が msdnmag.py になっていることがわかります。これは、コマンドライン引数として ipy.exe に追加したファイルです。[ローカル] ウィンドウで cc.SourceUnit.Name が "__main__" になっているのは、Python では主要な実行モジュールにこのような名前が付けられるためです。

次に、F10 キー ([デバッグ] の [ステップ オーバー]) を 3 回押してこの行をステップ オーバーします。

ast = parser.ParseFileInput()

これで、IronPython の AST をデバッガで表示できるようになりました (図 1 を参照)。

図 1 デバッガの IronPython AST
図 1 デバッガの IronPython AST

この AST のルート ノードは SuiteStatement になっています。これは、一連のステートメントを表します。SuiteStatement には Statements メンバが含まれています。最初のステートメント (インデックス 0) を展開すると、FunctionDefinition ノードが含まれているのがわかります。FunctionDefinition ノードには、この関数の定義のステートメントの SuiteStatement である Body メンバと、結果の関数オブジェクトにバインドされる名前である Name メンバがあります。

msdnmag.py ファイル全体の IronPython AST を、詳細を一部省略して図式化すると、図 2 のようになります。

図 2 msdnmag.py の IronPython の抽象構文ツリー
図 2 msdnmag.py の IronPython の抽象構文ツリー(画像を縮小するには、ここをクリックします)
図 2 msdnmag.py の IronPython の抽象構文ツリー
図 2 msdnmag.py の IronPython の抽象構文ツリー (画像を拡大するには、ここをクリックします)

このツリーには複数の視覚表現が混在しています。AST ノードに別の AST ノードを指すメンバがある場合、たいていは、サブノードを指す矢印として示されています。サブノードには通常、親ノードのメンバの型の名前が付いています。たとえば、FunctionDefinition ノードには SuiteStatement 型のメンバがあるため、次のノードは SuiteStatement になっています。ノードにサブノードの記述が、インデントと中かっこを使用して含まれている場合もあります。これらのノードは、図を単純化するためにこのような形で示されています。

DLR の AST を見てみる

IronPython で DLR の AST を構築して、コンパイルの最後の段階を実行するための準備が整いました。ParseFile に設定したブレークポイントの下に、BindAndTransform の呼び出しがあります。これにより、IronPython の AST が DLR ツリーに変換されます。この変換の過程で IronPython によってすべての識別子が解決されて、DLR ツリー内のそれらの表現が、それらが表す変数の宣言/割り当ての情報を指すようになります。もちろん、実行時に値を検索することを表す AST ノードにコンパイルされる必要がある識別子もあります (実行時に追加されるモジュール メンバに解決される識別子や、モジュール辞書を通じてホストによって提供される遅延バインディングの値に解決される識別子など)。

このセクションの詳細の多くは、DLR に言語を移植しようとしている方にしか関心のないものかもしれませんが、DLR ツリーの図を見るだけでも、MethodCallExpression ノードと ActionExpression ノードがあることがわかります。MethodCallExpression ノードは、操作を実行する IronPython ランタイム関数の呼び出しにコンパイルされます。この関数は、引数が渡されれば、呼び出されるたびに同じ処理を実行します。

ActionExpression は特に興味深いノードです。というのも、このノードは、抽象的な操作やアクションを表すからです。抽象的なアクションには、たとえば、メンバの取得、オブジェクトのインデックス付け、オブジェクトの呼び出し、オブジェクトに対する加算の実行などがあります。DLR は、ActionExpression ノードを動的呼び出しサイトにコンパイルします。動的サイトとは、アクションと引数の型の特定の組み合わせに対応するメソッドをキャッシュするランタイム メカニズムです。これにより、コンパイル時に静的な型情報がほとんど (まったく) なくても、動的なコードを高速に実行できます。動的サイトについては後ほど詳しく説明します。

では、プログラムのステップ実行に戻りましょう。Shift + F11 キー ([デバッグ] の [ステップ アウト]) を 2 回押して SourceUnit.Compile で停止し、F10 キー ([ステップ オーバー]) を押して変数 "block" への代入を実行します。この変数は、ファイル全体の DLR AST のルートを保持します。

これで、DLR の AST を表示できるようになりました。Visual Studio の [ローカル] ウィンドウで、CodeBlock AST ノードである block というノードを展開します。これは、Python コードのファイルのルート ノードです。言語固有の構文ツリーから DLR の AST への変換の一環として、変数が (明示的または暗黙的に) 宣言されている場所と、識別子が参照している変数が明確に示されます。block の variables メンバを展開すると、モジュールのスコープで "__name__" と "yo" という 2 つの変数が宣言されていることがわかります (要素 0 と 1 を展開して、Variable オブジェクトの name メンバを表示します)。最初の変数は Python で必要とされるバインド名で、モジュールに対して作成されます。2 つ目の変数は、関数の定義を保持するモジュール変数です。

body の詳細に入る前に、図 3 をざっと見ておきましょう。この図は DLR AST の一部を展開したもので、先ほど見た IronPython の AST と同様の表現が反映されています。

図 3 DLR の AST
図 3 DLR の AST(画像を縮小するには、ここをクリックします)
図 3 DLR の AST
図 3 DLR の AST (画像を拡大するには、ここをクリックします)

ローカル変数 block の下の Body プロパティを展開します。Visual Studio デバッガの表示特性により、BlockStatement の内容を実際に表示するには body ノードの下の最初のノードを展開する必要があります。この DLR AST の BlockStatement ノードは、IronPython AST の SuiteStatement ノードに対応するもので、それぞれが一連のステートメントを表します。statements プロパティを展開します。

最初のステートメント (インデックス 0) は、EmptyStatement です。これは、IronPython の DLR ツリーへの変換の結果として生じるものです。IronPython では、モジュール ドキュメント文字列があった場合に備えて、2 つのステートメントが割り当てられます (ドキュメント文字列があった場合は __doc__ への代入が作成されます)。ここではドキュメント文字列がないため、最初のステートメントはプレースホルダ EmptyStatement のままになります。

ここでの関心の対象は、2 つ目のステートメント (インデックス 1) です。これが、ファイルの残りの部分 (記述したコード) の BlockStatement です。この BlockStatement には ExpressionStatement が含まれており、その後に BlockStatement が続いています (これについては後ほど説明します)。この ExpressionStatement ノードは BoundAssignment ノードをラップしています。これにより、代入の結果の値がスタックからポップ オフされます。Python の代入では値は返されません。この BoundAssignment には name メンバと value メンバがあります。これらは、ローカル変数 yo (Python コードが実行されるモジュールのローカル変数) を関数呼び出しの結果に設定することを表しています。この関数呼び出し (MethodCallExpression によって表されています) は、IronPython の関数オブジェクトを作成するランタイム ヘルパ関数を呼び出します。MethodCallExpression には、その呼び出しが MakeFunction というメソッドの呼び出しであることを示すメンバと、その呼び出しが受け取る引数を示すメンバがあります。引数の 1 つは関数の本体の CodeBlockExpression AST ノードで、もう 1 つは、作成された関数が受け取るパラメータの配列です。

BoundAssignment の value メンバである MethodCallExpression AST ノードを展開します。Arguments プロパティの 3 番目の要素 (インデックス 2) を見ると、yo の本体の CodeBlockExpression であることがわかります。この CodeBlockExpression には、CodeBlock である Block プロパティがあります。この CodeBlockExpression は、ラップしている CodeBlock を、モジュール変数に代入できる呼び出し可能な値に変換します。yo 関数の body の AST に入る前に図 4 を見てみましょう。これは、msdnmag.py の DLR AST 全体を図式化したものです。

図 4 msdnmag.py の DLR AST 全体
図 4 msdnmag.py の DLR AST 全体(画像を縮小するには、ここをクリックします)
図 4 msdnmag.py の DLR AST 全体
図 4 msdnmag.py の DLR AST 全体 (画像を拡大するには、ここをクリックします)

この図には注意すべき点がいくつかあります。前の図と同様に、このツリーには複数の視覚表現が混在しています。AST ノードに別の AST ノードを指すメンバがある場合、たいていは、サブノードを指す矢印として示されています。矢印とボックスの代わりにインデントと中かっこを使用して、ノードにサブノードの記述が含まれている場合もあります。Visual Studio の [ローカル] ウィンドウのスナップショットは、図の左側の MethodCallExpression ノードまでの AST を示しています。最初の矢印が点線になっているのは、EmptyExpression を含む BlockStatement が省略されているからです。[ローカル] ウィンドウのデータ構造をさらに展開していくと、この図に示されているようなツリー全体の構造が表示されます。

yo 関数の本体を表す CodeBlock ノードには、variables、parameters、および body のメンバがあります。これは、msdnmag.py ファイル全体を表す CodeBlock に似ています。後ほど示すように、このファイルの CodeBlock はモジュールの Initialize メソッドになります。yo 関数には、text という 1 つのローカル変数があります。これは、variables メンバを展開し、要素 0 を展開して、Variable オブジェクトを展開するとわかります。同じように展開していくと、yo 関数に yourname という 1 つのパラメータがあることもわかります。

この CodeBlock の body は、代入とリターンの 2 つのステートメントを持つ BlockStatement です。先ほど yo への代入で見たように、この BlockStatement の最初のステートメントは、BoundAssignment を保持する ExpressionStatement です。この代入は、text を "hello, " に設定することを表しています。この BoundAssignment オブジェクトには、ConstantExpression の value メンバと、Variable メンバがあります。Variable オブジェクトには text がローカル変数として記述されており、ローカル変数のスコープとして CodeBlock を指す block メンバが含まれています。

BlockStatement の 2 つ目のステートメントは、expression メンバが ActionExpression である ReturnStatement です。この AST ノードは、既に説明したように、特に興味深いノードです。この ActionExpression ノードには、DoOperationAction である action メンバがあります。DoOperationAction は、ここでは Add 操作を表しています。そのほかに、BoundExpression オブジェクトである 2 つの要素を持つ arguments メンバもあります。1 つ目の BoundExpression には、"hello, " に設定されたローカル変数 text を表す variable メンバがあります。2 つ目の BoundExpression には、パラメータ yourname を表す variable メンバがあります。

次に、ツリーを遡って、2 つの ExpressionStatment を含む BlockStatement を見てみましょう。2 つのうちの 1 つは、関数オブジェクトに yo を代入する BoundAssignment の ExpressionStatment です。2 つ目の ExpressionStatement は、式の値をスタックからポップ オフするためのコードがコンパイラによって出力されるようにするために存在しています。この ExpressionStatement には、MethodCallExpression である expression メンバがあります。

この MethodCallExpression は、print ステートメントを実装する IronPython ランタイム関数の呼び出しを表しています。MethodCallExpression には、print を表す .NET RuntimeMethodInfo である method メンバと、ActionExpression である arguments メンバがあります。ActionExpression は、既に触れたように、高速呼び出しのための動的サイトにコンパイルされます。この ActionExpression には、yo の呼び出しの CallAction である action メンバがあります。そのほかに、2 つの要素を持つ arguments メンバもあります。1 つは、式がローカル関数 yo の値であることを表す variable メンバを持つ BoundExpression です (yo は、定義されているモジュールのローカル関数です)。もう 1 つは、文字列 "bill" を保持する value メンバを持つ ConstantExpression です。

以上で、DLR の AST をひととおり見てきました。IronPython のツリーには開発者が目にする Python コードが直接表現されているのに対し、DLR の AST にはより明確な情報が含まれています。たとえば、識別子のバインドの情報や、スコープを表す明確なブロックが含まれているほか、動的サイトにコンパイルされるアクション ("text + yourname" のノード) の代わりにランタイム メソッド呼び出し (Print の呼び出し) の明確な表現が含まれています。さらに、Python のセマンティクスに準拠したコードになるよう、式の結果がスタックからポップ オフされるようにするノードも含まれています。

生成されるコードを見てみる

プロセスがどのように開始され、どのように解析が行われるのかを見てきたので、今度は、生成されるコードを見てみましょう。そのためには、ipy.exe のスイッチと、Lutz Roeder の Reflector プログラムを使用するのが最も簡単です。これらのスイッチを使用すると、ipy.exe は (Lightweight Code Generation や動的メソッドの代わりに) 静的コードを生成してディスクに保存します。aisto.com/roeder/dotnet/ から Reflector をダウンロードし、ironpython.sln をビルドした bin debug ディレクトリで、コマンド ラインから次のコマンドを実行します。

ipy.exe -D -X:SaveAssemblies -X:StaticMethods msdnmag.py

snippets1.dll と msdnmag.exe という 2 つのファイルがディレクトリに生成されます。次に、次のコマンド ラインを使用して、これらのファイルに対して Reflector を起動します (必要に応じて絶対パスを指定してください)。

C:\where\you\put\reflector.exe snippets1.dll, msdnmag.py

DLL と EXE のリストで msdn と snippets1 を見つけます。msdnmag、msdnmag.exe、"{} -"、"__main__$mod_2" の順に展開し、Initialize メソッドをダブルクリックします。これが、msdnmag.py ファイルの本体です。DLR が生成した IL から逆アセンブルされたソース コードが表示されます。Reflector を使用したことがある方はご存じのように、これはすばらしいツールですが、いくつかの欠点もあります。たとえば、余分な一時変数が生成されることがあるため、表示される C# コードを鵜呑みにしないようにしてください。

ここではこの Initialize 関数について詳しく説明しませんが、AST で見たように yo が MakeFunction の呼び出しの結果に設定されていることがわかります。その後、yo を呼び出した結果に対してランタイム ヘルパの PythonOps.Print が呼び出されています。"Call-Simple-1.Invoke" という部分に注目してください。Call-Simple-1 オブジェクトは、メソッド キャッシュ メカニズムの一部です。メソッド キャッシュについては、それが yo 関数でどのように使用されているのかを辿りながら、この後で詳しく説明します。

次に、yo 関数を見てみましょう。左側のペインで yo$1 をダブルクリックします。どこで text が "hello," に設定され、どこで "DoOperation-Add-0.Invoke(text, yourname)" の結果が返されるのかがわかります。これが、Python で記述したコードの本質です。DoOperation-Add-0 は、メソッド ルックアップの結果をキャッシュするオブジェクトです。これについてはこの後すぐに説明します。また、DoOperation-Add-0 の詳細を見ていく過程で、snippets1 の実行時に生成されるいくつかの関数についても説明します。

動的サイトについて理解する

動的サイトを使用すると、動的言語のコードを高速に実行できます。動的サイトは、特定の型のオブジェクトに対して特定の操作を実行するためのメソッドのキャッシュを管理します。DLR で使用されている動的サイトのメカニズムは、実証済みの動的言語実装による研究と経験に基づいています。生成されたコードに含まれていた Call-Simple-1 や DoOperation-Add-0 などのオブジェクトは、DynamicSite<Targ1, Targ2, Tresult> の派生型のインスタンスです。ここでは、キャッシュの理論について簡単に説明します。詳細については、Web で "dynamic language method caching (動的言語のメソッド キャッシュ)" または "polymorphic inline method caching (ポリモーフィック インライン メソッド キャッシュ)" を検索してください。

動的言語では、各呼び出しサイトで発生する追加のチェックと検索がパフォーマンスの障害になります。そのままの実装では、コードの特定の行を実行するたびに繰り返しクラス優先順位リストでメンバが検索され、場合によっては、メソッドの引数の型のオーバーロードが解決されます。動的言語では、o.m(x, y) や x + y などの式に対して、オブジェクト o の正確な種類は何か、m は o の何にバインドされているのか、x の型は何か、y の型は何か、x と y の実行時の実際の型に対して "+" は何を意味するのか、などをチェックしなければなりません。静的に型指定された言語では (またはコード内に型のヒントが十分にあり、型の推定を使用する場合)、各呼び出しサイトで適切な命令やランタイム関数呼び出しを正確に出力できます。これが可能なのは、コンパイル時に何が必要かが静的な型からわかるからです。

動的言語は、その動的機能により、生産性の大幅な向上と強力で簡潔な表現を実現します。しかし実際には、コードは毎回同じ種類のオブジェクトに対して実行される傾向があります。したがって、コードのセクションが初めて実行されたときに行われたメソッド検索の結果を記憶しておけば、パフォーマンスを向上させることができます。たとえば x + y で、この式が初めて実行されたときに x と y が整数だった場合に、コードのシーケンスや、2 つの整数が渡された場合に加算を実行するランタイム関数を記憶しておくことができます。その後は、その式が実行されるたびに検索が行われることはなくなります。x と y がまた整数であることがチェックされるだけで、検索なしで適切なコードにディスパッチされます。これにより、操作のセマンティクスと使用するメソッド キャッシュ メカニズムによっては、2 つの型チェックと加算の命令によるインライン コード生成にまで結果を縮小できる場合もあります。

DynamicSite オブジェクトの機能のしくみ

静的言語コンパイラでは、コンパイル時にメソッドを選択したり特定のコードを出力したりして、加算、メンバの取得、インデックス付けなどの操作を実行できます。DLR では、コンパイル時に出力すべきコードがわかるとは限らないため、DynamicSite オブジェクトに対して Invoke メソッドを呼び出すためのコードが出力されます。このオブジェクトは、操作とデリゲート (Invoke によって呼び出されます) をキャプチャします。デリゲートにはキャッシュ ロジックが含まれています。このキャッシュ ロジックは、デリゲートが実行時に新しい引数の型の組み合わせに遭遇するたびに更新されます (デリゲートは実際には関数へのポインタをラップしていますが、ここでは説明を簡潔にするためにデリゲートを関数のように使用しています)。ここまでは簡単です。では、さらに詳しく見ていくことにしましょう。

デリゲートでは、まず最初に UpdateBindingAndInvoke が呼び出されます。呼び出しサイトが初めて実行されたときには (x + y が実行されるとします)、UpdateBindingAndInvoke はその型の x とその型の y の Add 操作の実装を引数で照会します。実装を取得できた場合は、x と y が現在の型と同じ型かどうかをチェックするコードを含む新しいデリゲートが生成されます。その後の呼び出しで x と y が同じ型だった場合は、新しいデリゲートのコードによって、先ほど取得した実装が呼び出されます。x または y の型が違っていた場合は、デリゲートのすべてのキャッシュ チェックが失敗して、再び UpdateBindingAndInvoke が呼び出されます。その結果、また新しいデリゲートが生成されます。最新のデリゲートのコードには、x と y の新しいテストだけでなく最初のテストも取り込まれるため、どちらのターゲット実装が呼び出されても型が一致します。

では、msdnmag.py のコードの実行で DLR によって生成された DoOperation-Add-0 デリゲートについて見てみましょう。Reflector に戻り、左側のペインで、今度は snippets1、snippets.dll、"{} -"、および "Type$_stub_$4" の各ノードを展開します。Handle メソッドをクリックすると、右側のペインに図 5 のコードが表示されます。

Figure 5 逆アセンブルされた "+" のキャッシュ (Close Figure 5)
public static object Handle(object[],
    FastDynamicSite<object, object, object> site1,
    object obj1, object obj2)
{
    if (((obj1 != null) && (obj1.GetType() == typeof(string)))
        && ((obj2 != null) && (obj2.GetType() == typeof(string))))
    {
        string text;
        string text1 = text =
            StringOps.Add(Converter.ConvertToString(obj1),
                          Converter.ConvertToString(obj2));
        return text;
     }
     return site1.UpdateBindingAndInvoke(obj1, obj2);
 }

これは、"text + yourname" の実行後に DLR によって DoOperation-Add-0 デリゲートに対して生成された IL の逆アセンブル コードです。yo 関数を再度呼び出すと、このデリゲートが実行されます。このコードのパフォーマンスの問題に気付いた方もいらっしゃるかもしれませんが、それについては後ほど説明します。このコードでは、2 つの引数が文字列かどうかがテストされ、2 つの文字列が渡された場合は加算の IronPython ランタイム実装が呼び出されます。

いずれかの引数が文字列でない場合、このデリゲートは UpdateBindingAndInvoke を呼び出して新しいデリゲートを生成します。新しいデリゲートは現在のデリゲート (上のコードのデリゲート) と入れ替わるため、次に DoOperation-Add-0.Invoke が実行されるときには更新後のデリゲートが呼び出されます。ここで msdnmag.py のコードに変更を加えて、2 回目の yo の呼び出しでは "text + yourname" が 2 つの整数に対して実行されるようにしてみました。すると、2 回目の呼び出しの後に、DLR によって図 6 のような 2 つ目のデリゲートが生成されました。

Figure 6 逆アセンブルされた "+" のキャッシュ 2 (Close Figure 6)
      public static object Handle(object[]
    FastDynamicSite<object, object, object> site1,
    object obj1, object obj2)
{
    if (((obj1 != null) && (obj1.GetType() == typeof(int)))
        && ((obj2 != null) && (obj2.GetType() == typeof(int))))
    {
        return Int32Ops.Add(Converter.ConvertToInt32(obj1),
                            Converter.ConvertToInt32(obj3));
    }
    if (((obj1 != null) && (obj1.GetType() == typeof(string)))
        && ((obj2 != null) && (obj2.GetType() == typeof(string))))
    {
        return = StringOps.Add(Converter.ConvertToString(obj1),
                               Converter.ConvertToString(obj2));
    }
    return site1.UpdateBindingAndInvoke(obj1, obj3);
  }

ここでは、Reflector によって挿入される余分な一時変数を削除しています。これと同じ追加の試みを実際に行うと、先ほどとは多少異なるコードが得られます。今度は、コードで引数が整数かどうかがテストされるようになっています。引数が整数だった場合は、整数に対する Add を実装するランタイム ヘルパ関数が呼び出されます。整数のテストとターゲット コードの後には、先ほどと同じコードが続いています。ここでは新しい型から順番にテストされるようにしていますが、プロファイラを使用してプログラムを実行すれば、すべての適切な引数のテストを最適な順序で生成することも可能です。もちろん、以前に見つかっていない新しい型の組み合わせを持つ引数が見つかった場合は、デリゲートは再び UpdateBindingAndInvoke を呼び出します。

このコードのパフォーマンスの問題に気付いた方もいらっしゃるでしょう。現在の DLR ではより適切なコードが生成される可能性もありますが、アルファ リリースの段階では、まだ DLR で機能とコード生成の両立が実現されていません。このコードでは、型 (文字列) のテストが行われた後に引数が文字列に変換されています。これには 2 つの問題があります。1 つ目の問題は、.NET Framework 2.0 JIT では "if" テストから結果のコード ブロックに型情報が伝達されないことです。Add の実装は静的に型指定されたメソッドであるため、引数が文字列として渡される必要があります。2 つ目の問題は、DLR によって生成されたコードがランタイム変換関数の遅延呼び出しを行っていることです。DLR はここでキャスト操作を出力することもできたはずです。

ここで取り上げる最後のパフォーマンスの問題は、現在の DLR では同じテストの繰り返しを避けるような構造のテストが生成されないことです。たとえば、2 つの整数や 2 つの文字列の加算をすばやく実行できる更新後のデリゲートでは、引数が null に対してテストされています。テストのたびにこれが繰り返されていますが、これは必ずしも必要ではありません。UpdateBindingAndInvoke は、操作の実装を検索する際、実際にはその操作の実行方法をオブジェクトに問い合わせています。つまり、DynamicSite によって表される操作の実行方法のルールを提供するようにオブジェクトに要求しているのです。このルールには、実行するテストの AST と、テストが true だった場合に実行されるターゲット コードの AST が含まれます。DLR でテスト全体のツリーの変換を実行して、デリゲートのコードのさらなる最適化を行うことも可能です。

最後のポイントとして、ここではオブジェクトのメンバを取得するために動的サイトを使用しているため、インスタンスと動的な型の変更を処理する必要があります。DLR では o.m() について、o が同じオブジェクトを指していればどの m を呼び出せばよいかが正確にわかるとは想定できません。一部の言語では実行時に型を変更できるため、メンバが置き換えられる可能性があります (インスタンスでメンバが必然的にオーバーライドされるなど)。DLR ではグローバル バージョン カウンタが使用されており、新しい型や変更された型があった場合にインクリメントされます。型のバージョンはさまざまな原因によって変更されます (メンバの変更など)。DynamicSite のキャッシュが、型のバージョンが変更されたオブジェクトを参照していた場合、そのキャッシュは失敗します。その場合、DynamicSite は再度検索を実行してデリゲートを更新します。

大きなメリット

生成される DynamicSite 型はジェネリック型です。msdnmag.py のサンプル Python コードではジェネリック型を使用してもあまりメリットはありませんが、"if x < 1:" などの式では DynamicSite<Object, Int, Bool> を生成できます。このサイトの Invoke メソッドは、1 つの引数を整数として受け取ってブール値を返すように静的に型指定されます。動的サイトの内部のデリゲートも同じように静的に型指定されるため、結果のコードが高速化されます。デリゲートがキャッシュでテストしなければならない引数は 1 つだけになり、スタックの処理が進むにつれて、JIT でブール値を適切な場所に渡せるようになります。これにより、if テストの周囲のキャストやチェックがすべて取り除かれます。

一部の動的言語には、明示的な型指定のオプションがあります。そうでない場合も、.NET 静的メソッドの呼び出しやリテラル定数などから、実装時には型を推定することができます。この型情報は、変数の有効期間や条件の分岐などの全体にわたって伝達できます。言語に型情報がある場合は、完全な型情報を持つ DynamicSite 型にコンパイルされる DLR AST を生成できるため、キャッシュの Invoke メソッドの呼び出しとターゲット コードの呼び出しのインライン コード生成が可能になります。

現在、DLR によって生成されるジェネリック DynamicSite 型には 6 個以下のジェネリック型パラメータがあります。最初の N-1 個のパラメータは引数の型を表し、N 番目のパラメータは操作の結果の型を表します。型が 6 個を超えた場合は、ランタイム ヘルパ関数のメソッド呼び出しを実行するだけの AST を生成する必要があります。それらの関数では、すべてを実行時に計算するために遅延処理を行う必要があります。将来的には DLR でジェネリックの組を使用して、静的に型指定された引数を多数指定できるようになるでしょう。ほとんどのプログラムにとって最適な数が見つかれば、この数が 6 から増やされる可能性もあります。

ここでは、言語固有の AST と DLR の AST の調べ方を詳しく説明しました。単純なツリーについての基本的な情報に基づいて、特定の言語が DLR 上でどのように構築されているのかを調べることができます。実際に自分で DLR 上に言語を実装しようとする場合も、この手法を使用できます。そうでない場合も、内部で何が行われているのかを見るだけでも楽しいものです。

そのほか、言語実装者のために用意されている DLR の共有インフラストラクチャも確認しておく必要があります。ガベージ コレクション、JIT コンパイル、動的サイト、共通動的型モデルなど、CLR と DLR には、活用できる機能が数多く用意されています。.NET ランタイム上への言語の実装は、現在でも良い結果が得られるようになっていますが、さらなる改良が続けられています。DLR の Version 1 はすばらしいものになるでしょう。もう間もなく、.NET 上への動的言語の実装が驚くほど簡単になり、優れた成果が得られるようになります。

ご意見やご質問は、

こちら clrinout@microsoft.com.

まで英語でお送りください。
Bill Chiles は Microsoft のシニア プログラム マネージャで、動的言語のプロジェクト (CMU Common Lisp、Dylan) とツールにそのキャリアの大半を費やしてきました。Visual Studio Core Technologies チームに 9 年間所属していましたが、現在は IronPython と 動的言語ランタイムに取り組んでいます。


基本的な本能: Visual Basic 2008 における型の推定

基本的な本能
Visual Basic 2008 における型の推定
Bill Horst

コンテンツ
型の推定が必要となる理由
型の推定とラムダ式
その他の影響
型定義に移動する
型の推定を有効または無効にする

近くリリースされる Visual Basic® 2008 には、LINQ (Language Integrated Query) をサポートする目的で一連の新しいテクノロジが追加されています。その 1 つが型の推定です。以前のバージョンの Visual Basic では、図 1 に示すように、コード内で別の型が明示的に宣言されない限り、メンバの型は Object であると想定されていました。このように変数を宣言する場合の難点は、コンパイラが想定できる変数の値の型が Object に限定されることです。IntelliSense® ではすべてのオブジェクトに共通のメンバの情報しか表示されませんが、実行時に実際のインスタンスで他のメンバを使用できる場合があることを開発者は知っています。遅延バインディングを使用する場合は、他のメンバが実行時にバインドされます。つまり、コード行が実行されるまでそれらのメンバは一切検証されません。したがって、存在しないメンバ (図 1 の cust.Name など) がコードで参照されていても、コンパイラではその問題が発見されません。このため、MissingMemberException が発生しやすくなり、開発者はより多くの情報を自力で追跡しなければならなくなります。

Figure 1 型の推定が行われない Visual Basic 2005 (Close Figure 1)
Class Form1
    Sub Main()
        Dim a = 3                   ' Type: Object    Value: 3 {Integer}
        Dim b = DateTime.Now        ' Type: Object    Value: Current Time

        Dim cust = GetCustomer(someID)
        MsgBox(cust.Name) ' Customers have FirstName and LastName fields.
                          ' This code generates an exception at Run-Time.

        Dim procs = Process.GetProcesses
        For Each g In procs   ' Compile Error: 'g' is not declared
            Console.WriteLine(g.Id)
        Next
    End Sub
End Class

Visual Basic 2008 の型の推定を使用すれば、宣言ステートメントにおける割り当ての右側に基づいて、メソッドレベルのメンバの型が推定されます。図 2 にそのいくつかの例が示されています。ここでは、図 1 で示した cust.Name という存在しないメンバへの参照をコンパイラが認識し、型が明示的に指定されている場合と同じように、使用可能なすべてのメンバについて IntelliSense 情報が提供されます。図 2 に示されている For ループでは、変数 g が "procs" 式の型の宣言として認識されます。

Figure 2 Visual Basic 2008 における型の推定 (Close Figure 2)
Class Form1
    Sub Main()
        Dim a = 3                   ' Type: Integer   Value: 3
        Dim b = DateTime.Now        ' Type: Date      Value: Current Time

        Dim cust = GetCustomer(someID)
        MsgBox(cust.Name) ' Here we get a compiler error, catching the
                          ' problem quickly

        Dim procs = Process.GetProcesses
        For Each g In procs         ' Type: Process
            Console.WriteLine(g.Id) ' No compile errors
        Next
    End Sub
End Class

ただし、型の推定が行われない場合もあります。ローカルの Dim ステートメントでは、宣言行に割り当てがある場合にのみ、型の推定が行われます。そのため、変数の宣言の外部で割り当てを行うと、コンパイラはその型が Object であると想定します。クラスレベルのメンバの型もこれまでのように Object と推定されるので、関数、サブルーチン、プロパティ、クラス/構造のフィールドなどには型の推定が適用されません。具体的な例を、図 3 に示します。

Figure 3 型の推定が行われないケース (Close Figure 3)
Class Form1
    Sub Main()
        Dim d         ' Assignment not on the declaration line
        d = 3         ' Type: Object    Value: 3 {Integer}
    End Sub
                      ' Class-level
    Dim e             ' Type: Object    Value: Nothing
    Dim f = 3         ' Type: Object    Value: 3 {Integer}

    Function Func1()  ' Type: Object    Value: 3 {Integer}
        Return 3
    End Function
End Class

型の推定が必要となる理由

Visual Basic 2008 で型の推定が必要なのは、明示的に定義されない型の作成を可能にする新しい言語構成要素が導入されたためです。コード内では型を指定できないので、型が明示的に定義されていない式に変数を割り当てるには、型を推定する必要があります。

この明示的に定義されない型は、匿名型と呼ばれます。事前に定義しなくても、オブジェクトを作成し、一部のメンバを初期化できます。これにより LINQ ステートメントでは、考えられるすべての形態のオブジェクトを事前に定義することなく、さまざまな数のフィールドを (さまざまな順序で) 指定できます。オブジェクトは既に定義されている型と必ずしも一致しないので、コンパイラは暗黙的に型を生成します (この型の名前は重要ではなく、コードで使用できるように開発者に公開されることもないので、この型は匿名型と呼ばれます)。型の推定機能を使用することで、コンパイラは、匿名型のこのオブジェクトを型が明示的に定義されているオブジェクトと同じように扱うことができます。匿名型は、SQL スタイルのクエリを Visual Basic で作成する LINQ ユーザーにとって非常に便利です。

匿名型が LINQ の開発にいかに役立つかを次の例で示します。自社への注文のレコードを含んだデータベースから、ワシントン州に出荷されたすべての注文に関する特定の情報を表示するとします。次の SQL コードでは、ID、Date、Address、City、State、Zip、Shipped などのさまざまなフィールドで構成されるレコードを含んだ Orders というテーブルを操作します。このクエリは、このテーブルから Shipped フィールドの値が True で、かつ State フィールドの値が Washington であるすべてのレコードを返します。

SELECT Orders.ID, Orders.Date AS OrderDate, Orders.Address,
       Orders.City, Orders.State, Orders.Zip
FROM Orders
WHERE Shipped = 'True' AND State = 'Washington'

LINQ の進化により、同様の操作を Visual Basic で実行できるようになりました。次のコードは、Orders に対して同じクエリを実行し、結果を OrdersShippedToWA 変数に割り当てます。Orders は、データベース テーブル、メモリ内配列、メモリ内コレクション、または IEnumerable インターフェイスを実装する任意の型のいずれであってもかまいません。

Dim OrdersShippedToWA = From Ord In Orders _
                        Where Ord.Shipped _
                            And Ord.State = "Washington" _
                                Select Ord.ID, OrderDate = Ord.Date, _
                                Ord.Address, Ord.City, _
                                Ord.State, Ord.Zip

OrdersShippedToWA に対して推定される型は IEnumerable(Of <anonymous type>) です。IEnumerable の各要素は、クエリで選択されたメンバ (ID、OrderDate、Address、City、State、および Zip) を持つことになります。クエリ式の Select 部分で使用するメンバの組み合わせごとに型を定義する必要がなければ、LINQ がはるかに使いやすくなり、その機動性も増すことは想像がつきます。こうした理由から、Visual Basic コンパイラは適切なメンバを含んだ匿名型を生成し、後からコードで要素が参照された場合にその型を推定します。次の For ループでは、Ord 変数の型が OrdersShippedToWA で説明した同じ匿名型と推定されます。

For Each Ord In OrdersShippedToWA
    Dim City As String = Ord.City
Next

この例では、Ord 変数の型が明示的に宣言されていませんが、そのメンバ名はわかっています。バックグラウンド コンパイラが型を推定し、IntelliSense はこれまでと同じようにサポートされます。これが Visual Basic 2008 に型の推定が追加された主な理由です。

Order オブジェクト全体を操作する別のシナリオを考えてみましょう。クエリによって式が 1 つだけ選択される場合は、特定の型が推定されます。コレクションで使用されている型に対してメソッドまたは他のメンバが定義されている場合は、そのメソッドやメンバも型の推定を通じて呼び出すことができます。

Dim SortedWAOrders = From Ord In Orders _
                     Select Ord _
                     Order By Ord.ID Ascending

Dim FirstShipTime = SortedWAOrders(0).CalculateShipTime()

実際に、型の推定により、クエリ自体の中でメソッドを使用できます。次の例では、メソッドによって計算された出荷時間の順に結果が返されます。

Dim SortedShipTimes = From Ord In Orders _
                      Select Ord _
                      Order By Ord.CalculateShipTime() Ascending

コード内で匿名型を初期化できると便利な場合があるため、この機能も Visual Basic でサポートされています。図 4 の Part A は、明示的な型がない NewCustomer 変数を示しています。定義内のメンバの型はどこにも定義されていないので、それらの型も推定されることに注意してください。Part B が示す別の割り当てでは、匿名型のメンバがコンパイラによって認識されます。NewCustomerCity 割り当てで変数が参照されるときには、その変数で IntelliSense 機能がサポートされます。Part C では、匿名型のメンバと一致するメンバを選択する SeattleCustomers というクエリが初期化されます。この場合、コンパイラはこのステートメントに必要な匿名型と一致する匿名型が既に存在することを認識します。新しい匿名型を作成する代わりに、SeattleCustomers に対して NewCustomer と同じ型が推定されます。Part E では、NewCustomer に割り当てられたオブジェクトを SeattleCustomers クエリからのデータに追加できます。これは、両方の場所で同じ匿名型を使用できることが型の推定で認識されたためです。

Figure 4 クエリと変数から一致させる匿名型 (Close Figure 4)
'[Part A] Anonymous Type variable declared
Dim NewCustomer = New With {.Name = "Bill Horst", _
                            .Age = 25, _
                            .City = "Seattle"}

'[Part B] Member recognized by Compiler/IntelliSense
Dim NewCustomerCity = NewCustomer.City

'[Part C] Name, Age and City selected from records in
'         Customers database table
Dim SeattleCustomers = From cust In db.Customers _
                       Where cust.City = "Seattle" _
                       Select cust.Name, cust.Age, cust.City

'[Part D] Seattle Customers stored in a variable with
'         type List(Of <anonymous type>)
Dim SCList = SeattleCustomers.ToList()

'[Part E] Variable Type and Query Result Type match
SCList.Add(NewCustomer)

型の推定とラムダ式

型の推定は、ラムダ式に対応する際にも役立ちます。ラムダ式は、単純な関数に対してインラインでデリゲートを使用できるようにする新しい Visual Basic 言語機能です。この機能は、単純なデリゲートをパラメータとして渡す場合に特に役立ちます。関数名を指定する必要はなく、パラメータと返す式を宣言してラムダ式を渡すことができます。次の例では、Customer と String を受け取って、Customer の都市名が String と一致するかどうかを表すブール値を返す関数が宣言されています。その後、この関数は Filter メソッドにデリゲートとして渡されて、おそらくはこのブール値に基づいてフィルタ処理を行います。

Orders.Filter( _
    Function(cust As Customer, city As String) cust.City = city)

これは基本的に戻り値の型がない関数定義であるため、式の型を特定して適切なデリゲートを宣言し、その型が有効であることを検証する際に、型の推定が役立つことが明らかになります。

その他の影響

Option Explicit をオフにすると、明示的な宣言なしにコードでローカル変数を使用できます。既に説明したケースと同様に、この変数は Object 型と想定され、呼び出しはすべて遅延バインディングされます。注目されるのは、暗黙的に定義された変数では型の推定が行われない点です。このため、動作はまったく変わっていません。次のコードは一例を示しています。

Sub SwapTwoInts(ByVal x As Integer, ByVal y As Integer)
    z = x
    x = y
    y = z
End Sub

Visual Basic 2005 では、イミディエイト ウィンドウでのデバッグ中に、現在のスコープで定義されていない識別子に式を割り当てることで、一時変数を暗黙的に宣言できました。その結果、Object 型の変数が作成され、この変数に指定した値が割り当てられました。これは、Option Explicit Off を指定したコードでこの割り当てを行う場合の動作とほぼ同じです。Visual Basic 2008 では、ここで説明したイミディエイト ウィンドウ使用時の動作と Option Explicit Off 指定時の動作が Visual Basic 2005 の場合と同じになります。したがって、型の推定は行われません。イミディエイト ウィンドウに次の 2 つのコマンドを入力すると、整数値 12 が代入された Object 変数が表示されます。

intVar = 12
?intVar
12 {Integer}

型定義に移動する

変数定義で型が明示的に指定されている場合は、型識別子のコンテキスト メニューから Visual Studio® の [定義へ移動] 操作を簡単に呼び出して、型が定義されている場所を特定できます。次のケースでは、ユーザーが IEnumerable をクリックすると、オブジェクト ブラウザに IEnumerable(Of T) が表示されます。

Dim customers As IEnumerable(Of Customer) = GetCustomers()

ここで説明する新しい言語機能を使用すると、型を一目で見分けるのが難しくなる場合が増えることが予想されます。型の推定により、型は明示的である必要がなくなりました。また、LINQ をサポートする新しい言語構成要素によって、型がかなり不明確になる可能性があります。次の変数の型も IEnumerable(Of T) ですが、ここではかなり不明確であり、その型に移動するために [定義へ移動] を呼び出す術もありません。

Dim customers = From cust In _
                    (From c In db.Customers Select c) _
                Where cust.FirstName = "Danae" _
                Select cust.LastName, cust.Address

For Each c In customers
...

この問題が契機となって、Visual Studio 2008 に [Go To Type Definition] (型定義へ移動) コマンドが導入されました。このコマンドはコンテキスト メニューからも呼び出せるので、コードに変数識別子しかない場合でも、変数の型の定義を見つけられます。[Go To Type Definition] (型定義へ移動) コマンドは、変数または参照の定義に対して実行できます。前のコード スニペットでは、定義内または For ループ内の customers 変数に対して [Go To Type Definition] (型定義へ移動) コマンドを呼び出すと、IEnumerable(Of T) が選択されたオブジェクト ブラウザが表示されます (図 5 を参照)。コードで型が明示的に定義されていれば、代わりにその場所に移動します。

図 5 For ループでの [Go To Type Definition] (型定義へ移動) コマンド
図 5 For ループでの [Go To Type Definition] (型定義へ移動) コマンド

型の推定を有効または無効にする

型の推定をサポートすることによる主な影響の 1 つとして、型の推定が有効なプロジェクトでは、以前のバージョンの Visual Basic でコンパイルされたコードがコンパイルされない場合があります。アップグレードされたプロジェクトで型の推定が既定でオフになるのはそのためです。このようなビルド エラーは、主に遅延バインディングを使用するコードで発生する可能性があります。

以前のバージョンの Visual Basic では、Option Strict がオフのときに遅延バインディングが行われ、明示的な型のない変数は式に初期化されます。この変数には Object 型が指定され、この変数に対するメンバやメソッドの呼び出しは、すべて実行時にバインドされます。

' Late Binding with Visual Basic 2005
Dim x = 5
x = New List(Of Integer)
x.Sort()
x = New Queue(Of Integer)
x.Enqueue(6)

型の推定が有効になっていると、このような呼び出しでコンパイル エラーが発生します。コンパイラは、変数に Object 型を指定する代わりに、その変数が初期化された式と一致する型をその変数に指定します。型の推定が有効であっても遅延バインディングは可能ですが、型として Object を明示的に指定しなければなりません。

' Late Binding with Visual Basic 2008
Dim x As Object = 5
x = New List(Of Integer)
x.Sort()
x = New Queue(Of Integer)
x.Enqueue(6)

型の推定を無効にすることが必要になるケースは他にもあります。さいわいにも、Option Strict や Option Explicit によく似た Option Infer というコンパイラ オプションで、型の推定のオンとオフを切り替えることができます。このオプションは、Visual Basic の他のコンパイラ オプションと同様に、コード ファイルの冒頭で記述します。

Option Infer On    ' This enables type inference for the file

Option Infer Off   ' This disables type inference, so declarations
                   ' with no type specified will use Object

特定のコード ファイルで Option Infer が指定されていない場合は、プロジェクトレベルの設定が使用されます。プロジェクトレベルでの Option Infer の既定の設定は、プロジェクト デザイナの [コンパイル] タブで設定できます。プロジェクト デザイナには、[プロジェクト] メニューまたはソリューション エクスプローラの [My Project] ノードからアクセスできます。

Visual Basic 2008 の新規プロジェクトでは Option Infer が既定でオンになり、アップグレードされたプロジェクトでは、既存のコードを壊さないように Option Infer が既定でオフになります。特定のファイルについては、ファイルレベルの設定でこのオプションがオーバーライドされます。

LINQ または LINQ に関連する他の技術を使用するようになるとわかるように、型の推定を使用することには多大なメリットがあります。その一方で、この機能を使用するかどうかを切り替えることもできるので、ユーザーは柔軟に対応することができます。

ご意見やご質問は、

Bill instinct@microsoft.com.

まで英語でお送りください。
Bill Horst は、Microsoft Visual Basic チームでテストを担当するソフトウェア設計エンジニアです。これまでに LINQ クエリのテストの大部分を担当し、現在は Visual Basic のデバッグもテストしています。


データ ポイント: 共通テーブル式

データ ポイント
共通テーブル式
John Papa

コードのダウンロード : : DataPoints2007_10.exe (150KB)
コンテンツ
ビュー、派生テーブル、および CTE
CTE の構造
CTE について理解する
再帰のルール
再帰に関する補足
まとめ

開発者が携わる多くのプロジェクトでは、基本の SELECT/FROM/WHERE の類のステートメントとは異なる複雑な SQL ステートメントを記述します。たとえば、FROM 句内で派生テーブル (インライン ビューとも呼ばれます) を使用する Transact-SQL (T-SQL) クエリを記述することがあります。この手法では、開発者は行セットを取得して、SELECT ステートメント内で即座に他のテーブル、ビュー、およびユーザー定義関数と結合できます。派生テーブルの代わりにビューを使用することもできます。どちらの方法にもそれぞれの長所と短所があります。

SQL Server™ 2005 を使用する場合、筆者が好むのは共通テーブル式 (CTE) という第 3 の選択肢です。CTE では、パフォーマンスを低下させることなくコードの読みやすさ (ひいては保守性) を高めることができます。また、SQL Server の従来のバージョンと比べて、T-SQL で再帰的なコードを記述するのが非常に簡単になります。

今月のコラムでは、CTE を使用して一般的な開発シナリオに取り組む手法を取り上げます。まず、CTE がどのように動作するかと、どのようなシナリオに適用できるかについて説明します。次に、派生テーブル、ビュー、カスタム プロシージャなどの従来の T-SQL 構造と比較した、CTE を使用する利点に触れます。このコラム内では随時サンプルを示して、どこでどのように利用できるかを解説します。さらに、CTE が再帰ロジックをどのように処理するか、再帰 CTE がどのように動作するかを示します。このコラムで提示するコードはすべて、MSDN® Magazine Web サイトからダウンロードして使用できます。コードでは、SQL Server 2005 に付属する Northwind データベースと AdventureWorks データベースを使用しています。

ビュー、派生テーブル、および CTE

CTE は、クエリで選択する必要があるデータ セットがデータベース内のテーブルとして存在しない場合に便利です。たとえば、集約されたデータ セットに対して、顧客とその注文に基づいて値を計算するクエリを記述するとします。集約されたデータでは、Customers、Orders、Order Details のテーブルを結合し、注文の合計および平均値を計算します。集約された行セットに対してクエリを実行することもあります。1 つの方法は、まず集約データを集めるビューを作成し、そのビューに対するクエリを記述することです。また、派生テーブルを使用して、集約されたデータに対するクエリを実行する方法もあります。これには、SQL ステートメントを FROM 句内に移し、それに対するクエリを実行します。

一般にビューは、大きなクエリを分割して、読みやすい形式でクエリを実行できるようにするために使用されます。たとえば、ビューでは、10 個のテーブルを結合して数十の列を選択し、関連する一連のロジックに基づくフィルタリングで複数の行を除外する SELECT ステートメントを表現できます。このビューに対して、他の SELECT ステートメントによるデータベース全体へのクエリを実行できます。この抽象化により、ビューが表す行セットへのアクセスが容易になります。データを複製したり一時テーブルに保存したりする必要はありません。

また、ビューは権限が許す限り、データベース全体に対して繰り返し利用できます。たとえば、図 1 では、ビューが作成された後、別の T-SQL ステートメントから使用されています。ただし、データを収集して 1 回だけ使用する状況では、ビューは最善の手段ではないこともあります。ビューは、データベース全体に対してすべてのバッチから使用できるデータベース オブジェクトなので、1 つの T-SQL バッチでのみ使用するビューを作成するのは行き過ぎです。

Figure 1 クエリを実行するビュー (Close Figure 1)
CREATE VIEW vwMyView AS
SELECT
    EmployeeID, COUNT(*) AS NumOrders,  MAX(OrderDate) AS MaxDate
FROM Orders
GROUP BY EmployeeID
GO

SELECT
    e.EmployeeID, oe.NumOrders, oe.MaxDate, e.ReportsTo AS ManagerID,
    om.NumOrders, om.MaxDate
FROM
    Employees AS e
    INNER JOIN vwMyView AS oe ON e.EmployeeID = oe.EmployeeID
    INNER JOIN vwMyView AS om ON e.ReportsTo = om.EmployeeID

派生テーブル (インライン ビューとも呼ばれます) を使用するという選択肢もあります。派生テーブルは、SELECT ステートメントをかっこで囲んで FROM 句内に移すだけで作成できます。作成された派生テーブルは、テーブルやビューと同様にクエリを実行したり結合したりできます。図 2 に示すコードは、図 1 が解決するのと同じクエリを解決しますが、ビューの代わりに派生テーブルを使用しています。派生テーブルにアクセスできるのはその派生テーブルが存在するステートメント内のみなので、通常、クエリが読みにくくなり、したがって保守も難しくなります。この問題は、派生テーブルを同じバッチ内で複数回使用する場合にさらに大きくなる可能性があります。派生テーブルを再利用するにはコピーして貼り付けなければならないためです。

Figure 2 派生テーブルを使用するクエリ (Close Figure 2)
SELECT
    e.EmployeeID, oe.NumOrders, oe.MaxDate,  e.ReportsTo AS ManagerID,
    om.NumOrders, om.MaxDate
FROM
    Employees AS e
    INNER JOIN
        (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
         FROM Orders
         GROUP BY EmployeeID) AS oe(EmployeeID, NumOrders, MaxDate)
        ON e.EmployeeID = oe.EmployeeID
    LEFT JOIN
        (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
         FROM Orders
         GROUP BY EmployeeID) AS om(EmployeeID, NumOrders, MaxDate)
        ON e.ReportsTo = om.EmployeeID

CTE では T-SQL が (ビューのように) 読みやすくなり、しかも、同じバッチの直後のクエリで複数回使用できるため、この種のシナリオに適しています。もちろん、そのスコープを超えて使用することはできません。加えて、CTE は言語レベルの構造です。つまり、SQL Server が内部的に一時テーブルや仮想テーブルを作成しません。CTE の基になるクエリは、直後のクエリで参照されるたびに呼び出されます。

したがって、この同じシナリオは CTE を使用すると 図 3 のように記述できます。EmpOrdersCTE が集約データを集めると、CTE の直後のクエリがそのデータを使用します。CTE によって、図 3 のコードは (ビューのように) とても読みやすくなりますが、メタデータを格納するシステム オブジェクトは作成されません。

Figure 3 CTE を使用するクエリ (Close Figure 3)
;WITH EmpOrdersCTE (EmployeeID, NumOrders, MaxDate) AS
(
  SELECT EmployeeID, COUNT(*), MAX(OrderDate)
  FROM Orders
  GROUP BY EmployeeID
)

SELECT
    e.EmployeeID,  oe.NumOrders, oe.MaxDate,
    e.ReportsTo AS ManagerID,  om.NumOrders, om.MaxDate
FROM
    Employees AS e
    INNER JOIN EmpOrdersCTE oe ON e.EmployeeID = oe.EmployeeID
    LEFT JOIN EmpOrdersCTE om ON e.ReportsTo = om.EmployeeID

CTE の構造

CTE を作成する方法を、簡単な CTE の例を挙げて説明します。CTE は WITH キーワードで開始します。ただし、CTE がバッチ内の最初のステートメントでない場合、WITH キーワードの前にセミコロンが必要です。お勧めなのは、CTE の前にいつもセミコロンを付ける方法です。常に付ける方が、セミコロンが必要かどうかを覚えておくよりも簡単です。

CTE の名前の後に WITH キーワードを記述し、続けて列の別名のオプションの一覧を記述します。列の別名は、CTE 内の SELECT ステートメントが返す列に対応します。オプションの列の別名の後に、AS キーワードを指定します (必須)。AS キーワードの後に、CTE を定義するクエリ式をかっこで囲んで記述します。

次の例を見てみましょう。

;WITH myCTE (CustID, Co) AS
(
  SELECT CustomerID, CompanyName FROM Customers
)
SELECT CustID, Co FROM myCTE

CustomerID 列と CompanyName 列には、CustID と Co という別名が設定されています。CTE の直後には、列の別名で CTE を参照する SELECT ステートメントがあります。

CTE について理解する

CTE を設計する前に、CTE がどのように動作し、どのようなルールに従っているかを理解する必要があります。ここでは、CTE が使用できる場所、CTE 内で何が使用できて何が使用できないかについて説明します。まず、CTE は、T-SQL バッチ、ユーザー定義関数、ストアド プロシージャ、トリガ、ビュー内で作成して使用できます。

CTE を参照できるのは、CTE の直後のステートメントのみです。これは、CTE を使用する場合、CTE を参照するクエリは、T-SQL バッチ内で CTE の直後に記述する必要があることを意味します。たとえば、次のバッチはエラーとなります。

;WITH myCTE (CustID, Co) AS
(
  SELECT CustomerID, CompanyName FROM Customers
)
SELECT CompanyName FROM Customers WHERE CustomerID = 'ALFKI'
SELECT CustID, Co FROM myCTE

このコードでは、myCTE は後に続く最初のクエリでのみ使用できます。2 番目のクエリで myCTE を参照すると、CTE はスコープ内にないため、オブジェクト名 myCTE が無効であるという例外がスローされます。

CTE は別のクエリからの参照を目的としていて、参照時にデータが再処理される可能性もあるため、CTE のクエリには ORDER や COMPUTE などのステートメントを指定することはできません。ただし、FOR XML などの複合ステートメントを CTE に対して定義して操作することはできます。たとえば、次に示すように、CTE にクエリを実行し、FOR XML 句で結果を返すことができます。

;WITH myCTE AS
(
  SELECT c.CustomerID, c.CompanyName, o.OrderID, o.OrderDate
  FROM Customers c
  INNER JOIN Orders o ON c.CustomerID = o.CustomerID
)

SELECT CustomerID, CompanyName, OrderID, OrderDate
FROM myCTE FOR XML AUTO

CTE を定義すると、後続の最初のクエリから複数回参照できます。これは、クエリで CTE を 2 回以上参照する必要がある場合に特に便利です。図 3 のコード サンプルは、クエリでどのようにして EmpOrdersCTE を 2 回参照し、従業員とその管理者の情報を取得するかを示しています。同じ行セットを 2 回以上参照する必要がある場合はこの方法が便利です。クエリを 2 回複製するよりも、CTE を 2 回参照する方が簡単です。

CTE は、必ず SELECT ステートメントから使用するわけではありません。CTE が生成する行セットを参照するどのステートメントからも使用できます。つまり、CTE の後には、CTE を使用する SELECT、INSERT、UPDATE または DELETE ステートメントを記述できます。また、CTE を使用するクエリで転送専用のカーソルとスナップショット カーソルを使用できます。

さらに、CTE の後に別の CTE を記述することができます。この方法は、複数の中間結果を 1 つの行セットに集める場合に、ある CTE とは別の CTE を作成するために使用できます。複数の CTE を作成する際には、CTE 定義をコンマで区切ります。

図 4 に示す例では、従業員の一覧と各従業員の注文の合計を収集する EmpOrdersCTE を定義しています。2 番目の MinMaxOrdersCTE という名前の CTE は、最初の EmpOrdersCTE へのクエリを実行し、行セットに集計関数を実行して、従業員の注文数の平均値、最小値、および最大値を求めます。

Figure 4 別の CTE を参照する CTE (Close Figure 4)
;WITH
EmpOrdersCTE (EmployeeID, NumOrders)
AS
(
  SELECT EmployeeID, COUNT(*)
  FROM Orders
  GROUP BY EmployeeID
),
MinMaxOrdersCTE (Mn, Mx, Diff)
AS
(
  SELECT MIN(NumOrders), MAX(NumOrders), AVG(NumOrders)
  FROM EmpOrdersCTE
)
SELECT Mn, Mx,    Diff
FROM MinMaxOrdersCTE

CTE を複数定義するには、WITH キーワードの後にコンマで区切って指定します。各 CTE は、それぞれ別個に作成され、後続の CTE から参照できます。CTE 定義の後に指定されたデータ操作言語 (DML) ステートメントでも、WITH 句内に定義された任意の CTE を参照できます。

再帰のルール

CTE では、再帰アルゴリズムを実装することもできます。再帰ロジックは、自己呼び出しのアルゴリズムを記述する必要がある場合に便利です。これは多くの場合、入れ子にしたデータ セットをスキャンすることによって実現されます。再帰ロジックの記述は、特に T-SQL のような言語では複雑になります。ただし、これは CTE が取り扱うよう設計された特定の問題の 1 つです。再帰 CTE を作成する基本の式は、次のとおりです。

  1. 最上位レベルを返すクエリを作成します (これはアンカー メンバです)。
  2. 再帰クエリを記述します (これは、再帰メンバです)。
  3. 最初のクエリと再帰クエリを UNION で結合します。
  4. 行が 1 つも返されない場合があることを確認します (これは、終了チェックです)。

再帰 CTE は、次のようになります。

;WITH myRecursiveCTE(col1, col2, ... coln) AS
(
  -- Anchor Member Query
  UNION ALL
  -- Recursive Member Query that references myRecursiveCTE
)

CTE を含まないカスタム再帰プロシージャを記述する場合は、明示的な終了句を指定する必要があります。終了句は、再帰アルゴリズムが最終的に終了して、再帰のコール スタックをさかのぼって戻ることを確認する役割を担います。この句がないと、コードは無限ループに陥る可能性があります。

ただし、CTE には、終了句の処理を支援する 2 つの側面があります。1 つ目は、再帰メンバがレコードを返さない場合に発生する暗黙的な終了句です。この条件が発生すると、再帰メンバ クエリは CTE を再帰的に呼び出す代わりにコール スタックをさかのぼって戻ります。もう 1 つは、MAXRECURSION レベルを明示的に設定する方法です。

MAXRECURSION レベルは、CTE を含むバッチ内で、またはサーバー側設定によって明示的に設定できます (サーバー側設定の既定値は、変更しない限り 100 です)。この設定は、CTE が再帰的に自己呼び出しする回数を制限します。制限値に達すると、例外がスローされます。MAXRECURSION レベルを設定する構文は、CTE に続く SELECT ステートメント内で OPTION 句を使用し、次のようになります。

-- DEFINE YOUR CTE HERE
SELECT * FROM EmpCTE
OPTION (MAXRECURSION 7)

再帰 CTE の設計時に注意が必要なルールは他にもいくつかあります。再帰 CTE は、アンカー メンバと再帰メンバの両方を含んでいる必要があります。いずれのメンバも列数が同じで、両方のメンバに属する列はデータ型が一致している必要があります。再帰メンバでは CTE を 1 回のみ参照できます。いずれのメンバも次の句またはキーワードを使用できません。

  • SELECT DISTINCT
  • GROUP BY
  • HAVING
  • TOP
  • LEFT/RIGHT OUTER JOIN

再帰に関する補足

データおよび行セットにおいて、同じデータ セットに対して異なる条件で同じロジックを繰り返し実行する必要がある場合、再帰を使用して問題を解決します。 たとえば、すべての営業担当者とその上司を洗い出し、階層順にデータを返すとします。図 5 は、再帰によって営業部長の配下の従業員一覧を取りまとめる CTE を使用した解決法を示しています。

Figure 5 営業担当者の再帰的な集計 (Close Figure 5)
;WITH EmpCTE(EmployeeID, EmployeeFirstName,
 EmployeeLastName, MgrID, SalesLevel)
AS
(
  -- Anchor Member
  SELECT EmployeeID, FirstName, LastName, ReportsTo, 0
  FROM Employees
  WHERE EmployeeID = 2 -- Start with the VP of Sales

  UNION ALL

  -- Recursive Member
  SELECT
    e.EmployeeID, e.FirstName, e.LastName, e.ReportsTo, m.SalesLevel+1
  FROM
    Employees AS e
    INNER JOIN EmpCTE m ON e.ReportsTo = m.EmployeeID
)

 -- Using the CTE
SELECT EmployeeID, EmployeeFirstName, EmployeeLastName,
       MgrID, SalesLevel
FROM EmpCTE

いくつかの追加点を除き、図 5 に示す再帰 CTE は標準的な CTE とほとんど変わりません。標準的な CTE には行セットを定義するクエリが記述されますが、再帰 CTE では 2 つのクエリ定義が記述されます。最初のクエリ定義はアンカー メンバで、CTE が呼び出されるときに実行されるクエリを定義します。2 番目のクエリ定義は再帰メンバで、アンカー メンバと同じ列およびデータ型を返すクエリを定義します。再帰メンバはまた、再帰的に CTE を再呼び出しするために使用される値を取得します。クエリの結果は、UNION ステートメントによって結合されます。

図 5 の EmpCTE は、営業部長 (EmployeeID = 2) の従業員レコードを取得するアンカー メンバを示しています。アンカー メンバのクエリの最後の列は、値 0 を返します。これは、階層の 0 番目のレベル、つまり最上位を表します。再帰メンバのクエリは、前の従業員の配下にいる従業員の一覧を取得します。これは、Employees テーブルを EmpCTE に結合することで実行できます。

同じ列が再帰メンバから取得されますが、SalesLevel 列は、現在の従業員の管理者を取得し、管理者の SalesLevel を取得し、1 を加えることによって計算されます。式 m.SalesLevel+1 は、(アンカー メンバから取得された) 営業部長直属のすべての従業員に 1 という SalesLevel を割り当てます。部長直属の従業員の部下である任意の従業員は、SalesLevel が 2 となります。同様に、営業組織の階層のレベルが下がるごとに SalesLevel が 1 ずつ増えます。

まとめ

CTE によって、クエリ内で複雑な派生テーブルを使用するシナリオや、T-SQL バッチの外部に定義があるビューを参照するシナリオと比べて、T-SQL の記述が読みやすくなります。また、CTE によって、再帰アルゴリズムにかかわる問題の対応が容易になります。再帰的であるかどうかにかかわらず、CTE を使用することによって、多くの一般的な開発シナリオに対応でき、パフォーマンスを低下させることなく読みやすさを向上させることができます。

ご意見やご質問は、

John mmdata@microsoft.com.

まで英語でお送りください。
John Papa は、ASPSOFT (aspsoft.com) で働く .NET シニア コンサルタントです。野球を熱狂的に愛し、夏の夜のほとんどを家族および忠犬 Kadi (カーディ) と共にヤンキースの応援に費やします。C# MVP でもある John は、ADO、XML、および SQL Server に関する何冊かの書籍の執筆を担当しました。また、主に業界のカンファレンス (VSLive など) での講演やブログ (codebetter.com/blogs/john.papa) (英語) などで活躍しています。


Cutting Edge: AJAX アプリケーション アーキテクチャ、第 2 部

Cutting Edge
AJAX アプリケーション アーキテクチャ、第 2 部
Dino Esposito

コードのダウンロード : : CuttingEdge2007_10.exe (151KB)
コンテンツ
AJAX を使用してログインする
サービスの特色
AJAX サービスの特長
JSON と XML
セキュリティの問題
ASP.NET AJAX のサービス
ASP.NET AJAX サービスと SOAP
AJAX サービスを呼び出す
ユーザー インターフェイスを作成する
まとめ

ASP.NET AJAX Extensions を使用してサイトを AJAX 環境に移行する場合、部分的なレンダリングまたはスクリプト サービスのいずれかの主要なプログラミング モデルを選択します。先月のコラムでは、主にアーキテクチャの観点から部分的なレンダリングについて説明しました。簡潔に言うと、部分的なレンダリングでは、ASP.NET アプリケーションの基礎となるアーキテクチャを変更する必要はありません。サイト内のページに対するちらつきのない更新など、AJAX の最適な要素のいくつかを簡単に実装できます。この拡張動作は、単にいくつかの新しいサーバー コントロール (具体的には ScriptManager と UpdatePanel) を追加し、XMLHttpRequest オブジェクトを使用して実行される非同期要求で従来のポストバックを変換する処理がこのコントロールによって暗黙的に実行されるようにするだけで実現できます。このアプローチは、単に AJAX 機能を既存の Web 開発モデルに適用するだけなので非常に簡単です。

AJAX アプリケーションの構築における完全なパラダイム シフトに対応できる場合は、スクリプト サービス アプローチを検討してください。概して、一般的な AJAX アーキテクチャは非常に理解しやすいものです。図 1 は、そのしくみの概要を示しています。バックエンドはアプリケーション固有のサービスで構成されます。これらのサービスは通常、ビジネス ロジックが存在し動作しているシステムの中間層の上にあり、AJAX のスクリプトから呼び出し可能なファサードです。サービスとフロントエンドは、さまざまな形式を使用して HTTP 経由でデータを交換し、パラメータを渡して値を返します。クライアントで実行される JavaScript コードで構成されるフロントエンドは、データを受信して処理したら、HTML および JavaScript を使用してグラフィカル UI を作成する重要なタスクを実行します。JavaScript への依存はブラウザの構造上の制約で、これは、ブラウザがより強力なプログラミング機能をサポートできるようになったときにだけ変更されます。

図 1 一般的な AJAX アーキテクチャ
図 1 一般的な AJAX アーキテクチャ

AJAX を使用してログインする

従来の ASP.NET モデルから移行すると、実際的な影響が数多く生じます。ログイン プロセスについて考えてみます。このプロセスが純粋な AJAX ソリューションでどのように変更されるかを確認してみましょう。

現在の ASP.NET では、ログイン プロセスを有効にするには、Login コントロールでログイン ページを構成し、保護されたページを LoginView コントロールで構成して、ASP.NET メンバシップ プロバイダを設定します。保護されたページは、ログイン テンプレートまたは匿名テンプレートを使用して、認証プロセスの結果をグラフィカルに反映します。ASP.NET 2.0 では、コードを記述せずにこの作業の大部分を実行できます。ただし、ログインが正常に行われるようにするために、ユーザーが保護されたページにアクセスするとリダイレクト (HTTP 302) が行われ、ログイン ページからのポストバックが発生して資格情報が認証されると、最初に要求されたページに再度リダイレクトされます。

AJAX ページの場合、必ずしもそうとは限りません。ユーザーがアドレス バーから保護されたページを要求した場合、ASP.NET でのプロセスを繰り返す以外にできることはあまりありません。ただし、保護されたページを参照するリンクをページに配置している場合、リンクの onclick イベントにスクリプトを追加して、ユーザーが認証されているかどうかを確認できます。認証されていない場合、次のような警告メッセージをポップアップ表示できます。

function checkFirst()
{
    var loggedIn = Sys.Services.AuthenticationService.get_isLoggedIn();
    if (!loggedIn)
    {
        alert("You must be logged in to follow this link");
        return false;
    }
    return true;
}

ここからわかるように、このアプローチでは、特に現在のユーザーがログインしているかどうかを確認できるクライアント フレームワークを使用します。ASP.NET AJAX Extensions では、Sys.Services.AuthenticationService クラスを使用してこのプロセスを実行します。

同じサービスを使用して、ユーザーが有効な資格情報を指定した後にユーザーを認証することができます。この場合、リダイレクトもポストバックも発生しません。次のコードが示すように、すべてが同じページのコンテキストで発生します。

function OnLogin()
{
   Sys.Services.AuthenticationService.login(
      $get("UserName").value,
      $get("Password").value,
      false,
      null,
      null,
      OnLoginCompleted,
      OnLoginFailed);
}

資格情報の確認は、認証するスクリプト サービスを使用する標準のスクリプト処理になります。クライアント ページの UI を更新し、認証の成功後にユーザーがログインしていることを反映するのは開発者です。使用するフレームワークに HTML をコントロールにラップするオブジェクト モデルが用意されていない場合、ドキュメント オブジェクト モデル (DOM) スクリプトを使用してかなり遠回りする必要があります。ASP.NET AJAX Extensions には現在、マークアップ チャンクをコントロールとして表すオブジェクト モデルは用意されていません。

サービスの特色

AJAX におけるサービスは、アプリケーションのドメイン上に存在してクライアント スクリプト コードに機能を公開するコードを示します。AJAX で使用されるサービスでは、実際のアプリケーションのバックエンドと中間層をエンド ユーザーが直接操作できないようにする実装を作成する設計が必要です。このようなサービスは WS-* Web サービスでしょうか。サービス指向アーキテクチャ (SOA) サービスと言えるでしょうか。

AJAX アプリケーションの理想的なサービスは、主にデータとリソースを Web クライアントに公開することに関係しています。サービスには HTTP 経由でアクセスでき、クライアントは URL (オプションで HTTP ヘッダー) を使用してデータおよびコマンド操作にアクセスする必要があります。クライアントは、GET、POST、PUT、DELETE などの HTTP 動詞を使用してサービスと対話します。つまり、URL はリソースを表し、HTTP 動詞はリソースに対して実行するアクションを表します。このような対話で交換されるデータは、JSON やプレーンな XML などの簡単な形式および RSS や ATOM などのシンジケーション形式で表されます。

このような特性を持つサービスを Representational State Transfer (REST) サービスと呼びます。REST の定義の詳細については、REST の展望について説明している論文 (ics.uci.edu/~fielding/pubs/dissertation/top.htm) を参照してください。

結局、AJAX アプリケーションで使用されるサービスは SOAP を使用して通信しない傾向があり、必ずしも SOA の意味での自律的なサービスであるわけではありません。代わりに、このサービスをホストするプラットフォームおよびドメインにバインドされます。これに基づき、このサービスを WS-* Web サービスまたは SOA サービスと呼ぶことはまずありません。

また、WS-* Web サービスの Web Services Description Language (WSDL) などとは異なり、このサービスには公開ドキュメントまたは検出スキーマはありません。そのため、サービスへの依存関係が減少し、サービス コードがより速く進化します。全体として見ると、AJAX サービスに推奨されるパラダイムは Web サービスと SOA サービスのパラダイムほど大がかりではありませんが、使用するコンテキストでは有効です。

このコラムの残りの部分では、スクリプト サービス アプローチを使用して AJAX アプリケーションのバックエンドを実装するために使用されるサービスを示す際に、"AJAX サービス" という表現を使用します。

AJAX サービスの特長

AJAX サービスは HTTP のみを使用して公開されるので、ほとんどすべてのテキスト形式を使用して要求および応答の本文をパッケージ化することができます。最も一般的に使用される形式は JavaScript Object Notation (JSON) ですが、プレーンな XML や生のテキストなどの他の形式も使用できます。

JSON は、アプリケーションの複数の層間でオブジェクトの状態を移行するように設計されたテキストベースの形式です。JSON 文字列は、おなじみの eval 関数を使用して JavaScript オブジェクトに簡単に評価できます。JSON 形式では、次に示すようにオブジェクトが記述されます。

{"ID":"ALFKI", "Company":"Alfred Futterkiste"}

この文字列は、ID と Company の 2 つのプロパティおよびそれぞれのテキストにシリアル化された値が格納されたオブジェクトを示します。プロパティにカスタム オブジェクトなどの非プリミティブ値が割り当てられると、その値は次のように再帰的に JSON にシリアル化されます。

 "ID":"ALFKI",
 "Company":"Alfreds Futterkiste",
 "Location":
      "{"City":"Berlin", "Country":"Germany"}",
}

JSON 文字列は、eval 関数で処理されると、各エントリに名前と値が含まれる連想配列 (名前と値の一種のコレクション) になります。JSON 文字列が Customer などのカスタム オブジェクトの状態を表すことを目的としている場合、対応するクラスの定義をクライアントで使用できるようにするのは開発者の役割です。つまり、JavaScript の eval 関数では、JSON 文字列から汎用コンテナに情報が抽出されるだけです。この情報を Customer オブジェクトなどのカスタム オブジェクトとして公開する必要がある場合、クラス定義を指定してデータをその定義に読み込むタスクは、すべて開発者または使用するフレームワークが実行します。JSON の構文および目的の詳細については、www.json.org を参照してください。

JSON と XML

長年にわたって、XML が Web の共通言語として推奨されてきました。ご存知のとおり AJAX が Web を変えつつあるので、データ表現に関する限り、JSON が支持され XML が隅に追いやられつつあります。

JSON の方がやや簡単で、JavaScript 言語での使用に適しています。人間が理解しやすいのはどちらかという点に関しては議論の余地がありますが、Web ブラウザによる処理は確実に XML よりも JSON の方が簡単です。JSON では、XML パーサーのようなものは不要です。テキストの解析に必要なものはすべて JavaScript 言語に組み込まれています。JSON は XML ほど大がかりではないので、より簡潔です。JSON が完璧だというわけではありません。JSON では膨大な数のコンマと引用符が必要になるので、むしろ特異な形式と言えます。

JSON では、主要なアーキテクチャの利点を比較的低コストで得ることもできます。オブジェクトの定義はどこででも同じように判断できます。サーバーでは、エンティティを定義して好きなマネージ言語のクラスとして実装します。サービス メソッドが任意のクラスのインスタンスを返す必要があるとき、オブジェクトの状態が JSON にシリアル化され、送信されます。クライアントでは、JSON 文字列が受信および処理され、その内容が配列 (サーバー クラスと同じインターフェイスを持つ一種のミラー JavaScript オブジェクト) に読み込まれます。クラスのインターフェイスは JSON ストリームから推測されます。このようにして、サービスとクライアント ページ コードの両方でエンティティの同じ論理定義が使用されます。

純粋に技術的な観点では、厳密には AJAX サービスで JSON をデータ表現形式として実装する必要はありません。XML でも同じ結果が得られますが、この場合、JavaScript から使用できる XML パーサーが必要になります。JavaScript で簡単な XML テキストを解析するのであれば問題にならないでしょうが、本格的なパーサーを使用する場合は事情が異なります。パフォーマンスおよび機能関連の問題のため、共通点がほとんどない一見同様のコンポーネントが数多く存在することになる可能性があります。また、JavaScript XML パーサーが名前空間、スキーマ、空白、コメント、処理命令などをサポートするかどうかという問題もあります。

私の考えでは、最終的には互換性のためにノードおよび属性に制限された XML のサブセットを使用することになるでしょう。その時点では、XML の山かっこと JSON の中かっこのどちらかを選択するだけになります。

セキュリティの問題

AJAX では、より動的で対話的な閲覧が実現できます。ただし、これにより、クロスサイト スクリプティング (XSS) やクロスサイト リクエスト フォージェリ (CSRF) などの一般的な攻撃の対象領域が増加します。このような攻撃は、一般的に攻撃者が URL 経由で Web ページにスクリプト コードを挿入することで発生します。これにより、攻撃者は Web ブラウザを制御できるようになり、ユーザー名とパスワードを盗んだりユーザーの知らない間に HTTP 要求を実行したりします。

たとえば、攻撃者は動的に作成された <script> タグを使用して悪意のあるスクリプトをクライアントに挿入し、データを攻撃者の Web サイトにインポートできるようにする可能性があります。CSRF 攻撃の場合、攻撃者はスクリプトをクライアントに挿入し、クライアントに保存された認証情報 (Cookie など) を使用して別の Web サイトで無許可のサービス メソッドを実行できるようにする可能性があります。

JSON は、データをパッケージ化してクライアントに配信する優れた手段です。ただし、JSON は JavaScript 言語の安全なサブセットと見なされるので (割り当て操作と呼び出し操作を除きます)、多くの AJAX アプリケーションでは、JavaScript オブジェクトを作成するために、単に JSON 文字列が JavaScript の eval 関数に直接渡されます。その結果、JSON 文字列が正しくない場合、クライアントで無許可のスクリプトを実行する新しい手段を攻撃者に与えることになります。

このような攻撃のほとんどは、HTTP 要求での GET 動詞の悪用に依存します。さいわいなことに、ASP.NET AJAX サービスでは、リモート サービスに対する GET 動詞が既定で無効になっています。また、要求で特殊なコンテンツ タイプを設定するように求めることができます。設定されない場合、ASP.NET AJAX サービスによって呼び出しが拒否されます。仕様では、<script> タグから間違ったコンテンツ タイプの ASP.NET AJAX サービスに直接呼び出しを行った場合、呼び出しは失敗します。

ASP.NET AJAX のサービス

ASP.NET AJAX Extensions でスクリプト サービスを実装する方法には、特殊な ASP.NET Web サービスを使用する方法とページ メソッドを使用する方法の 2 とおりがあります。前者は、ASMX リソースにリンクされたクラスを設計および構築するだけです。

<%@ WebService Language="C#"
    CodeBehind="~/App_Code/TimeService.cs"
    Class="IntroAjax.WebServices.TimeService" %>

このクラスは、オプションで WebService クラスを継承することもできます。また、新しい ScriptService 属性で修飾されている必要があります。

[ScriptService]
public class TimeService : System.Web.Services.WebService
{
   ...
}

スクリプトから呼び出し可能な各メソッドがパブリックとして宣言され、通常の WebMethod 属性のタグが付けられます。

ページ メソッドは単なるパブリックな静的メソッドで、それぞれが WebMethod としてマークされ、単一の ASP.NET ページのコンテキストで定義されます。このメソッドは、ホスト ページのみから呼び出せます。保存方法は異なりますが、Web サービスまたはページ メソッドの呼び出しは ASP.NET AJAX 環境によって同じ方法で処理されます。

AJAX サービスがバックエンドの一部を表すことを理解しておく必要があります。このサービスは、WSDL スクリプトで完全に文書化され、SOAP データを送信する POST コマンドを使用してアクセスされる WS-* Web サービスという意味のパブリック Web サービスではありません。AJAX サービスはまさにローカル サービスであり、通常は呼び出し元となるアプリケーションで定義されます。ただし、同じドメイン内に存在する場合は別の Web アプリケーションまたは Web サイトにも配置できます。

ScriptService 属性は、ASP.NET AJAX ランタイムがサービスの呼び出しを受け入れることができるようにする重要な役割を果たします。ScriptService 属性がない場合、呼び出しを試みるとサーバーで例外がスローされます。図 2 に、属性のフラグが付けられていないサービスに AJAX ページがリンクしたときに返されるメッセージを示します。図に示されているページはユーザーには表示されないということに注意してください。そのようなサービスを使用するように宣言している ASP.NET AJAX ページに開発者が接続したときに、そのすべてのマークアップが表示されます。属性が指定されていない ASP.NET Web サービスへのスクリプト呼び出しの処理が内部 ASP.NET メカニズムによって拒否されるので、マークアップによって HTTP 500 エラー コードが返されます。

図 2 スクリプトを実行できないサービスを参照するページ
図 2 スクリプトを実行できないサービスを参照するページ(画像を縮小するには、ここをクリックします)
図 2 スクリプトを実行できないサービスを参照するページ
図 2 スクリプトを実行できないサービスを参照するページ (画像を拡大するには、ここをクリックします)

既定では、AJAX サービス メソッドは POST 動詞のみを使用して呼び出され、JSON 形式のデータを返します。ただし、個々のサービス メソッドに対してオプションの ScriptMethod 属性を使用することによって、これらの設定を変更できます。図 3 に、ScriptMethod 属性でサポートされるパラメータの詳細を示します。クライアントに返されるデータは XML に変更でき、GET 要求のサポートを追加することもできます。ただし、既に述べたように、この明らかに無害な変更のために攻撃者が新しい可能性を得ることになり、メソッドのクロスサイト呼び出しが行われる可能性が高まる場合があります。次のコードは、Web サービス メソッドの定義を示しています。

Figure 3 ScriptMethod 属性のパラメータ (Close Figure 3)

パラメータ | 説明
ResponseFormat | 応答を JSON としてシリアル化するか XML としてシリアル化するかを指定します。既定は JSON ですが、メソッドの戻り値が XmlDocument の場合は XML 形式が便利です。
UseHttpGet | HTTP GET 動詞を使用して Web サービス メソッドを呼び出せるようにするかどうかを指定します。セキュリティ上の理由から、既定ではこのパラメータは false に設定されています。
XmlSerializeString | 文字列などのすべての戻り値の型を XML としてシリアル化するかどうかを指定します。既定値は false です。応答形式が JSON に設定されている場合、このプロパティの値は無視されます。

[WebMethod]
[ScriptMethod]
public DateTime GetTime()
{
   ...
}

既定の設定 (ほとんどの場合はこの設定を推奨) を変更しない場合、ScriptMethod 属性は省略できます。ただし、WebMethod 属性は必須です。

ASP.NET AJAX サービスと SOAP

作成した AJAX Web サービスは、ASMX リソースとして公開されます。既定では、これはパブリック URL で、AJAX クライアントで使用でき、SOAP クライアントおよびツールで検出および使用できます。ただし、SOAP クライアントおよびツールを完全に無効にできます。サービスをホストする ASP.NET アプリケーションの web.config ファイルに次の構成設定を入力するだけで済みます。

<webServices>
   <protocols>
      <clear />
   </protocols>
</webServices>

この簡単な設定を行うと、ASP.NET 2.0 Web サービスに対して定義されているすべてのプロトコル (具体的には SOAP) が無効になり、サービスは JSON 要求のみに応答できるようになります。この設定を有効にすると、簡単なテストのためにブラウザのアドレス バーから Web サービスを呼び出すことができなくなるので注意してください。同様に、?wsdl サフィックスを URL に追加して WSDL を要求することもできません。

Web アプリケーションで ASP.NET AJAX を有効にするには、web.config ファイルに次の設定を追加する必要があります。

<httpHandlers>
  <remove verb="*" path="*.asmx"/>
  <add verb="*" path="*.asmx" validate="false"
       type="System.Web.Script.ScriptHandlerFactory, System.Web.Extensions" />
</httpHandlers>

<remove> ノードによって、ASMX リソースに対応する既定の HTTP ハンドラ (SOAP 経由で要求を処理するハンドラ) が切断されます。<add> ノードによって、新しい HTTP ハンドラが追加されます。このハンドラは、基本的に受信した各 ASMX 要求のコンテンツ タイプをチェックし、コンテンツ タイプ ヘッダーが application/json に設定されている場合に JSON を使用して要求を処理します。application/json に設定されていない場合、HTTP ハンドラは要求が SOAP ベースであると見なし、その要求を標準の ASP.NET 2.0 Web サービス ハンドラに転送します。SOAP プロトコルが無効になっている場合、要求は拒否されます。

結局、表面的な状況とは異なり、ASP.NET AJAX サービスには必ずしも SOAP がかかわっているわけではありません。ただし、web.config ファイルで明示的に無効にしない限り、SOAP クライアントのサポートは保証されます。

ASP.NET AJAX サービスを期待どおりに動作させるには、受信した要求のコンテンツ タイプ HTTP ヘッダーが application/json に設定されている必要があります。これは、<script> タグを使用して実行されるクロスサイト攻撃への有効な対策にもなります。

AJAX サービスを呼び出す

AJAX サービスを呼び出すには、AJAX クライアントは、Windows および従来の ASP.NET アプリケーションで Web サービスを参照する場合と同じパターンに従います。プロキシ クラスによって、リモート サービスと同じインターフェイスがローカルに提供されます。ASP.NET AJAX アプリケーションでは、このプロキシは、ページのダウンロード時にランタイムによって生成される JavaScript クラスです。

JavaScript プロキシ クラスの名前はスクリプト サービスと同じで、このクラスには多数のプロパティが追加されています。同じメソッドのセットが採用されていますが、少し拡張されたシグネチャが使用されます。通常、プロキシ クラスのソース コードの内容を確認する必要はありません。クラスの構造を確認する場合は、ブラウザのアドレス バーから次の URL を呼び出します。

http://.../service.asmx/js

ブラウザで、後で確認するためにローカル ディスクに保存できる JavaScript ファイルがダウンロードされます。

JavaScript プロキシ クラスは、Sys.Net.WebServiceProxy という基本クラスを継承します。このクラスには、JSON 呼び出しを行うための基本機能があります。このコラムのコード サンプルには、次のインターフェイスを持つ Web サービスのプロキシ クラスが用意されています。

interface ITimeService
{
    DateTime GetTime();
    string GetTimeFormat(string format);
}

JavaScript プロキシ クラスには、図 4 に示すプロパティがあります。ミラー化された各メソッドには、通常の引数のセットに加えて 3 つのパラメータがあります。これらのパラメータは、メソッドが成功した場合に呼び出されるコールバック関数、メソッドが失敗したかタイムアウトした場合に呼び出されるコールバック関数、および両方のコールバックに渡されるコンテキスト オブジェクトです。図 4 に示す 3 つの既定の関連プロパティを使用すると、複数の呼び出しで同じ関数を再利用できます。たとえば、1 つの JavaScript 関数を使用して複数のエラーを処理できます。ASP.NET AJAX ページからリモート AJAX サービスを呼び出すサンプル コードを次に示します。

Figure 4 JavaScript プロキシ クラスのプロパティ (Close Figure 4)

プロパティ | 説明
Path | 基になる Web サービスの URL を示します。
Timeout | タイムアウトが呼び出されるまでにメソッドを実行できる時間 (ミリ秒) を示します。
defaultSucceededCallback | 呼び出しの成功時に呼び出される既定の JavaScript コールバック関数を示します。
defaultFailedCallback | 呼び出しの失敗時またはタイムアウト時に呼び出される既定の JavaScript コールバック関数を示します (存在する場合)。
defaultUserContext | 成功と失敗のコールバックに渡される既定の JavaScript オブジェクトを示します (存在する場合)。

function getTime()
{
    IntroAjax.WebServices.TimeService.GetTimeFormat(
        "ddd, dd MMMM yyyy [hh:mm:ss]", onMethodComplete);
}

function onMethodComplete(results)
{
    $get("Label1").innerHTML = results;
}

結果に関係なくメソッドの呼び出しの終了時に呼び出されるコールバックのプロトタイプは、次のとおりです。

function method(results, context, methodName)

context パラメータは、呼び出しが行われたときに指定されたコンテキスト オブジェクトを表します。methodName パラメータは、サービス メソッドの名前に設定される文字列です。最後に、呼び出しの成功時に呼び出されるコールバックの場合、results パラメータは JavaScript バージョンのメソッドの戻り値を格納するオブジェクトです。失敗時のコールバックの場合、このパラメータは Sys.Net.WebServiceError オブジェクトを表します。

ユーザー インターフェイスを作成する

AJAX は、広義では連続感、ちらつきのない更新、インターフェイス機能、マッシュアップ、ライブ データなどのユーザー エクスペリエンスのためのものです。ただし、利用できるのはブラウザとそのプログラミング機能 (主にブラウザのオブジェクト モデル、DOM 実装、DHTML の拡張機能のサポート、CSS、JavaScript、およびプラグイン) だけです。

JavaScript は、UI を作成および操作するための主要なツールです。ユーザー インターフェイス タスクの一般的なパターンでは、クライアントで JavaScript を使用してリモート サービスを呼び出し、JSON または XML データを受信し、ページを再編成して変更を表示する必要があります。

このような簡単なモデルは、実際のアプリケーションのサイズと複雑さを考慮すると、必ずしも有効ではありません。リモート呼び出しの後にページを再編成して新しいデータを組み込むと、UI の構造が高度になるにつれて重大な問題が生じるようになります。ポイントは、UI がどの程度高度になると問題になるかということです。

基本的に、実際の各アプリケーション (特に基幹業務アプリケーション) が依存するユーザー インターフェイス機能には、レイアウト、データ バインド、およびスタイル設定の 3 つがあります。JavaScript 環境では、CSS を使用して一部のスタイル設定がサポートされることを除き、これらの機能はどれもサポートされません。また、JavaScript はインタープリタ言語です。たとえば、インタープリタ言語で集中的なプログラミングを行うと、多くの場合、些細なブラウザのバグや不適切なプログラミングのためにメモリの問題が生じます。

比較的簡単で一般的なシナリオであるデータのグリッドのページングについて考えてみましょう。グリッドは、ブラウザが要求ごとに解析およびレンダリングする必要がある比較的複雑なテーブルです。初めてページが表示されるとき、レンダリング操作はページのダウンロード全体に組み込まれているので、ユーザーがこの操作のコストを測定するのは困難です。ただし、新しいグリッド ページに対して AJAX ポストバック要求を実行すると、ブラウザのウィンドウの更新コストがすぐにわかります。ブラウザはオブジェクトのコレクションを含む JSON 文字列を受信し、そのコレクションを新しいテーブルに変える必要があります。大きな HTML 文字列はクライアントで作成し、メモリ バッファでテキストを構成する必要があります。適切な場合、同じ文字列をグラフィカルにレンダリングする必要があります。

このような集中的な操作を実行すると、応答時間が遅くなります。このようなシナリオで、有効なソリューションを作成するには、JavaScript と DHTML の高度なスキルが必要です。

他に、サーバーで一部のマークアップを事前に生成するという方法もあります。この方法では、リモート サービスはデータを返すだけでなく、マークアップ情報も組み込みます。サーバーでは、コンパイル済みコードを使用してマークアップが作成されます。より簡単にマークアップをデバッグおよびテストでき、アクセシビリティ機能を追加するためのより強力なプログラミング ツールにアクセスできます。その一方で、送受信データ量が増加します (増加はしますが、データ量は通常の ASP.NET ポストバックを使用する場合よりもはるかに少なくなります)。

結論として、実際のユーザー インターフェイスを効果的なものにするには、パフォーマンスと開発の両方の観点から、クライアントに強力なツールが必要です。適切で完全なウィジェットとコントロールのライブラリは開発者にとって不可欠です。ただし、最適なライブラリでも、JavaScript などのインタープリタ言語に特有の制限に対抗できるわけではありません。おそらく、Windows® Presentation Foundation (WPF) フレームワークのサブセットを組み込むマイクロソフトのクロスプラットフォーム ブラウザ プラグインである Silverlight によって、Web 開発者が次世代の Web および AJAX アプリケーションに求めるクライアント側の環境が最終的に実現されるでしょう。ただし、Silverlight は外部プラグインなので別途ダウンロードする必要があり、まだ発展途中です。バージョン 1.0 がリリースされたばかりですが、実際の Web アプリケーションのプレゼンテーション層の構築に使用するには、より高度な機能が必要です。

まとめ

部分的なレンダリングは、AJAX に対する最も簡単なアプローチです。再設計するための時間、予算、またはその必要がない従来のアプリケーションに AJAX 機能を追加する場合に特に適しています。アーキテクチャの観点では、部分的なレンダリングを使用すると、同じアプリケーション モデルおよび基礎となるエンジンを保持したまま現在の ASP.NET を簡単に拡張できます。

純粋な AJAX アーキテクチャは、緩く結合されているクライアントとサーバーに基づいています。基本的に、2 つの個別の環境は、メッセージを交換する JSON を使用して HTTP 経由で接続されます。純粋な AJAX アーキテクチャには、サービスベースのバックエンドと JavaScript で稼動するフロントエンドがあります。効果的な HTML UI は、開発者が作成するか、コントロール ライブラリを選択して作成する必要があります。ただし、この分離によって Silverlight などのテクノロジが登場し、Web 開発者は、サーバー プラットフォームの制約を受けることなくより対話的なユーザー インターフェイスを作成できるようになります。

ご意見やご感想は、

Dino cutting@microsoft.com.

まで英語でお送りください。
Dino Esposito は、Solid Quality Learning の指導者であり、近々発表される『Programming ASP.NET 3.5 Core Reference』(Microsoft Press、2007 年) の著者でもあります。Dino はイタリアに在住し、世界各地で開催される業界のイベントで頻繁に講演しています。weblogs.asp.net/despos でブログを公開しています。


テストの実行: Windows XP Embedded におけるテスト自動化

テストの実行
Windows XP Embedded におけるテスト自動化
Dr. James McCaffrey and Mike Hall

コードのダウンロード : : TestRun2007_10.exe (1402KB)
コンテンツ
テスト用アプリケーション
テスト自動化
UI テスト自動化を Windows XP Embedded システムに移植する
コンポーネントを作成する
OS コンポーネント
イメージを作成する
イメージを展開する
簡単な要約

Windows® XP Embedded オペレーティング システムはもうご使用になりましたか。これは基本的に、開発者によってカスタマイズされた、Windows XP オペレーティング システムの縮小バージョンで、ゲーム コンソール、情報キオスク、ネットワーク アプライアンスなど、PC に似たデバイスでの使用が意図されています。モバイル デバイスや組み込みデバイスでの使用が急速に進むにつれて、このプラットフォームの重要性は高まりつつあります。

これまでにも述べてきましたが、Windows XP Embedded ベースのソフトウェア システムに携わるソフトウェア テスト エンジニアと開発者の間には知識の差が存在するようです。この差を埋めるため、今月の私のコラムでは、Microsoft® Mobile and Embedded Devices グループのシニア製品マネージャであり、Windows XP Embedded の専門家でもある Mike Hall にも協力してもらいました。今回は、Windows XP Embedded 上で稼働するソフトウェア システム用に、軽量かつ強力な UI テスト自動化を作成する方法について解説していきます。

最終目標をわかりやすく示すため、スクリーンショットを用意しました。図 1 は、軽量な UI テスト自動化が実行されるようすを示しています。しかし、Windows XP Embedded を実行しているデバイスでテスト自動化が実行されていることは、スクリーンショットからすぐにはわかりません。

図 1 Windows XP Embedded における UI テスト自動化
図 1 Windows XP Embedded における UI テスト自動化(画像を縮小するには、ここをクリックします)
図 1 Windows XP Embedded における UI テスト自動化
図 1 Windows XP Embedded における UI テスト自動化 (画像を拡大するには、ここをクリックします)

図 1 の UI テスト自動化をよく見ると、Windows XP Embedded を実行するキオスク デバイスで使用されるような、単純かつ典型的な Windows フォームベース アプリケーションが実行されていることがわかります。このテスト自動化は、以下を行うコンソール アプリケーションです。

  • テスト用のフォームベース アプリケーションの起動と操作
  • "idg" というテキストが製品名に含まれる製品の検索のシミュレーション
  • 結果内のアプリケーションの状態が "111" であるかどうかの検査
  • テスト シナリオの結果の判定

この際に、テスト自動化は、Microsoft .NET Framework の P/Invoke メカニズムを使用して FindWindow、SendMessage、GetMenu などの Win32® API 関数を呼び出し、テスト用アプリケーションを操作しています。

今月のコラムでは、テスト用アプリケーションについて簡単に説明した後、図 1 の結果を生成したテスト自動化について説明します。次に、このアプリケーションとテスト自動化が含まれた Windows XP Embedded システムを作成するための、Windows Embedded Studio ツールの比較的単純な使用方法について、詳しく説明します。テスト自動化をカスタム Windows XP Embedded シェルとして作成する、さらに高度な手法についても取り上げます。最後に、各自のニーズを満たすために、このコラムで示した考え方を応用する方法について簡単に説明します。

Windows XP Embedded に馴染みがない読者は、このコラムで興味深いテクノロジを知ることができます。.NET Framework インターフェイスを使用した Win32 ベースの UI テスト自動化に馴染みがない読者にとっても、この手法の紹介としてこのコラムは役立つでしょう。

テスト用アプリケーション

テスト用の Windows フォームベース アプリケーションの重要なコードを見てみましょう。これは一般的な Windows XP Embedded ソフトウェア システムの中心となる部分です。アプリケーションとテスト自動化の全コードは、この記事に付属のコード ダウンロードに含まれています。図 1 からわかるように、このダミー アプリケーションには、TextBox コントロール、ComboBox コントロール、Button コントロール、ListBox コントロールなどの基本的なユーザー コントロールがあります。実際の Windows XP Embedded 検索アプリケーションでは、多くの場合、データベース サーバーや Web サービスから検索結果を取得します。ここで使用するアプリケーションでは、次のような単純な Product クラスでローカル データ ストアを保持しています。

public class Product
{
    public string id, name, price;

    public Product(string id, string name, string price)
    {
        this.id = id; this.name = name; this.price = price;
    }
}

Product インスタンスのデータは、ArrayList コレクションに格納され、メインの Form オブジェクトが読み込まれるときに渡されます。

private ArrayList al = new ArrayList();
private void Form1_Load(object sender, System.EventArgs e)
{
    Product p1 = new Product("111", "Widget", "$11.11");
    Product p2 = new Product("222", "Gadget", "$22.22");
    Product p3 = new Product("333", "Thingy", "$33.33");
    al.Add(p1); al.Add(p2); al.Add(p3);
}

実行しているのが UI テスト自動化であるため、検索アプリケーションでデータがどこからどのように取得されるのかは重要ではありません。結果データがどこから取得されてもアプリケーションの UI に反映されるので、検査を行ってテスト シナリオの合否結果を判定できます。すべてのアプリケーション ロジックは、検索用 Button コントロールのクリック メソッド内にあります。アプリケーションは、最初にユーザーが検索条件 (製品 ID または製品説明) を指定したかどうかを簡単に確認します。指定されていない場合、アプリケーションは MessageBox ウィンドウにエラー メッセージを表示します。

if (comboBox1.Text == "" || textBox1.Text == "") {
    MessageBox.Show("You must enter search criteria and term",
                    "Error");
}
listBox1.Items.Clear();

アプリケーションは、検索結果が保持された ListBox コントロールをクリアした後、検索条件や対象文字列を取得し、ArrayList データ ストアを調べて一致するものを探します。

string filter = textBox1.Text;
if (comboBox1.Text == "Product ID") {
    foreach (Product p in al) {
        if (p.id.IndexOf(filter) >= 0) {
            listBox1.Items.Add(p.id + " " + p.name + " " + p.price);
        }
    }
}
else if (comboBox1.Text == "Product Name") {
    // search by Name similarly
}

短くてわかりやすいコードにするため、また開発の初期段階にあるアプリケーションの比較的粗削りな性質をシミュレートするため、ここではあえて模範的なコーディング スタイルや方法を無視しています。実際、サンプル コードには重大なロジックの欠陥がいくつか含まれているので、探してみるのも一興です。

Windows XP Embedded に馴染みがない方は、「このアプリケーションが Windows XP Embedded と何の関係があるのだろう。どう見ても普通の Windows フォームベースのアプリケーションだが」とお思いでしょう。その考えは間違っていません。それこそが重要な点なのです。Windows XP Embedded は基本的に Windows XP のコンポーネント化されたバージョンですから、すべての開発スキルをそのまま移行できます。これが、組み込みデバイス用の他の開発プラットフォームに比べて Windows XP Embedded が大きく優れている点です。まったく新しいプラットフォームやツールセットを学ぶ必要がなく、大規模なサポート コミュニティにアクセスでき、.NET Framework ライブラリの機能を使用できるのです。また、このコラムで説明するように、強力なテスト自動化手法も利用できます。

テスト自動化

UI テスト自動化を作成するには、いくつかの方法があります。ここで使用する手法は、P/Invoke メカニズムを使用してネイティブ Win32 API 関数を呼び出すことによってテスト用のフォームベース アプリケーションを操作する、C# コンソール アプリケーションのテスト ハーネスを記述することです。2005 年 9 月号の「テストの実行」コラムの「低レベルの UI テスト自動化」(msdn.microsoft.com/msdnmag/issues/05/09/testrun) では、このトピックを詳しく論じています。

このテスト自動化では、まず、System.Diagnostics 名前空間で Process クラスの静的な Start メソッドを使用して、テスト用アプリケーションを起動します。

Console.WriteLine("\nLaunching Windows application");
Process p = Process.Start(".\\TheAppToTest.exe");

サンプル コードでは、テスト用アプリケーションの実行可能ファイルがテスト ハーネスの実行可能ファイルと同じディレクトリにあることを前提としています。後で説明しますが、この設計は Windows XP Embedded システムにおけるテスト自動化にとって非常に好都合です。図 2 に示すように、テスト用アプリケーションの起動後、Win32 の FindWindow 関数を使用して、テスト用アプリケーションの Form オブジェクトへのハンドルを取得します。

Figure 2 ハンドルの取得 (Close Figure 2)
Console.WriteLine("\nLooking for Form1");
IntPtr ptrToForm;
bool formFound = false;
int numTries = 0;
while (!formFound && numTries < 100) {
    ++numTries;
    ptrToForm = FindWindow(null, "Form1");
    if (ptrToForm == IntPtr.Zero) {
        Console.WriteLine("Form1 not found yet. . . ");
        Thread.Sleep(100);
    }
    else {
        formFound = true;
        Console.WriteLine("Form1 found with ptr = " +
            ptrToForm.ToString());
    }
}
if (numTries >= 100) throw new Exception("Form was not found");

P/Invoke の中核は、Win32 の FindWindow 関数のインポートです。これは、テスト ハーネスで DllImport 属性を配置することによって行います。

[DllImport("user32.dll", EntryPoint = "FindWindow",
           CharSet = CharSet.Auto)]
static extern IntPtr FindWindow(
    string lpClassName, string lpWindowName);

この属性の大まかな意味は次のとおりです。"user32.dll ライブラリから Win32 関数をインポートする。この Win32 関数の名前を FindWindow とする (ただし、同じ名前で 'A' または 'W' で終わるものがあるかどうかもシステムで確認する必要がある)。ASCII および Unicode 文字エンコードの問題を自動的に処理する。C# 別名は .NET IntPtr 型を返す静的メソッドであり、ウィンドウへのハンドルを表す。この C# 別名は FindWindow という名前で、2 つの文字列引数 (ウィンドウ コントロールのクラス名とコントロールのフレンドリ名) を受け取る。"

図 2 のコードでは、Form ウィンドウの検索を試みます。Form ウィンドウが見つからない場合 (通常、Form オブジェクトが初期化されていないため)、100 ミリ秒間待機してから再試行します。100 回試行しても Form オブジェクトが見つからない場合、テスト自動化は例外をスローします。テスト自動化ハーネスは、テスト用アプリケーションを起動した後、サイズ変更や移動を行って Form オブジェクトを操作します。次に例を示します。

Console.WriteLine("\nMoving Form1 around");
SetWindowPos(ptrToForm, IntPtr.Zero, 100, 200, 336, 404, 0);

ここでは、Win32 の SetWindowPos 関数を使用して、Form オブジェクトの左上隅を画面座標 (100,200) に、幅 336 ピクセル、高さ 404 ピクセルで配置しています。

C# 別名に関連付けられた DllImport 属性は次のとおりです。

[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
    int x, int y, int cx, int cy, uint uFlags);

Form オブジェクトを操作した後、Form 上の子ウィンドウ コントロールへのハンドルを取得します。たとえば、button1 コントロールへのハンドルを取得するためのテスト ハーネス コードは次のようになります。

Console.WriteLine("Looking for button1");
IntPtr btn = FindWindowEx(ptrToForm, IntPtr.Zero, null, "Search");
if (btn == IntPtr.Zero)
    throw new Exception("Could not find search button");
else
    Console.WriteLine("button1 found with ptr = " + butt.ToString());

ここでは、Win32 の FindWindowEx 関数を使用して、Name 属性が "Search" である子コントロールのハンドルを取得しています。

TextBox コントロールへのハンドルを取得するためのコードは、次に示すようにやや複雑です。

Console.WriteLine("\nLooking for textBox1");
IntPtr tb = FindWindowByIndex(ptrToForm, 3);
if (tb == IntPtr.Zero)
    throw new Exception("Could not find textBox");
else
    Console.WriteLine("textBox1 found with ptr = " + tb.ToString());

ここでは、FindWindowByIndex という非常に便利なプログラム定義メソッドを使用しています。

ウィンドウ コントロールの中には、Name 属性を持つもの (Form1 など) も存在しますが、大多数はこの属性を持ちません。名前のないウィンドウ コントロールへのハンドルを取得するには、次のように定義した FindWindowByIndex を使用できます。

static IntPtr FindWindowByIndex(IntPtr hwndParent, int index)
{
    int ct = 0;
    IntPtr result = IntPtr.Zero;
    do {
        result = FindWindowEx(hwndParent, result, null, null);
        if (result != IntPtr.Zero) ++ct;
    } while (ct < index && result != IntPtr.Zero);
    return result;
}

つまり、Name 属性を持たないウィンドウ コントロールも存在しますが、すべてのウィンドウ コントロールには暗黙のインデックス番号があります。Win32 関数 FindWindowEx と、コントロールの暗黙のインデックス番号とを使用すると、そのコントロールへのハンドルを取得できます。ウィンドウ コントロールの暗黙のインデックス番号は、タブ コントロール オーダーとは異なります。暗黙のインデックス番号は、ウィンドウ コントロールが親ウィンドウに追加される順序によって決まります。したがって、この例では FindWindowByIndex(ptrToForm, 3) と指定します。これは、テスト用アプリケーションのコードに次のような記述があるからです。

this.label1 = new System.Windows.Forms.Label();       // index 0
this.label2 = new System.Windows.Forms.Label();       // 1
this.comboBox1 = new System.Windows.Forms.ComboBox(); // 2
this.textBox1 = new System.Windows.Forms.TextBox();   // 3 <-
this.button1 = new System.Windows.Forms.Button();
// etc.

テスト ハーネスの残りのコードで行うテスト用アプリケーションの操作は、TextBox コントロールへの文字送信、Button コントロールのクリック、エラーを示す MessageBox の待機、OK ボタンのクリックなどです。最後にテスト ハーネスは ListBox コントロールの内容を調べ、シナリオの合否結果を判定します。

UI テスト自動化を Windows XP Embedded システムに移植する

Windows XP Embedded システムにおける主要な概念の 1 つに、オペレーティング システム シェルがあります。これは、オペレーティング システム上で実行されるシェル アプリケーションに対置される概念です。どの Windows ベースのオペレーティング システムにも、プライマリ シェル インターフェイスが必ず 1 つだけあります。たとえば、標準の Windows XP オペレーティング システムの場合、通常は Explorer Shell がプライマリ シェルです。ただし、cmd.exe コマンド シェル、Windows PowerShell® シェルなどのアプリケーション シェルも使用できます。Windows XP Embedded は Windows XP のカスタマイズ可能なコンポーネント化されたバージョンであるため、Explorer Shell 以外もプライマリ シェルとして使用できます。プライマリ シェルを複数のシェルから選択できるだけでなく、カスタム プライマリ シェルを作成することも可能です。

「カスタム シェルとしてのテスト自動化」では、非カスタム プライマリ シェルを使用する場合の、UI テスト自動化を Windows XP Embedded システムに移植する方法を示します。次のセクションでは、カスタム プライマリ シェルを使用する場合にテスト自動化を移植する高度な方法を説明します。

ドキュメントの用語で "デバイス" と言うと、Windows XP Embedded を実行する一連の装置 (情報キオスクなど) を指す場合と、個々のハードウェア アイテム (マウス、キーボード、ディスプレイ アダプタなど) を指す場合があります。この例では、対象デバイスはラップトップ PC です。現実的なシナリオにおける対象デバイスは、もっと PC らしくない、おそらく縮小版キーボードや小さいビデオ ディスプレイの付いた機器でしょう。それでも、Windows XP Embedded の開発原則はまったく同じです。

この例の対象デバイスには物理ハード ドライブが 1 つあり、C: パーティションと D: パーティションに分割されています。今回の目的は、Windows XP Embedded システムを D: パーティションにコピーすることです。Windows XP Embedded システムの作成に使用できる Windows Embedded Studio には、一連のツールと、ソフトウェア コンポーネントのバックエンド データベースとが用意されています。Windows XP Embedded ツールは通常、対象デバイスではなく開発マシン (一般に Windows XP を実行) にインストールします。次に、カスタム Windows XP Embedded OS とアプリケーションのシステム イメージを開発マシン上に作成した後、そのイメージを対象デバイスに展開します。

Windows XP Embedded イメージの作成は、最初は複雑に思えるかもしれませんが、何度か繰り返すと理解しやすくなります。具体例を詳述する前に、このプロセスの概要を大まかに説明します。イメージ作成プロセスの第 1 段階では、Windows XP Embedded ツールセットに含まれている Target Analyzer Probe というユーティリティを使用して、対象デバイス上の個々のハードウェア デバイスを解析します。次の段階では、Component Designer (詳細は後述) というツールを使用して、Windows XP Embedded コンポーネントを作成します。第 3 段階では、コンポーネントを組み合わせて 1 つの Windows XP Embedded イメージを作成します。第 4 段階では、このイメージを対象デバイスに展開します。このように、プロセスは基本的には単純です。

それでは、Windows XP Embedded の UI テスト自動化の作成について詳しく説明しましょう。まず、Target Analyzer Probe (tap.exe。16 ビット システム用の ta.exe プログラムもあり) を使用して、対象デバイス上のハードウェア アイテムを解析します。tap.exe ユーティリティを対象デバイス上にコピーするだけです。コピーの実行方法は、対象デバイスで使用可能な I/O デバイスによって異なります (tap.exe ユーティリティを使用するには、Windows 2000 または Windows XP が必要です。または、この用途で起動可能な Windows XP Embedded インストール ディスクを使用することもできます)。

tap.exe を対象デバイス上にコピーしたら、単にこれを実行します。その結果、devices.pmq という名前のファイル (俗に "パンプキン ファイル" とも呼ばれる) が生成され、tap.exe と同じディレクトリに格納されます。このファイルには、対象デバイス上で検出された全ハードウェア アイテムの一覧が含まれています。このファイルは XML 形式なので、任意のテキスト エディタで読み取り、すべてのハードウェア デバイスが検出されたかどうかを確認できます。必要に応じて、ファイルを手動で作成または編集することもできます。devices.pmq が作成されたら、それを対象デバイスから開発マシンにコピーします。

コンポーネントを作成する

以降の段階では、Component Designer という GUI ベースのツールを使用して "コンポーネント" を作成します。Windows XP Embedded コンポーネントは、関連するソフトウェアのパッケージと考えることができます。この記事において、Windows XP Embedded パッケージを指すときには "コンポーネント" (二重引用符付き)、アイテムのコレクションを一般的に指すときにはコンポーネント (二重引用符なし) と表記します。

Windows Embedded ツールでは、個々のファイルではなく "コンポーネント" を処理します。Windows XP Embedded イメージには多数のファイルが含まれており、個々のファイルを管理するのは厄介だからです。作成する必要があるカスタム Windows XP Embedded "コンポーネント" は 2 つあります。1 つはフォームベース アプリケーションとそのコンソールベースのテスト自動化をカプセル化するもの、もう 1 つは適切なハードウェア デバイス ドライバをカプセル化するものです。

準備として、開発マシン上に C:\Automation ディレクトリを作成し、このディレクトリに TheAppToTest.exe (アプリケーション) と Demo.exe (テスト自動化) を配置します。これはどうしても必要というわけではありませんが、テスト自動化 "コンポーネント" を作成する場合に便利な方法です。これについては後で触れます。

まず、テスト自動化コンポーネントの作成手順について説明し、続いてデバイス ドライバ コンポーネントの作成手順を説明します。デスクトップの [スタート] メニューから Component Designer ツールを起動し、[File] (ファイル) メニューの [New] (新規作成) をクリックします。[File] (ファイル) メニューの [Save] (保存) を使用することで、独自の構成を AutoTestAutomationComponent.sld (スライド ファイル) として任意のディレクトリに保存できます。その際に .sld 構成ファイルを "コンポーネント" のリソース ファイルと同じディレクトリに保存すると、すべてのファイルが 1 つのディレクトリに格納されるため、"コンポーネント" 全体を移植できるようになります。

Component Designer の左側にあるナビゲーション ペインで、[Windows XP Embedded Client (x86)] (Windows XP Embedded クライアント (x86)) アイテムを展開します。ここで、テスト自動化 "コンポーネント" 用の Repository オブジェクトを作成します。Repository は、"コンポーネント" を構成するファイルが格納される物理ストレージと考えることができます。[Repositories] (リポジトリ) アイテムを右クリックし、[Add Repository] (リポジトリの追加) をクリックします。右側のデータ ペインで、Repository に AutoTestAutomationRepository という名前を付け、Source Path プロパティを (対象デバイス上の最終的な場所ではなく) 開発マシン上のテスト自動化ファイルの場所を示す C:\Automation に変更します。Repository には、この時点で設定可能な属性が他にもありますが、説明を簡単にするため、ここでは触れません。

次に、Component Designer の左側にあるナビゲーション ペインに戻って、[Components] (コンポーネント) を右クリックし、[Add Component] (コンポーネントの追加) をクリックします。この時点で、Component Designer の左ペインは図 3、右ペインは図 4 の画面のように表示されているはずです。右ペインの [Component Properties] (コンポーネントのプロパティ) 領域では、コンポーネントの Name 属性が AutoTestAutomationComponent に設定されていることがわかります。

図 3 Component Designer の左ペイン
図 3 Component Designer の左ペイン
図 4 Component Designer の右ペイン
図 4 Component Designer の右ペイン(画像を縮小するには、ここをクリックします)
図 4 Component Designer の右ペイン
図 4 Component Designer の右ペイン (画像を拡大するには、ここをクリックします)

次に、作成した Repository を "コンポーネント" に関連付けるため、[Repositories ...] (リポジトリ) ボタンをクリックし、[Select Repository] (リポジトリの選択) ダイアログ ボックスで [AutoTestAutomationRepository] エントリを選択してから、[OK] をクリックします。Repositories も含め、ほとんどの Windows XP Embedded オブジェクトでは、オブジェクト名にリビジョン番号が自動的に付加されます。これはオブジェクトの管理にたいへん役立ちます。今度は左ペインで、物理ファイルに参照を追加します。これを行うには [Files] (ファイル) アイテムを右クリックし、[Add] (追加)、[Multiple Files] (複数ファイル) の順にクリックします。[Add Component File Resources] (コンポーネント ファイル リソースの追加) ダイアログ ボックスで、アプリケーションとテスト自動化のファイルがある開発マシン上の場所 (ここでは C:\Automation) に移動します。<Ctrl> キーを使用して TheAppToTest.exe ファイルと Demo.exe ファイルを選択し、[Open] (開く) をクリックします。

さて、次は Component Dependency を追加します。この例の場合、フォームベース アプリケーションとコンソールベースのテスト自動化がどちらも .NET Framework 2.0 上に構築されているため、最終的な Windows XP Embedded イメージに .NET Framework 2.0 "コンポーネント" を含める必要があります。この依存関係を明示的に指定することによって、.NET Framework 2.0 "コンポーネント" を含めるよう、Target Designer ツール (前に述べたような、すべての "コンポーネント" を組み合わせて最終の実用的な Windows XP Embedded イメージにするツール) に暗黙的に指示できます。[Component or Group Dependency] (コンポーネントまたはグループの依存関係) アイテムを右クリックし、[Add] (追加)、[Component Dependency] (コンポーネントの依存関係) の順にクリックします (Group Dependency については、このコラムの次のセクションで説明します)。この時点で、Component Designer が "コンポーネント" のバックエンド データベース (Windows XP Embedded Studio ツールの一部としてインストール済み) に接続します。

[Add Component Dependency] (コンポーネントの依存関係の追加) ダイアログ ボックスで、[Software] (ソフトウェア)、[System] (システム)、[System Services] (システム サービス)、[Other] (その他) の順に移動します。.NET Framework 2.0 "コンポーネント" を選択し (図 5 を参照)、[OK] をクリックします 使用する Windows XP Embedded ツールセットのバージョンによっては、.NET Framework 2.0 "コンポーネント" を別途インストールする必要があります。"コンポーネント" の一覧は動的に増え続けています。Windows XP Embedded には、マイクロソフトだけでなくサードパーティのエンジニアや企業を擁する、大規模なサポート コミュニティが存在するのです。

図 5 [Add Component Dependency] (コンポーネントの依存関係の追加)
図 5 [Add Component Dependency] (コンポーネントの依存関係の追加)(画像を縮小するには、ここをクリックします)
図 5 [Add Component Dependency] (コンポーネントの依存関係の追加)
図 5 [Add Component Dependency] (コンポーネントの依存関係の追加) (画像を拡大するには、ここをクリックします)

これでテスト自動化 "コンポーネント" は完成です。.sld 構成を保存し、Component Designer ツールを閉じます。ここで、Component Database Manager ツールを使用して、カスタム "コンポーネント" をバックエンド データベースに転送する必要があります。このツールは、デスクトップの [スタート] メニューから起動します。ツールでバックエンド データベースに接続したら、メインの [Database] (データベース) タブにある [Import] (インポート) ボタンをクリックします。[Import SLD] (SLD のインポート) ダイアログ ボックスで .sld ファイル (ここでは AutoTestAutomationComponent.sld) を参照し、[Import] (インポート) をクリックします。処理はすぐに完了します。ログ メッセージを確認した後、[Close] (閉じる)、[Close] (閉じる) の順にクリックします。これで、テスト自動化 "コンポーネント" を Target Designer ツールで使用できるようになりました。

次は、もう 1 つの "コンポーネント" を作成する必要があります。つまり、対象デバイス上の個々のハードウェア アイテムをサポートするために必要とされるデバイス ドライバなどのソフトウェアすべてをカプセル化する "コンポーネント" です。対象デバイスの全ハードウェアの一覧である devices.pmq ファイルのおかげで、このプロセスは非常に簡単です。もう一度 Component Designer ツールを起動します。ただし今回は、[File] (ファイル) メニューの [Import] (インポート) をクリックします。[Choose File for Import] (インポートするファイルの選択) ダイアログ ボックスで devices.pmq ファイルに移動し、[Open] (開く) をクリックします。[Import File] (ファイルのインポート) ダイアログ ボックスが表示されるので、[Start] (開始) ボタンをクリックします。インポート処理には数分かかります。Component Designer では、devices.pmq 内の XML データをインポートするだけでなく、このファイルを解析し、ハードウェア デバイスをサポートするために必要なソフトウェア ファイルを判断し、その情報から "コンポーネント" を作成するためです。つまり、1 回限りのテスト自動化 "コンポーネント" の作成時に手動で行う必要がある作業のほとんどが、ハードウェア コンポーネントの場合は自動的に行われるのです。"Import session completed successfully" (インポート セッションが正常に完了しました) というメッセージが表示されたら、[Import File] (ファイルのインポート) ダイアログ ボックスを閉じます。

[Components] (コンポーネント) アイテムを展開してデバイス アイテムを選択した場合、"コンポーネント" の名前を AutoHardwareComponent に変更できます。構成を AutoHardwareComponent.sld として保存し、Component Designer ツールを閉じます。この後、テスト自動化 "コンポーネント" の場合と同様に Component Database Manager ツールを使用して、ハードウェア "コンポーネント" をバックエンド データベースにインポートできます。

OS コンポーネント

テスト自動化 "コンポーネント" とハードウェア "コンポーネント" が作成されたので、これらとオペレーティング システム "コンポーネント" を組み合わせて Windows XP Embedded イメージを作成する準備ができました。いよいよ Target Designer ツールを使用します。

[スタート] メニューから Target Designer を起動し、[File] (ファイル) メニューの [New] (新規作成) をクリックします。[New Configuration] (新しい構成) ダイアログ ボックスが表示されたら、構成に名前を付け (この例では AutoTestAutomation)、[OK] をクリックします。Target Designer の UI には、主要なペインが 3 つあります。左ペインには、Windows XP Embedded イメージに追加できる "コンポーネント" が一覧表示されます。中央のペインには、現在のイメージ内の "コンポーネント" が一覧表示されます。右ペインには、左または中央のペインで選択したアイテムに関する情報が表示されます。

左ペインでは、バックエンド データベースを自動的に読み取り、作成済みのすべての "コンポーネント" (ここでは AutoHardwareComponent と AutoTestAutomationComponent) を一覧表示します。[AutoHardwareComponent] を右クリックして [Add] (追加) をクリックし、ハードウェア "コンポーネント" を追加します。同様にして AutoTestAutomationComponent も追加します (追加する順序はどちらでもかまいません)。

2 つのカスタム "コンポーネント" を追加した後は、コア OS "コンポーネント" を追加します。使用可能な何千 (通常は 10,000 以上) もの "コンポーネント" を調べる方法もありますが、事前に構築されたメタ "コンポーネント" を使用する方がはるかに簡単です。Target Designer の左ペインで、[Design Templates] (デザイン テンプレート) アイテムを展開してオプションを表示します。この例では、Windows-based Terminal Professional テンプレートを選択して右クリックし、[Add] (追加) をクリックします。これにより、PC に似たデバイスで一般的に使用される多数のソフトウェア "コンポーネント" が追加されます。Retail Point of Sale Terminal、Home Gateway、Network Attached Storage など、複数のテンプレートが用意されています。"コンポーネント" テンプレートを使用する際のマイナス面は、多くの場合、実際に必要としない "コンポーネント" が追加されてしまうことです。テンプレートは単に出発点として使用し、不要な "コンポーネント" があればすべて削除することをお勧めします。

中央のペインで [Settings] (設定) を選択してから、右ペインで [Target Device Settings] (対象デバイスの設定)、[Show] (表示) の順にクリックします。これらのエントリは、開発マシンではなく対象デバイスを表します。したがって、ここでは、ブート ドライブは C: のままとし、Windows フォルダ、Program Files フォルダ、および Documents and Settings フォルダのエントリでは、C: を D: に変更します。これは、前に述べたように、デバイスの単一ハード ドライブの D: パーティションに Windows XP Embedded イメージをコピーする予定だからです。

Boot ARC パス エントリを編集して multi(0)disk(0)rdisk(0)partition(1) から multi(0)disk(0)rdisk(0)partition(2) に変更します。rdisk (物理ドライブ) アイテムは 0 から始まりますが、パーティション アイテムは 1 から始まります。また、ブート パーティションのサイズを 5000 (MB) に変更します。これは、5 GB 以上のブート パーティションを表す魔法の値です。値として約 700 MB を指定すると、起動可能な CD を作成できるようになります。この例では、パーティションのクラスタ サイズを 4096 バイトのままとします。ここで構成を保存することをお勧めします。AutoTestAutomation.slx として保存します。

イメージを作成する
カスタム シェルとしてのテスト自動化

場合によって、 Windows XP Embedded を使用する開発者が、カスタム プライマリ シェルを実装することがあります。たとえば、図 1 のフォームベース アプリケーションが、完成された UI として機能しているような場合です。この方法を使用すると、ユーザー アクティビティが厳密にアプリケーション上に限定され、通常はエクスプローラや Internet Explorer® などのプログラムと対話できないようになります。カスタム シェルのシナリオでは、通常のテスト自動化手法を使用できるとは限りません。OS シェルの起動方法がアプリケーションの起動方法とは異なるためです。

Windows XP Embedded カスタム シェルのシナリオにおけるテスト自動化ソリューションとしては、Windows Embedded Studio ツールを使用してテスト自動化 "コンポーネント" を作成し、この "コンポーネント" をプライマリ シェルとして構成する方法があります。このプロセスの概要を簡単に説明します (Windows XP Embedded のカスタム シェル作成の詳細については、msdn2.microsoft.com/ms838335 の Mike Hall による記事「Windows XP Embedded シェルを作成する」を参照してください)。

カスタム テスト自動化プライマリ シェルを作成するには、まず、このコラムで既に説明したとおりに Component Designer ツールを使用し、テスト自動化 "コンポーネント" を作成します。続いて Component Designer で 4 つの設定を構成し、この "コンポーネント" を特別なプライマリ シェル "コンポーネント" に変換する必要があります。

  1. Component Prototype プロパティとして、Software | System | User Interface | Shells | Windows Shells | Shell Prototype Component を設定します。
  2. 次の設定の Advanced Property を追加します。Name = cmiShellPath (大文字と小文字を区別)、Format = String、Value = \Automation\Demo.exe (テスト自動化へのパスなど)。
  3. グループ メンバシップとして、Group Memberships | Add Group Membership | Dependencies | Shell を設定します。
  4. もう 1 つのグループ メンバシップとして、Group Memberships | Add Group Membership | Software | System | User Interface | Shells を設定します。

これらの 4 つの設定を構成して特別なプライマリ シェル "コンポーネント" を作成した後は、Explorer Shell、Command Shell などと共に、この "コンポーネント" も UI シェルとして Target Designer ツールに表示されるようになります。このカスタム テスト自動化シェル "コンポーネント" を Windows XP Embedded イメージに追加すると、オペレーティング システムでテスト自動化が直接起動され、関連付けられたフォームベース アプリケーションが実行されるようになります。

これで、"コンポーネント" を組み合わせて 1 つのイメージにする準備がほぼ整いました。ただし、作成を試みる前に、"コンポーネント" の依存関係を確認し、必要な "コンポーネント" が構成から抜け落ちていないかどうかを調べる必要があります。これは重要な手順です。オペレーティング システムは複雑なソフトウェアであり、Target Designer などのツールを使用せずに手動でコンポーネントのすべての依存関係を確認することは非常に困難です。

メインのメニュー バーで、[Configuration] (構成) メニューの [Check Dependencies] (依存関係の確認) をクリックします。ダイアログ ボックスが開き、進行状況メッセージが表示されます。依存関係の確認時に、必要な "コンポーネント" が構成に自動的にインポートされます。依存関係の確認が終了したら、ダイアログ ボックスを閉じることができます。多くの場合、Target Designer の下部ペイン内のタスク リストにエラーが表示されます。エラーを右クリックすると [Action] (アクション) オプションが表示され、推奨される解決策が示されます。たとえば、この例の場合、"Regional and Language Options requires at least one additional enabled component not in the configuration" (地域と言語のオプションには、構成に存在しない使用可能なコンポーネントを少なくとも １ つ追加する必要があります) というエラーが表示されます。右クリックして [Action] (アクション) をクリックすると、[Resolve Dependency] (依存関係の解決) ダイアログ ボックスが表示されます。このダイアログ ボックスで、言語の一覧から [English] (英語) を選択して [Add] (追加) をクリックします。この例では、その他のエラーを解決するため、NT Loader "コンポーネント"、NTFS Format "コンポーネント"、および Explorer shell "コンポーネント" も追加する必要があります。

これで、[Configuration] (構成) メニューの [Build Target Image] (ターゲット イメージの作成) をクリックできます。クリックすると、構成の名前 (AutoTestAutomation.slx) が含まれる作成用のダイアログ ボックスが表示されます。次に [Build] (作成) ボタンをクリックすると、構成が変更された (依存関係の確認によって検出されたエラーを解決するために新しい "コンポーネント" を 4 つ追加したため) という警告が Target Designer から出され、依存関係の確認を再度実行するかどうかをたずねられます。[Yes] (はい) をクリックすると、依存関係の確認がもう一度実行されます。ただし、今回は前回よりもはるかに高速です。2 度目の依存関係の確認が完了し、エラーが検出されなかった場合、[Dependency Check] (依存関係の確認) ダイアログ ボックスを閉じると作成プロセスが開始されます。

Target Designer で [Tools] (ツール)、[Options] (オプション)、[Build] (作成) の順にクリックすると、システムに Windows XP Embedded イメージが格納される既定の場所を構成できます。[Build] (作成) ダイアログ ボックスを使用して、作成時ごとに場所を指定することもできます。Windows XP Embedded イメージが既に存在する場合、既存のイメージが上書きされるという警告メッセージが表示されますが、通常は問題ありません。

作成終了後に警告がいくつか表示されても驚くことはありません。これらを読み、無視しても安全な小さい問題 (評価版ソフトウェアを使用しているという警告など) であることを確認します。

イメージを展開する

これで、Windows XP Embedded イメージを対象デバイスに展開する準備ができました。新しいイメージは、4 つのファイルと、4 つのサブディレクトリを持つ 1 つのディレクトリから成ります。ファイルのうちの 3 つ (boot.ini、ntdetect.com、ntldr) は、Windows XP ベースのオペレーティング システムを起動するために必要なキー ファイルです。4 つ目のファイル (weruntime.ini) は基本的に、Windows XP Embedded を有効化する特別な Windows XP Embedded キーです。

これら 4 つのファイルを、開発マシンから対象デバイスのブート ドライブにコピーします。これらのファイルを実際にコピーする方法は、対象デバイスが持つ I/O デバイスの種類によって異なります。この場合の対象デバイスは、標準の Windows XP が C: ドライブにインストールされたラップトップであるため、対象デバイスを単に起動し、CD/DVD ドライブを使用してファイルをコピーできます。メインの Windows XP Embedded イメージは、DriveD というディレクトリに配置されます (対象デバイス上のコピー先が D: ドライブであるため)。このディレクトリには 3 つのディレクトリ、つまり Documents and Setting、Program Files、および Windows が含まれています。さらに、Automation ディレクトリもあり、フォームベース アプリケーション (TheAppToTest.exe) とコンソールベースのテスト自動化 (Demo.exe) が格納されています。これら 4 つのディレクトリを、対象デバイスの D: パーティションにコピーします。

対象デバイスの起動を試みる前に 1 か所だけ編集を加えて、デバイスをマルチブート可能にすることをお勧めします。 Windows XP Embedded イメージの boot.ini ファイルを編集して、[operating systems] セクションに次の行を追加します。

multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /fastdetect

これにより、D: パーティションの Windows XP Embedded イメージがうまく機能しなかった場合には C: ドライブの標準 Windows XP セッションで、対象デバイスを簡単に起動できるようになります。対象デバイスにフロッピー ディスク ドライブがある場合は、起動可能な回復用フロッピー ディスクを作成する方法もあります。また、元の Windows XP インストール CD を常に用意しておき、必要に応じて回復モードで起動することもできます。

さあ、ついに決定的瞬間がやってきました。対象デバイスの電源を入れます。Windows XP Embedded イメージが正しければ、対象デバイスがカスタム システムで起動します。まとめて First Boot Agent (FBA) と呼ばれる一連の特別なプログラムによって、最初の起動時にシステムが初期化されます (FBA は 2 度目以降は実行されません)。

これで、テスト自動化を実行できます。この例では Explorer Shell コンポーネントを使用しているため、システムには GUI インターフェイスがあります。Windows キーを押しながら E キーを押すと、エクスプローラが起動します。D:\Automation に移動して Demo.exe ファイルをダブルクリックすると、テスト自動化が起動します。結果は図 1 のスクリーンショットのようになります。または、CMD (Windows Command Processor "コンポーネント") を Windows XP Embedded イメージに含める方法もあります。こうすると、テスト自動化の起動に cmd.exe コマンド シェルを使用できるようになります。

簡単な要約

これまでの内容を要約し、この手法の利点について説明します。Windows XP Embedded は基本的に Windows XP オペレーティング システムの 1 つのバージョンであるため、標準的なプログラミング手法を使用して、強力な UI テスト自動化を作成できます。

Windows XP Embedded システム用の UI テスト自動化を作成する方法として、プライマリ アプリケーションとそれに関連付けられたテスト自動化とが格納された Windows XP Embedded イメージを含む "コンポーネント" を作成する方法があります。または、テスト自動化 "コンポーネント" を含まない通常のイメージを作成した後で、別途、テスト自動化を対象デバイスに移植する方法もあります。

テスト自動化 "コンポーネント" を使用する方法には、大きな利点が 3 つあります。第 1 に、テスト自動化 "コンポーネント" を作成するとテスト作業がモジュール化され、保守や管理が容易になります。第 2 の利点は、Windows XP Embedded "コンポーネント" のバックエンド データベースをリモート サーバー上に格納できるため、複数の開発者やテスト担当者のチームによるテスト自動化 "コンポーネント" の共有が簡単であることです。第 3 の利点は、テスト自動化 "コンポーネント" を作成すると、"コンポーネント" の複雑になりがちなソフトウェア依存関係を Target Designer ツールで自動的に確認できるようになることです。

ご意見やご質問は、

こちら testrun@microsoft.com.

まで英語でお送りください。
Dr. James McCaffrey は Volt Information Sciences, Inc. に勤務し、ワシントン州レドモンドにあるマイクロソフト本社で働くソフトウェア エンジニアの技術トレーニングを管理しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation Recipes』(Apress、2006 年) の著者でもあります。James の連絡先は jmccaffrey@volt.com または v-jammc@microsoft.com です。

Mike Hall は、Microsoft Mobile and Embedded Devices グループのシニア テクニカル製品マネージャです。Mike はマイクロソフトに 10 年以上在籍し、現在は Windows CE、Windows XP Embedded、および Windows Embedded for Point of Service を担当しています。また、マイクロソフトやサードパーティの多数のイベントで発表を行うかたわら、組み込みテクノロジに関するブログも執筆しています。


基礎: WPF の 3D テキスト

基礎
WPF の 3D テキスト
Charles Petzold

コードのダウンロード : : Foundations2007_10.exe (171KB)
コンテンツ
FormattedText と BuildGeometry
アウトラインとメッシュ
Text3D 階層
RibbonText と SliverText
ブレークスルーとなった SolidText
文字に膨らみを持たせる

TrueType を始めとするアウトライン フォント技術は、柔軟かつ正確な文字表現を可能にするだけでなく、表現に遊びを加える場合にも利用できます。プログラミングの際には、個々のテキスト文字を定義する実際のアウトラインにアクセスし、それらをベクタ グラフィックス オブジェクトとして扱うことができます。アウトラインは、輪郭を描画したり塗りつぶしたりできます。また、クリップ操作に使用したり、変換を適用したりすることもできます。Microsoft® Word でよく使われるワードアート機能は、この概念に基づいています。

この文字アウトラインの性質と制限事項を認識しておくことは重要です。文字アウトラインは、厳密に幾何学的であり、通常オペレーティング システムが画面にフォントをレンダリングする際に使用する "ヒント" がありません。ヒントは、使用可能なピクセル グリッドに基づいて文字を適切にラスタライズできるようにするものです。そのため、ヒントのない文字アウトラインは、大きなフォント サイズまたは高解像度のデバイスで最適に表示されます。テキストを通常のフォント サイズで画面にレンダリングする場合にはあまり適していません (ただし、プリンタの解像度が高くなり、画面のグラフィックスにアンチエイリアシングが使用される場合が増えたことで、ヒントの有用性は低下しています)。

新しい Windows

Windows Presentation Foundation (WPF) では、文字アウトラインにアクセスするためのクラスとメソッドが、隠れた形ですが確かに存在します。System.Windows.Media 名前空間の FormattedText クラスと GlyphRun クラスは、どちらも特定のフォントとテキスト文字列の Geometry オブジェクトを返す BuildGeometry というメソッドを持っています。この 2 つのクラスでは FormattedText の方が簡単なので、この記事では FormattedText のみを使用します。拙著『Applications = Code + Markup』 (Microsoft Press、2006 年) の 28 章と 30 章に、2 次元グラフィックスでの FormattedText と BuildGeometry の使用例を掲載しています。

WPF で 3D グラフィックスを扱い始めたときに当然ながら考えたのは、文字アウトラインを活字媒体やテレビでのフライング ロゴ効果に見られるような 3 次元テキストのブロックに変えられないかということでした (図 1 の例を参照)。この作業には、2 次元のアウトラインを 3 次元の三角形メッシュに変換する操作が伴うことはわかりましたが、それだけでなく、難しいプログラミングが必要になるということも確信しました。

図 1 ソリッド 3D テキスト
図 1 ソリッド 3D テキスト

FormattedText と BuildGeometry

WPF プログラマの多くは、FormattedText クラスをあまり使用したことがないと思います。ドキュメントでも説明されているように、このクラスは "テキストの描画を低レベルで制御" するために使用します。

FormattedText コンストラクタには TypeFace オブジェクトが必要です。このオブジェクトは、フォント ファミリ、スタイル (斜体など)、太字指定、およびフォントに関連付ける拡大や圧縮を定義します。また、FormattedText コンストラクタには、em サイズ (フォントの高さ)、フォント文字を着色するブラシ、そしてテキスト文字列自体が必要です。

FormattedText オブジェクトが作成されたら、さまざまなメソッドを呼び出して、各種フォント、スタイル、書式などをテキスト文字列のサブセットに対して設定できます。また、プロパティを使用して行間隔やテキストのその他の特性を設定できます。

FormattedText オブジェクトは、DrawingContext クラスの DrawText メソッドで最もよく使用されます。一般に、DrawingContext クラスを目にするのは、UIElement によって定義される OnRender メソッドをオーバーライドするときです。これは、アプリケーションで実行できる最も低いレベルのグラフィック出力であり、いまだに純粋な WPF アプリケーションと見なされています。DrawText メソッドに必要なのは、FormattedText オブジェクトと、テキストの開始位置となる座標点だけです。

BuildGeometry メソッドは、FormattedText のその他すべての要素との関係において、特異な存在であるように見えます。このメソッドは、Point 型の原点のみを引数として、Geometry オブジェクトを返します。

Geometry は WPF の重要なクラスであり、明らかに従来のグラフィック パスに関連しています。Geometry オブジェクトは、座標点として指定された直線と曲線のコレクションです。場合によってはこの直線と曲線の一部が連結され、連結された直線と曲線が閉じられて閉じた領域を表していることがあります。Geometry オブジェクトにレンダリングという概念はありません。2 次元グラフィックスのプログラミングで Geometry オブジェクトをレンダリングする場合は、上位の Shapes ライブラリにある便利な Path クラスにこのオブジェクトを渡します。または、Geometry、Brush、Pen の各オブジェクトに基づく GeometryDrawing を使用する方法もあります。

一般に、2 次元グラフィックスをプログラミングする場合に、特定の Geometry オブジェクトの内部を詮索する理由はほとんどありません。しかし、Geometry オブジェクトを 3 次元テキストに変換するのはまったく別の作業です。では、FormattedText.BuildGeometry から返される Geometry オブジェクトは何でしょうか。

Geometry は抽象クラスであり、ここから 7 つのクラスが派生します。私の経験では、FormattedText.BuildGeometry から返される Geometry オブジェクトは、実際には 1 つ以上のネストされた GeometryGroup オブジェクトであり、テキスト文字列内の文字ごとに 1 つの PathGeometry オブジェクトを含んでいます。各 PathGeometry オブジェクトは、閉じたパスを定義する 1 つ以上の PathFigure オブジェクトで構成されます。一部の文字 (l、t、x など) は、PathFigure を 1 つだけ必要とします。それ以外の文字には 2 つ必要です。たとえば小文字の i には、点を表す 2 個目の PathFigure が必要です。O には、外側の円と内側の円のアウトライン用に 2 つの PathFigure が必要です。大文字の B には 3 つの PathFigure オブジェクトが必要です。パーセント記号には 5 つ必要です。

各 PathFigure は、別の抽象クラスである PathSegment 型のオブジェクトのコレクションです。これまでの経験から、テキスト アウトラインに関連付けられている PathFigure オブジェクトには、LineSegment、PolyLineSegment、BezierSegment、PolyBezierSegment の各型のオブジェクトが複数含まれており、それらが単一の閉じたパスを定義していることがわかりました。

WPF 3D にポリラインやベジエ曲線の概念はありませんが、三角形メッシュはポイントのコレクションに基づいているので、最初に Geometry オブジェクトを FormattedText.BuildGeometry から一連の閉じたポリラインに変換する必要があります。これは簡単な作業のはずでした。しかし、これらのポリラインを三角形メッシュに変換するアルゴリズムを考え出すのは、複雑な数学的作業であり、私の能力の範囲を超えていることがすぐにわかりました。

アウトラインとメッシュ

MSDN Magazine の 2007 年 4 月号で、WPF 3D グラフィックスで使用する MeshGeometry3D オブジェクトを生成する手順について説明しました (msdn.microsoft.com/msdnmag/issues/07/04/Foundations を参照)。少なくとも、MeshGeometry3D オブジェクトの Positions プロパティと TriangleIndices プロパティは設定する必要があります。Positions プロパティは、3D 空間におけるポイントのコレクションです。TriangleIndices コレクションは、これらのポイントから三角形を作成する方法を表します。TriangleIndices 内の 3 つずつの整数が Positions コレクション内の 3 つのポイントを参照して、三角形が形成されます。

私は、テキスト アウトラインから生成したポリラインが MeshGeometry3D の Positions コレクションのメンバになると考えました。図 2 は、2 つの閉じたポリラインと合計 11 個のポイントから成る sans-serif フォントの大文字 A を示しています。2 番目のイメージは、この文字が 7 つの三角形に分割された様子を示しています。このような簡単な文字の場合、手作業で行うのは簡単ですが、そのプロセスをコードで記述する方法はまったくわかりませんでした。

図 2 ポイントと三角形で定義される文字
図 2 ポイントと三角形で定義される文字

当然ながら、各三角形の辺が文字の境界からはみ出さないようにして、文字の表面全体を三角形のコレクションで埋める必要があります。ここで使用した例が簡単な sans-serif フォントの単純な文字であることを忘れないでください。セリフ付きフォントの大文字 S で考えると、作業は実に厄介になります。ドロネー三角形分割と呼ばれる技法が役に立つ場合もありますが、数学的作業はかなり複雑です。

さいわいなことに、まだ方法は残されていました。この問題に取り組んでいるときに、ある広告のテキストが別の手法で 3D 化されているのを見ました。そこではアウトラインだけが処理され、各文字の本体は空洞になっていたのです。これなら私にもできると思いました。まずはクラス名が必要だったので、RibbonText という名前を考えました。

Text3D 階層

この記事でダウンロードできるコードには、Text3D という Visual Studio® ソリューションが 1 つだけ含まれています。このソリューションは、Petzold.Text3D という DLL プロジェクトと、この DLL を使用する、XAML で作成された複数の小さなデモ プログラムで構成されています。DLL 内のファイルの名前空間は Petzold.Text3D です。

Petzold.Text3D.dll ライブラリ内のクラス階層は ModelVisualBase で始まります。これは、WPF 3D の ModelVisual3D クラスから派生した抽象クラスです。この技法については、MSDN Magazine の 2007 年 4 月号で説明しています。ただし、前のコラムで説明した ModelVisualBase にはもう少し高い柔軟性が必要であったため、今回取り上げる ModelVisualBase クラスはまったく同じものではありません。ただ、概念は同じであることは認識しておいてください。ModelVisualBase は、内部に GeometryModel3D オブジェクトと MeshGeometry3D オブジェクトを格納し、GeometryModel3D オブジェクトに転送する Material および BackMaterial というパブリック プロパティを定義します。

Petzold.Text3D クラスのプロパティは、ほとんどが依存関係プロパティでサポートされます。ModelVisualBase クラスは 2 つの PropertyChanged イベント ハンドラ (1 つは静的メンバ、もう 1 つはインスタンス メンバ) を定義し、子孫クラスは依存関係プロパティを定義する際にこのハンドラを使用できます。インスタンス バージョンは、変更に備えて内部の MeshGeometry3D オブジェクトの各種プロパティを適切に準備し (4 月号のコラムで説明したプロセス)、Triangulate という抽象メソッドを呼び出します。子孫クラスでは、Triangulate をオーバーライドして MeshGeometry3D のさまざまなコレクションを定義できます。

Petzold.Text3D ライブラリでは、ModelVisualBase から Text3DBase 抽象クラスが派生しています。このクラスは、Text、FontFamily、FontStyle、FontWeight、FontStretch、FontSize という一連のテキスト関連プロパティを定義します。いずれも FormattedText コンストラクタに必要なプロパティです。またこのクラスは、BuildGeometry メソッドに必要な Origin プロパティも定義します。このクラスは、このいずれかのプロパティが変更されるたびに新しい FormattedText オブジェクトを作成し、BuildGeometry から返された Geometry オブジェクトから、Text3DBase の 8 番目のプロパティである TextGeometry を設定します。

FormattedText formtxt =
    new FormattedText(Text, CultureInfo.CurrentCulture,
        FlowDirection.LeftToRight,
        new Typeface(FontFamily, FontStyle,
            FontWeight, FontStretch),
            FontSize, Brushes.Transparent);

TextGeometry = formtxt.BuildGeometry(Origin);

この TextGeometry オブジェクトは、すべての子孫クラスで使用できます。

Text3DBase クラスが新しい TextGeometry オブジェクトを作成するときには、必ずヒープ割り当てが行われます。このクラスは新しい FormattedText オブジェクトを割り当てる必要があり、BuildGeometry が大量のメモリ割り当てを独自に行うことは明らかです。このヒープ割り当てを考えると、Text3DBase クラスで定義されるプロパティをアニメーション化するのはおそらく適切ではありません。そこで私は、UIPropertyMetadata によって定義される IsAnimationProhibited プロパティを使用して、通常はアニメーションの主な対象であるこれらのプロパティにフラグを設定しました。

3D テキストを生成するために私が作成したすべてのクラスにおいて、3D 空間でのテキストの位置を示すプロパティは Text3DBase によって定義される Origin プロパティしかありません。このプロパティの型は、2 次元での場所を示す Point です。3D 空間でのテキストの正確な位置を定めるプロパティの拡張セットを定義することも考えました。このプロパティには、3D テキストの原点だけでなく、ベースラインの方向を示す 3D ベクトルと、垂直方向を示すもう 1 つの 3D ベクトルが含まれている必要があります。そこで、代わりに Origin プロパティを使用して 3D 空間の XY 平面におけるテキストの位置を指定し、その他すべての位置指定を変換で実行することにしました。このアプローチの大きな利点は、数学的作業が簡単になることです。

Text3DBase によって定義される FontSize プロパティがフォントの em サイズを示し、このサイズがフォント文字の全体的な高さに大まかに関係します。3 次元ではフォントにも深さがあります。DeepTextBase クラスは、Text3DBase から派生し、Depth プロパティを定義するためにのみ存在します (クラス階層がこのように拡張された理由はいずれおわかりになるでしょう。前に述べたクラスをプログラミングしている間にかなりのリファクタリングが発生し、現在の構造になりました)。テキストの位置は XY 平面上で設定されるため、負の Z 軸に沿ってテキストの深さを記述する Depth プロパティを考え出しました。

ここまでは、どのクラスも実処理を行っていません。図 3 に示す GeometryTextBase 抽象クラスは、DeepTextBase から派生したもので、ModelVisualBase によって定義される Triangulate メソッドをオーバーライドします。GeometryTextBase クラスは、Geometry クラスの GetFlattenedPathGeometry メソッドを利用して、TextGeometry プロパティとして提供された Geometry オブジェクトを、テキスト アウトラインを構成する閉じた図形に対応する複数の連結されたポリラインに変換します。それぞれの閉じた図形に対し、次のように GeometryTextBase クラスが抽象メソッドを呼び出しす。

Figure 3 GeometryTExtBase クラス (Close Figure 3)
public abstract class GeometryTextBase : DeepTextBase
{
    // Field prevent re-allocations during mesh generation.
    CircularList<Point> list = new CircularList<Point>();

    protected override void Triangulate(
        DependencyPropertyChangedEventArgs args,
        Point3DCollection vertices, Vector3DCollection normals,
        Int32Collection indices, PointCollection textures)
    {
        // Clear all four collections.
        vertices.Clear();
        normals.Clear();
        indices.Clear();
        textures.Clear();

        // Convert TextGeometry to series of closed polylines.
        PathGeometry path =
            TextGeometry.GetFlattenedPathGeometry(0.001,
            ToleranceType.Relative);

        foreach (PathFigure fig in path.Figures)
        {
            list.Clear();
            list.Add(fig.StartPoint);

            foreach (PathSegment seg in fig.Segments)
            {
                if (seg is LineSegment)
                {
                    LineSegment lineseg = seg as LineSegment;
                    list.Add(lineseg.Point);
                }
                else if (seg is PolyLineSegment)
                {
                    PolyLineSegment polyline = seg as PolyLineSegment;

                    for (int i = 0; i < polyline.Points.Count; i++)
                        list.Add(polyline.Points[i]);
                }
            }

            // Figure is complete. Post-processing follows.
            if (list.Count > 0)
            {
                // Remove last point if it's the same as the first.
                if (list[0] == list[list.Count - 1])
                    list.RemoveAt(list.Count - 1);

                // Convert points to Y increasing up.
                for (int i = 0; i < list.Count; i++)
                {
                    Point pt = list[i];
                    pt.Y = 2 * Origin.Y - pt.Y;
                    list[i] = pt;
                }

                // For each figure, process the points.
                ProcessFigure(list, vertices, normals,
                    indices, textures);
            }
        }
    }

    // Abstract method to convert figure to mesh geometry.
    protected abstract void ProcessFigure(CircularList<Point> list,
        Point3DCollection vertices, Vector3DCollection normals,
        Int32Collection indices, PointCollection textures);
}

protected abstract void ProcessFigure(
    CircularList<Point> list, Point3DCollection vertices,
    Vector3DCollection normals, Int32Collection indices,
    PointCollection textures);

最初の引数は、図形における 2 次元のポイントのコレクションです。CircularList は、私が定義したコレクション クラスで、循環バッファのように機能します。インデックスによってメンバがアクセスされると、このオブジェクトがインデックスを適切な範囲に正規化します。つまり、-1 のインデックスはコレクションの最後のメンバにアクセスし、list.Count のインデックスはコレクションの最初のメンバにアクセスします。コレクションには、閉じたポリライン内のすべてのポイントが含まれています。

ProcessFigure のその他の引数は、MeshGeometry3D オブジェクトの Positions、Normals、TriangleIndices、TextureCoordinates の各コレクションに対応します。ProcessFigure メソッドを実装するクラスは、CircularList コレクション内のポイントに基づいて、少なくとも頂点およびインデックスのコレクションにデータを挿入する必要があります。

RibbonText と SliverText

実際に 3 次元テキストを生成するクラスとして私が最初に作成したのは、図 4 に示す RibbonText です。この図からわかるように、このクラスは GeometryTextBase から派生し、ProcessFigure メソッドの実装だけで構成されます。このクラスが生成する Point3D オブジェクトは、CircularList コレクション内の 2D ポイントおよび Depth プロパティだけに基づいています。このポイントは、XY 平面 (Z がゼロ) 上のポイントと XY 平面の背後にある Depth ユニットを交互に配置したものです。インデックス コレクションは、この 2 組のポイントを連結して一連の三角形を定義します。

Figure 4 RibbonText クラス (Close Figure 4)
public class RibbonText : GeometryTextBase
{
    protected override void ProcessFigure(CircularList<Point> list,
        Point3DCollection vertices, Vector3DCollection normals,
        Int32Collection indices, PointCollection textures)
    {
        int offset = vertices.Count;

        for (int i = 0; i <= list.Count; i++)
        {
            Point pt = list[i];

            // Set vertices.
            vertices.Add(new Point3D(pt.X, pt.Y, 0));
            vertices.Add(new Point3D(pt.X, pt.Y, -Depth));

            // Set texture coordinates.
            textures.Add(new Point((double)i / list.Count, 0));
            textures.Add(new Point((double)i / list.Count, 1));

            // Set triangle indices.
            if (i < list.Count)
            {
                indices.Add(offset + i * 2 + 0);
                indices.Add(offset + i * 2 + 2);
                indices.Add(offset + i * 2 + 1);

                indices.Add(offset + i * 2 + 1);
                indices.Add(offset + i * 2 + 2);
                indices.Add(offset + i * 2 + 3);
            }
        }
    }
}

ProcessFigure メソッドは、特定のテキスト文字列に対して複数回呼び出されることに注意してください。最初に MeshGeometry3D コレクションをクリアするのは GeometryTextBase の Triangulate メソッドです。ProcessFigure クラスは、offset という整数を使用して、頂点コレクションに追加する新しいポイントのインデックスを決定します。

図 5 は、RibbonText クラスを使用する小さな XAML ファイルを示し、図 6 は実際にどのように表示されるかを示しています。ある意味では、その変わった形状のために、通常の 3D ブロック テキストよりも少し手が込んでいるように見えます。しかしアルゴリズム上は、考えられる最も単純な形式の 3D テキストです。

Figure 5 RibbonTextDemo.xaml (Close Figure 5)
<!-- RibbonTextDemo.xaml by Charles Petzold, June 2007 -->
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:src="clr-namespace:Petzold.Text3D;assembly=Petzold.Text3D"
      WindowTitle="RibbonText Demo"
      Title="RibbonText Demo">

    <Viewport3D>
        <src:RibbonText Text="Ribbon"
                FontFamily="Times New Roman" Depth="2">
            <src:RibbonText.Material>
                <DiffuseMaterial Brush="Cyan" />
            </src:RibbonText.Material>

            <src:RibbonText.BackMaterial>
                <DiffuseMaterial Brush="Pink" />
            </src:RibbonText.BackMaterial>
        </src:RibbonText>

        <!-- Lights. -->
        <ModelVisual3D>
            <ModelVisual3D.Content>
                <Model3DGroup>
                    <AmbientLight Color="#404040" />
                    <DirectionalLight Color="#C0C0C0"
                        Direction="2 -3 -1" />
                </Model3DGroup>
            </ModelVisual3D.Content>
        </ModelVisual3D>

        <!-- Camera. -->
        <Viewport3D.Camera>
            <PerspectiveCamera Position="-3 0 8" UpDirection="0 1 0"
                               LookDirection="1 0 -2" FieldOfView="45" />
        </Viewport3D.Camera>
    </Viewport3D>
</Page>
図 6 RibbonTextDemo の表示
図 6 RibbonTextDemo の表示

実際には文字は白抜きです。テキストを正面から見ると、中を見通すことができます。斜めから表示されているので、外側と内側の色が異なっているのがわかるのです。Material ブラシ (RibbonTextDemo.xaml では Cyan に設定) が文字の外側を着色し、BackMaterial ブラシ (Pink に設定) が内側を着色しています。ただし、フォントによってはこの 2 色が反転する可能性があります。これは、文字アウトラインを定義しているポイントの方向によって決まります。あるいは、同じフォント内でも文字によってこの方向が異なる可能性があります。

RibbonText クラスは、TextureCoordinates コレクションのポイントを定義するため、単色ブラシに限定されることはありません。TextureCoordinates コレクションのポイントの Y 座標は、リボンの前面部分が 0、背面部分が 1 となっており、X 座標は CircularList コレクションのポイントのインデックスに基づきます。このテキストに使用する非単色ブラシは、各リボンに個別に適用されます。最も予測可能な結果になるのは、開始座標と終了座標を (0, 0) および (0, 1) として LinearGradientBrush を使用した場合です。その他のブラシでは、テキスト文字の予期しない場所に継ぎ目が見えることがあります。

RibbonText よりもかなり難しかったのが、SliverText と名付けたクラスです。このクラスは、同じくテキスト文字のアウトラインに的を絞っていますが、SliverWidth プロパティとして定義したゼロでない幅をアウトラインに割り当てます。図 7 は、FontSize を既定値の 1、Depth を既定値の 1、および SliverWidth を既定値の 0.05 にそれぞれ設定して SliverTextDemo プログラムを実行した結果を示しています。SliverText の TextureCoordinates をポイントの X 座標と Y 座標に基づいて定義することで、テキストの前面にわたってブラシを変化させられるようにしました。この例ではグラデーション ブラシを使用しています。文字の先端がやや丸みを帯びているのは、エッジの座標を上面の三角形と側面の三角形が共有しているためです。WPF 3D は、平均値に基づいて法線を計算します (法線は光の反射を制御します)。

図 7 SliverTextDemo の表示
図 7 SliverTextDemo の表示

SliverWidth に設定した値が FontSize と比較して高すぎると、文字が互いに重なり合うようになります。その後に変換によってテキストをアニメーション化すると、各表面が前面の位置を争うために見苦しいちらつきが生じる場合があります (技術用語ではこの現象を "Z ファイティング" と呼びます)。

SliverText クラスでは、2 次元の解析幾何学を少し利用してみました。個々の文字アウトラインは 2 次元のポリラインですが、SliverText には 2 組の平行ポリラインが必要です。Geometry によって定義される GetWidenedPathGeometry メソッドの使用も考えましたが、パスを拡張すると、絶対に避けたいアーティファクトが頻繁に生成されることが経験上わかっていました。

そこで、パスは自分で拡張しました。この作業では、ポリラインの個々の線に対して平行な線を計算する必要があります。ただし、この平行線は、連続性と形状を基準のパスと同じにするために、短縮または延長しなければならないことがよくあります。ここで役立ったのが、Line2D という自作の小さな構造体です。この構造体は、Line2D オブジェクトおよびベクトルの追加を、線のシフトとして定義します。2 つの Line2D オブジェクトを乗算することで、交点を示す Point オブジェクトが返されます。

ブレークスルーとなった SolidText

最初に RibbonText と SliverText を作成してから何か月も経ち、3D テキストへの取り組みもこれが限界かと思われました。テキスト文字の表面の三角形化に関する問題に戻ることもありましたが、厄介な数学を使わずに対処する方法は見つかりませんでした。

3D の平面にテキスト文字列を配置する方法はもちろん知っていました。必要なのは TextBlock 要素に基づいた VisualBrush だけです。この平面を RibbonText オブジェクトの上にオーバーレイし、ソリッド テキストの幻影を作り出すことも確かに考えました。しかし、ラスタライズされたテキスト (TextBlock が表示するテキスト) と幾何学的なアウトラインから構築されたテキストを組み合わせると問題が発生することが経験上わかっていました。この 2 つのアルゴリズムは視覚的に適合しないのです。

事態が進展したのは、3D 空間の平面を、RibbonText で使用されているものと同じジオメトリに基づいた DrawingBrush でカバーすればいいのではないかと気付いたときでした。概念は明確でしたが、実装はそうではありませんでした。

この DrawingBrush を生成するクラスに PlanarText という名前を付けました。このクラスは、Text3DBase から派生します。PlanarText は、Text3DBase が生成する TextGeometry プロパティを必要としますが、DeepTextBase によって定義される Depth プロパティは必要としません。これは、(その名前が示すように) PlanarText が平面上にテキストを表示するためです。ただし、PlanarText クラスは、テキストの平面が XY 平面と平行に位置する場所を示す Z というプロパティを定義します。

PlanarText は、2 つの三角形で構成されるフラットな四角形を定義することで、Triangulate メソッドを実装します。この四角形の四隅の座標は、TextGeometry プロパティから取得した Geometry オブジェクトに関連付けられている Bounds プロパティから取得します。

PlanarText が ModelVisualBase から派生する他のクラスと明らかに異なるのは、Material プロパティと BackMaterial プロパティの扱い方です。

おかしなことに、ModelVisualBase が最初に考え出されたときに、Material プロパティと BackMaterial プロパティは邪魔な存在と見なされていました。ModelVisualBase は ModelVisual3D から派生しますが、ModelVisual3D は Material プロパティと BackMaterial プロパティを定義しません。Material と BackMaterial を定義する WPF 3D クラスは、実際は GeometryModel3D であり、そのインスタンスを ModelVisualBase が自身の Content プロパティとして内部的に格納します。

ModelVisualBase は Material プロパティと BackMaterial プロパティを定義する必要があるので、次のようにマークアップの派生クラス内で両プロパティを設定できます。

<src:RibbonText Text="Ribbon">
    <src:RibbonText.Material>
        <DiffuseMaterial Brush="Cyan" />
    </src:RibbonText.Material>

    <src:RibbonText.BackMaterial>
        <DiffuseMaterial Brush="Pink" />
    </src:RibbonText.BackMaterial>
</src:RibbonText>

ModelVisualBase が定義する Material プロパティと BackMaterial プロパティには、MaterialPropertyChanged というプロパティ変更ハンドラが関連付けられます。このハンドラは、両プロパティに割り当てられているオブジェクトを、内部の GeometryModel3D オブジェクトの同一のプロパティに転送します。

しかし、PlanarText は別の作業を行う必要があります。PlanarText の Material プロパティが、たとえば DiffuseMaterial オブジェクトに設定されると、PlanarText はその DiffuseMaterial に関連付けられている Brush オブジェクトを抽出し、この既存のブラシと TextGeometry プロパティを基に新しい DrawingBrush オブジェクトを作成する必要があります。

new DrawingBrush(new GeometryDrawing(brush, null, TextGeometry))

この DrawingBrush は新しい DiffuseMaterial オブジェクトのベースとなり、PlanarText はこのオブジェクトを内部の GeometryModel3D オブジェクトの Material プロパティに設定します。

PlanarText の Material プロパティに割り当てられているオブジェクトが DiffuseMaterial であれば、このプロセスはかなり簡単であると思われます。しかし、Material プロパティには MaterialGroup オブジェクトが割り当てられている可能性があり、この MaterialGroup オブジェクトに DiffuseMaterial 型、SpecularMaterial 型、または EmissiveMaterial 型の子が存在する場合や、別の MaterialGroup がネストされている場合があります。一般的なケースでは、PlanarText クラスは Material オブジェクトの平行構造を作成して内部の GeometryModel3D オブジェクトに割り当てる必要があり、それらの Material オブジェクトに関連付けられている各 DrawingBrush を、PlanarText に関連付けられている Material オブジェクトの Brush オブジェクトから計算する必要があります。

この Material 転送ロジックのために、図 8 のように結果がそれほど複雑でなくても、PlanarText.cs は Text3D ライブラリで最長のファイルになることがわかりました。

図 8 PlanarTextDemo の表示
図 8 PlanarTextDemo の表示

PlanarText クラスにはまだ完全に満足していません。たとえば、テキストのサイズを変更する場合は、PlanarText でブラシを作成し直す必要があります。このクラスは、そのパブリック プロパティである Material と BackMaterial の構造が、内部の GeometryModel3D オブジェクトの同じプロパティの構造と同一であれば、一連の Material オブジェクトを再作成しません。ところが、Material オブジェクトに関連付けられているいずれかのブラシが ColorAnimation などを通じてアニメーション化される場合は、PlanarText で 1 回ごとに GeometryDrawing オブジェクトと DrawingBrush オブジェクトを再作成せざるを得なくなり、コストが高くなる可能性があります。

PlanarText 自体はそれほど重要ではありませんが、SolidText クラスを実現する手段となります。図 9 に示す SolidText は、DeepTextBase から派生します。つまり、このクラスには Text3DBase によって定義されるすべてのテキスト関連プロパティと、Depth プロパティが定義されています。ただし、SolidText はそれ自体で三角形メッシュを生成することはありません。Triangulate メソッドをオーバーライドしますが、単にそのメソッドから戻るだけです。

Figure 9 SolidText クラス (Close Figure 9)
public class SolidText : DeepTextBase
{
    public SolidText()
    {
        // Create RibbonText and two PlanarText children.
        RibbonText ribbon = new RibbonText();
        ribbon.Depth = Depth;
        Children.Add(ribbon);

        PlanarText planar = new PlanarText();
        Children.Add(planar);

        planar = new PlanarText();
        planar.Z = -Depth;
        Children.Add(planar);
    }

    // SideMaterial dependency property and property.
    public static readonly DependencyProperty SideMaterialProperty =
        DependencyProperty.Register("SideMaterial",
            typeof(Material), typeof(SolidText),
            new PropertyMetadata(SideMaterialChanged));

    public Material SideMaterial
    {
        set { SetValue(SideMaterialProperty, value); }
        get { return (Material)GetValue(SideMaterialProperty); }
    }

    // SideMaterialChanged handlers.
    static void SideMaterialChanged(DependencyObject obj,
        DependencyPropertyChangedEventArgs args)
    {
        ((SolidText)obj).SideMaterialChanged(args);
    }

    void SideMaterialChanged(DependencyPropertyChangedEventArgs args)
    {
        // Transfer SideMaterial to RibbonText.
        Text3DBase txtbase = Children[0] as Text3DBase;
        txtbase.Material = args.NewValue as Material;
        txtbase.BackMaterial = args.NewValue as Material;
    }

    // MaterialChanged override.
    protected override void MaterialPropertyChanged(
        DependencyPropertyChangedEventArgs args)
    {
        // Transfer Material and BackMaterial properties to PlanarText.
        if (args.Property == MaterialProperty)
            ((PlanarText )Children[1] ).Material =
                                    args.NewValue as Material;

        else if (args.Property == BackMaterialProperty)
            ((PlanarText)Children[2]).BackMaterial =
                                   args.NewValue as Material;
    }

    // TextPropertyChanged override.
    protected override void TextPropertyChanged(
        DependencyPropertyChangedEventArgs args)
    {
        base.TextPropertyChanged(args);

        // Transfer text-related property to all three children.
        for (int i = 0; i < 3; i++)
        {
            Text3DBase txtbase = Children[i] as Text3DBase;
            txtbase.SetValue(args.Property, args.NewValue);
        }
    }

    // PropertyChanged override.
    protected override void PropertyChanged(
        DependencyPropertyChangedEventArgs args)
    {
        if (args.Property == DepthProperty)
        {
            // Set Depth property to RibbonText and PlanarText children.
            double depth = (double)args.NewValue;
            ((DeepTextBase )Children[0]).Depth = depth;
            ((PlanarText)Children[2]).Z = -depth;
        }

        base.PropertyChanged(args);
    }

    // Move on, move on. Nothing to see here.
    protected override void Triangulate(
        DependencyPropertyChangedEventArgs args,
        Point3DCollection vertices, Vector3DCollection normals,
        Int32Collection indices, PointCollection textures)
    {}
}

代わりに SolidText は、ModelVisual3D から継承した Children プロパティを利用します。SolidText は、ModelVisual3D 型の子を持つことができ、したがって RibbonText 型と PlanarText 型の子も持つことができます。SolidText に適用される変換は、そのすべての子にも適用されます。さらに SolidText は、子の RibbonText に適用する SideMaterial プロパティを定義します。SolidText の主な役目は、自身に設定されたプロパティをそのすべての子に適用することです。

SolidTextDemo プログラムは、図 1 で示したように SolidText オブジェクトを回転させます。PlanarText の Material 転送ロジックを実際に試すために、この図形にはグラデーション ブラシと、回転中に光を受ける反射素材を適用しました。

文字に膨らみを持たせる

RibbonText と PlanarText の図形を結合して 1 つのテキスト ブロックにするという基本的な問題を解決した後に、表面を少し丸めてみることにしました。EllipticalText クラスは SliverText に似ていますが、テキスト文字のアウトライン上にある各ポイントが四角形ではなく楕円になる点が異なります。EllipticalText クラスは SliverText よりも若干短くなりました。

RoundedText は SolidText から派生し、自身のコンストラクタを使用して、SolidText が作成した RibbonText 子オブジェクトを EllipticalText オブジェクトで置換します。図 10 からわかるように、この組み合わせによって、テキストが PlanarText オブジェクトのエッジの部分でやや丸みを帯び、ブロックの中央がわずかに隆起します。

図 10 ソリッド 3D テキスト
図 10 ソリッド 3D テキスト

丸み付けの程度は、EllipticalText と RoundedText 両方のプロパティである EllipseWidth で制御されます (既定値は 0.05)。このプロパティの設定値が TextSize プロパティの一部としては大きすぎると、文字が互いに重なり合い、アニメーション化の際に Z ファイティングが発生します。

いずれは、アウトライン フォントをより柔軟に三角形化するアルゴリズムを解明したいと考えています。それまでは、今回取り上げたクラスで対応する必要があります。3D テキスト効果を組み込んだフライング ロゴはまだまだ少ないようなので、この記事での私のささやかな貢献が、その不足を少しでも解消することになればさいわいです。

ご意見やご質問は、

Charles cp@charlespetzold.com.

まで英語でお送りください。
Charles Petzold は、MSDN Magazine の寄稿編集者です。最新の著書には『3D Programming for Windows』 (Microsoft Press、2007 年) があります。


Windows と C++: タスク スケジューラ 2.0

Windows と C++
タスク スケジューラ 2.0
Kenny Kerr

コードのダウンロード : : WindowsWithC++2007_10.exe (156KB)
コンテンツ
タスク サービスと記憶域
タスク サービスを使用する
タスク定義
タスク アクション
タスク トリガ
次のステップ

Windows Vista® では、タスク スケジューラが大きく見直されました。類似点もいくつかありますが、Windows® 98 以来使用されてきた元のタスク スケジューラに比べて、新しいタスク スケジューラ (タスク スケジューラ 2.0) ははるかに強力です。単なるエンド ユーザー向けの簡単なツールから、複雑なバックグラウンド操作を設計および管理するための強力なプラットフォームへと進化を遂げました。このため、多くの場合に Windows サービスを開発する必要がなくなります。

たとえば、プロジェクトで更新を自動的に確認する必要があるものとします。この場合、バックグラウンドで稼働して数日ごとに更新を確認するような Windows サービスを記述することが考えられます。ただし、常に実行し続けるサービスの代わりに、数日ごとに実行されて更新を確認した後に停止するようなスケジュール タスクを設計することもできます。さらには、更新を実行するユーザーがいないときにリソースが浪費されないよう、ユーザーがログインしている間にのみ実行されるようにすることもできます。

これからは新しいタスク スケジューラの時代であるとマイクロソフトの開発者たちが考えていることは、Windows Vista のあちこちでバックグラウンド タスクの管理にこれが使用されている事実からも明らかです。副次的効果として、こうしたタスクすべてを処理するために新しく作成しなければならないサービスの数が減少します。また、ブラックボックス的な Windows サービスの目的を解明するよりは、タスクを調べてその動作を確認するほうがはるかに簡単なため、Windows の管理と診断も簡素化されます。Windows 自体の一部を成すタスクにとって、タスク スケジューラはタスク ホストとしても機能します。この場合、タスク固有の多数のプロセスが 1 つに統合されるため、それらの操作をホストするために必要なリソースがさらに減少します。

このすばらしい新サービスに読者が一刻も早く習熟して活用できるよう、このコラムではタスク スケジューラの主要概念と構成要素について説明します。

タスク サービスと記憶域

まず取り上げるのは、タスク スケジューラ サービス、つまりタスクを実際にスケジュールするサービスです。タスク スケジューラではファイル システムを使用してタスク情報を保存するため、このサービスを利用しなくてもタスクの列挙や準備が可能です。しかし、これを利用せずにできることには限りがあります。実際、タスク スケジューラの今後のバージョンでは保存の形式と場所が変更されると予想されるため、現在のそれに依存することはお勧めしません。現時点でも、ファイル システムに直接書き込まれたタスクはタスク スケジューラによって認識されない可能性があるため、また、改ざんされたタスクの実行が拒否される可能性が高いため、記憶域への直接アクセスに依存しないようにすることが重要です。タスク スケジューラは既に Windows Vista の不可欠な一部であり、管理者が単純にこれを無効化することはできなくなりました。つまり、バックグラウンド操作の実行とタスク記憶域の抽出に利用しても安全だということです。これは開発者にとって朗報です。

タスクは原則として、社名、製品名、およびコンポーネント名によってグループ化されます。たとえば、Windows のディスク デフラグ ツールでは、タスクが \Microsoft\Windows\Defrag サブフォルダに保存されます。各タスクは、タスク スケジューラ スキーマで定義された XML ドキュメントとして保存されます。このため、XmlLite などの XML パーサーを使用してタスクを直接作成したり (私の以前の MSDN® Magazine 記事 msdn.microsoft.com/msdnmag/issues/07/04/Xml/ を参照)、タスク スケジューラ API が提供するさまざまな COM インターフェイスを使用したりできます。COM インターフェイスを使用すると、タスクの XML 定義を直接提供したり照会したりもできます。そうすることにより、実際の記憶域にアクセスすることなく、XML を使用してタスクの作成と照会を行うメリットが得られます。タスク スケジューラにはリモートからアクセスすることもできます。

タスク サービスを使用する

ITaskService インターフェイスは、タスク スケジューラ API へのゲートウェイです。必要な定義は、すべて taskschd.h ヘッダー ファイル内にあります。もちろん、Windows Vista に対応した最新の Windows SDK も必要です。まず、次のようにローカルのインプロセス インスタンスを作成します。

CComPtr<ITaskService> service;
HR(service.CoCreateInstance(__uuidof(TaskScheduler)));

(これらのサンプルでは、確認を必要とする HRESULT がメソッドから返される場所を明確に特定するために、HR マクロを使用しています。これを、例外をスローするか HRESULT を返すようなエラー処理に置き換えてもかまいません。)

次に、Connect メソッドを呼び出して、任意のコンピュータ上にあるタスク スケジューラへの接続を確立します。Connect メソッドは次のように定義されています。

HRESULT Connect(
    VARIANT computer, VARIANT user,
    VARIANT domain, VARIANT password);

VARIANT 型を使用しているのが残念ですが、便利な Active Template Library (ATL) の CComVariant 派生クラスを使用すれば処理は簡単です。ローカル コンピュータに接続するには、コンピュータ名を省略します。呼び出し元の ID または実効トークンを使用して接続するには、ユーザー名の指定を省略します。ローカル コンピュータの現在のドメインを使用するには、ドメイン名の指定を省略します。最後に、ユーザー名を指定した場合はパスワードだけを指定します。よって、ローカル コンピュータのタスク スケジューラ サービスに接続する場合、次のような最も簡単な形式で Connect メソッドを呼び出します。

HR(service->Connect(
    CComVariant(),   // local computer
    CComVariant(),   // current user
    CComVariant(),   // current domain
    CComVariant())); // no password

これで、タスクの作成、既存のフォルダやタスクの列挙など、さまざまな操作を実行できるようになりました。タスクはフォルダに格納しなければ使用できないため、まず ITaskFolder インターフェイスについて説明します。ITaskService の GetFolder メソッドからは、指定したフォルダに対する ITaskFolder インターフェイス ポインタが返されます。次に示すように、バックスラッシュはルート タスク フォルダを表します。

CComPtr<ITaskFolder> folder;
HR(service->GetFolder(CComBSTR(L"\\"), &folder));

次に、アプリケーション用のフォルダを次のように作成できます。

CComPtr<ITaskFolder> newFolder;
HR(folder->CreateFolder(
    CComBSTR(L"Company\\Product"),
    CComVariant(), &newFolder));

CreateFolder に対する 2 つ目のオプション パラメータでは、新しく作成するファイル システム フォルダのセキュリティ記述子を指定します。省略した場合、フォルダには親のセキュリティ記述子が継承されます。セキュリティ記述子定義言語 (SDDL) を使用すると、既定のアクセス制御をオーバーライドできます。たとえば、ローカル システムと管理者とに完全な制御を許可するには、次のような SDDL を使用します。

HR(folder->CreateFolder(
    CComBSTR(L"Company\\Product"),
    CComVariant(L"D:(A;;FA;;;BA)(A;;FA;;;SY)"),
    &newFolder));

簡単に言うと、D: に続くアクセス制御エントリ (ACE) は、随意アクセス制御リスト (DACL) の一部となります。最初のエントリは、すべてのファイル (FA) へのアクセス許可 (A) が組み込みの管理者 (BA) に対して付与されることを意味します。2 番目のエントリは、同様のアクセス許可がローカル システム (SY) に対して付与されることを意味します。このセキュリティ記述子定義が使用されるのは、Product フォルダの作成時のみです。親の Company フォルダは、継承されたセキュリティ記述子を使用して作成されます。なお、Local System アカウントに対してアクセスを拒否しないようにしてください。この ID はタスク スケジューラ サービスによって使用されます。スケジュール対象のタスクにタスク スケジューラがアクセスできないと、予期しない動作が発生します。

フォルダが存在するようになったら、ITaskService の GetFolder メソッドを使用して、ルート タスク フォルダから相対指定したフォルダを開くことができます。ITaskFolder の同じメソッドを次のように使用すると、インターフェイス インスタンスによって表されるフォルダから相対指定したフォルダを開くことができます。

CComPtr<ITaskFolder> productFolder;
HR(service->GetFolder(
    CComBSTR(L"Company\\Product"),
    &productFolder));

CComPtr<ITaskFolder> componentFolder;
HR(productFolder->GetFolder(
    CComBSTR(L"Component"),
    &componentFolder));

このコラムのダウンロードに含まれている、タスク スケジューラ エクスプローラのサンプル アプリケーションを、図 1 に示します。これによってタスクを概観でき、タスク スケジューラ API を使い始める際に役立ちます。

図 1 タスク スケジューラ エクスプローラ
図 1 タスク スケジューラ エクスプローラ(画像を縮小するには、ここをクリックします)
図 1 タスク スケジューラ エクスプローラ
図 1 タスク スケジューラ エクスプローラ (画像を拡大するには、ここをクリックします)

タスク定義

タスクを作成するには、まず、タスク定義でタスクをモデル化する必要があります。タスク定義を構成するコンポーネントについて、図 2 で説明します。タスク定義は複雑であるうえ、タスク作成において必要とされるため、すぐにタスク作成に取りかかることはお勧めしません。基本的な構成要素について前もって理解しておくと役立ちます。アクションとトリガの定義の詳細を検討する前に、タスク定義の作成方法について簡単に説明します。

Figure 2 タスク定義のコンポーネント (Close Figure 2)

タスク | 定義
アクション | 1 つまたは複数のアクションで、タスクが実行する作業を定義します。
トリガ | 1 つまたは複数のトリガで、タスクをいつ開始するのかを指定します。
プリンシパル | 承認と監査は、このセキュリティ コンテキストに基づきます。
設定 | これらの設定で、タスクの実行時動作と制限を制御します。
データ | アクションで使用できるようにする文字列です。
RegistrationInfo | 管理用のブックキーピング情報です。

最初に、タスク スケジューラ サービスを使用して、設定に使用する空のタスク定義を作成します。

CComPtr<ITaskDefinition> definition;
HR(service->NewTask(0, // reserved
                    &definition));

タスク定義を特定のフォルダに登録する前に、少なくともアクションを 1 つ設定する必要があります。オプションで、トリガやその他の情報を設定することもできます。準備ができたら、RegisterTaskDefinition メソッドを次のように使用して、フォルダに登録します。

CComPtr<IRegisteredTask> registeredTask;
HR(folder->RegisterTaskDefinition(
    CComBSTR(L"Task"),
    definition,
    TASK_CREATE_OR_UPDATE,
    CComVariant(), // user name
    CComVariant(), // password
    TASK_LOGON_INTERACTIVE_TOKEN,
    CComVariant(), // sddl
    &registeredTask));

新しく登録されたタスクを表す IRegisteredTask インターフェイス ポインタが返されます。このインターフェイスを使用して、タスクの開始や停止のほか、タスクに関するランタイム情報の取得も行います。

RegisterTaskDefinition の最初のパラメータでは、新しいタスクの名前を指定します。この値は、フォルダに保存されるファイルの名前としても使用されるため、ファイル システムの名前付け規則に準拠する必要があります。このパラメータを NULL に設定すると、メソッドによって GUID が生成され、名前として使用されます。しかしこれは、コンピュータの管理者にとってわかりやすいとは言えないため、お勧めしません。2 番目のパラメータでは、作成するタスクを記述するタスク定義を指定します。これについては、以下のセクションで詳しく説明します。3 番目のパラメータでは、TASK_CREATION 列挙からの値を指定します。よく使用される値は、TASK_CREATE および TASK_UPDATE です。TASK_CREATE_OR_UPDATE は、これら 2 つのビット単位の OR です。

続く 3 つのパラメータでは、登録資格情報を指定します。RegisterTaskDefinition を使用すると、後続のパラメータで指定する DACL で明示的に付与しなくても、登録するタスクの少なくとも読み取りアクセス権が、指定したユーザーに必ず付与されます。タスク スケジューラは、何種類ものログオン方法をサポートしています。上の例で使用している TASK_LOGON_INTERACTIVE_TOKEN は、指定したユーザーの対話型ログオン セッションでのみタスクが実行されることを示します。ユーザー名とパスワードを指定しない場合、呼び出し元の ID が使用されます。対話型ログオン セッションを必要とするため、パスワードは保存されません。

代わりに TASK_LOGON_PASSWORD を使用すると、タスク用のバッチ ログオン セッションを作成するようタスク スケジューラに指示できます。この場合、ユーザー名とパスワードの両方を指定し、アカウントには "バッチ ジョブとしてログオン" 権限を付与する必要があります。

さらに安全性の高いオプションとして、TASK_LOGON_S4U があります。S4U (service for user) ログオンを利用し、指定したユーザーに代わってタスクを実行しますが、パスワードの保存は不要です。ローカル システム アカウント内でタスク スケジューラが実行されるため、S4U ログオン セッションを作成し、ID として使用されるだけでなくローカル コンピュータにおける偽装にも使用されるトークンを受け取ることができます。通常、S4U トークンは ID としてのみ有効です。

タスクには、登録時に指定した資格情報に加え、セキュリティ コンテキストに対する制御が IPrincipal インターフェイスによってさらに提供されます。具体的に言うと、IPrincipal は、タスク スケジューラ エンジンをホストするプロセスの実行レベルを、タスクに合わせて制御するための便利な方法です。まず、関連付けられたプリンシパル オブジェクトのタスク定義を照会した後、RunLevel プロパティを必要に応じて設定します。これは、タスク定義の登録前に行う必要があります。次に例を示します。

CComPtr<IPrincipal> principal;
HR(definition->get_Principal(&principal));
HR(principal->put_RunLevel(TASK_RUNLEVEL_HIGHEST));

TASK_RUNLEVEL_HIGHEST は、そのユーザーにとって最も制約が緩いセキュリティ コンテキストでタスクを実行することを表します。ユーザーが管理者である場合、ユーザー アカウント制御 (UAC) の制約を受けないトークンがプロセスに与えられます。代わりに TASK_RUNLEVEL_LUA を使用すると、制限された (昇格されない) トークンが与えられます。

タスク アクション
タスク スケジューラ .NET

タスク スケジューラ用の Microsoft® .NET Framework ラッパーはありませんが、共通言語ランタイム (CLR) のランタイム呼び出し可能ラッパー (RCW) 機能のおかげで、ラッパーを使用することは難しくありません。

タスク スケジューラには、スクリプト作成に役立つ COM インターフェイスが用意されているため、API をマネージ コードから簡単に使用できます。Visual Studio® の [参照の追加] ダイアログ ボックスを使用して、タイプ ライブラリを参照するだけです。わかりにくいのですが、タスク スケジューラ 2.0 のタイプ ライブラリには、TaskScheduler1.1 タイプ ライブラリという名前が付いています。ともあれ、Visual Studio によって作成される相互運用アセンブリが、すべてのマネージ定義を COM インターフェイスに提供します。このため、RCW を使用することにより、マネージ コードで直接、さまざまな COM オブジェクトを構築および参照できます。

このコラムの最初のセクションで使用したコード サンプルに相当する、C# の例を次に示します。タスク スケジューラ サービスに接続し、製品のタスク用にフォルダを作成するコードです。

ITaskService service = new TaskSchedulerClass();
service.Connect(null, // local computer
                null, // current user
                null, // current domain
                null); // no password

ITaskFolder folder = service.GetFolder("\\");
ITaskFolder newFolder = folder.CreateFolder(
    "Company\\Product", "D:(A;;FA;;;BA)(A;;FA;;;SY)");

この後も、.NET Framework を対象とする好みの言語を利用して、タスク定義、アクション、トリガなどの作成に進むことができます。

基礎的な説明が済んだので、タスク スケジューラに用意されているさまざまなアクションの種類を見てみましょう。アクションの作成とタスク定義への追加には、タスク定義の Actions プロパティから返される IActionCollection インターフェイス ポインタを使用します。

CComPtr<ITaskDefinition> definition;
HR(service->NewTask(0, // reserved
                    &definition));

CComPtr<IActionCollection> actions;
HR(definition->get_Actions(&actions));

IActionCollection が提供する Create メソッドは、新しいアクション オブジェクトを作成してコレクションに追加します。アクション自体は、IAction から派生するインターフェイスを通じて公開されます。次に例を示します。

CComPtr<IAction> action;
HR(actions->Create(TASK_ACTION_EXEC, &action));

TASK_ACTION_EXEC は、コマンドライン操作を記述し、あらゆるプログラムの起動に使用できるため、最もよく使用される種類のアクションです。Windows シェルに登録されたドキュメントやその他の種類のファイルを起動することもできます。アクション固有のプロパティを設定するには、まず、その種類に固有のインターフェイスを照会する必要があります。CComQIPtr は、この動作だけをうまくやってのけます。

CComQIPtr<IExecAction> execAction(action);

IExecAction インターフェイス ポインタを取得したら、任意のコマンドを実行するアクションを構成できます。次の例では、仮想ハード ドライブ イメージを最適化する Sysinternals Contig ツールを起動します。

HR(execAction->put_Path(CComBSTR(L"g:\\Tools\\contig.exe")));
HR(execAction->put_Arguments(CComBSTR(L"-s g:\\VirtualMachines")));

コマンドの実行は、管理者にとっては便利ですが、開発者にとって最適な解決策であるとは限りません。コマンドは、タスク内でどのようなアクションを意味するのかを認識しないばかりか、スケジュールされたタスクの一部であることすら関知しないからです。さいわい、タスク スケジューラには、まさにこの問題に対処するために設計された、別の種類のアクションが用意されています。そのアクションの種類 TASK_ACTION_COM_HANDLER は、COM サーバーを作成し、実装が必要な ITaskHandler インターフェイスをこのサーバー上で照会します。このため、アクションとタスク スケジューラ エンジンの間に通信チャネルが確立され、アプリケーション固有の操作がより明快にタスク スケジューラに統合されます。

COM アクションの作成方法を次に示します。

CComPtr<IAction> action;
HR(actions->Create(TASK_ACTION_COM_HANDLER, &action));

CComQIPtr<IComHandlerAction> comAction(action);
HR(comAction->put_ClassId(CComBSTR(
    L"{25C6DB11-4ADC-4e89-BA47-04576C7AA46A}")));

もちろん、指定したクラス識別子 (CLSID) で登録され ITaskHandler インターフェイスを実装する COM サーバーが必要です。図 3 に、COM クラスで ATL を利用して ITaskHandler を実装する例を示します。

Figure 3 COM アクション クラス (Close Figure 3)
class DECLSPEC_UUID("25C6DB11-4ADC-4e89-BA47-04576C7AA46A")
DECLSPEC_NOVTABLE CoSampleTask :
    public CComObjectRootEx<CComMultiThreadModel>,
    public CComCoClass<CoSampleTask, &__uuidof(CoSampleTask)>,
    public ITaskHandler
{
public:
    DECLARE_REGISTRY_RESOURCEID(IDR_SAMPLETASK)
    BEGIN_COM_MAP(CoSampleTask)
        COM_INTERFACE_ENTRY(ITaskHandler)
    END_COM_MAP()

private:
    STDMETHODIMP Start(IUnknown* taskScheduler, BSTR data);
    STDMETHODIMP Stop(HRESULT* actionResult);
    STDMETHODIMP Pause();
    STDMETHODIMP Resume();
};

もう 1 つ、追加の手順を実行する必要があります。これを執筆している時点ではどのドキュメントにも記載されていませんが、この手順を実行しないと COM アクションの読み込みに失敗します。タスク スケジューラ エンジンは、CoCreateInstance 関数に CLSID を指定して呼び出す際、CLSCTX_LOCAL_SERVER コンテキストを使用して COM サーバーを別個のプロセスに読み込もうとします (ただし、アクションが Windows 自体の一部である場合は、インプロセスで読み込まれることがあります)。これを成功させるには、代理プロセスでのアクティブ化を許可するよう COM サーバーを構成する必要があります。技術的には、従来のアウトプロセス コンポーネントを使用することもできますが、この方法は、多くの場合、一般的でも実際的でもありません。これを簡単に解決するには、登録のコードを更新します。ATL 登録を更新して DllSurrogate 値を含めます。次に例を示します。

HKCR
{
    NoRemove AppID
    {
        '%APPID%' = s 'SampleTask'
        {
            val DllSurrogate = s ''
        }
        'SampleTask.DLL'
        {
            val AppID = s '%APPID%'
        }
    }
}

COM アクションは、いくつもの点で、他の種類のアクションよりも優れています。特に、正常かつ予測可能な方法でアクションを停止できるのは、COM アクションだけです。COM クラスのインスタンスを作成した後、タスク スケジューラ エンジンは Start メソッドを呼び出し、タスク スケジューラとの通信に使用できるインターフェイス ポインタを提供します。次に Start メソッドで ITaskHandlerStatus インターフェイスを照会できます。これにより、タスクの進捗状況と最終的な完了をタスク スケジューラに通知する簡単なメカニズムが実現します。アクションが終了する方法は 2 つあります。結果として COM クラスの Stop メソッドが呼び出されるようなさまざまなメソッドを使用して、ユーザーがタスクを終了する場合があります。また、ITaskHandlerStatus の TaskCompleted メソッドを呼び出すことによって、COM クラスが処理の完了を報告する場合もあります。

タスクから電子メール メッセージを送信するための、便利なアクションの種類も用意されています。これまでに説明したアクションの種類とは異なり、操作や管理タスクの実行にはそれほど役立ちませんが、通知メカニズムとしての使用に最適です。次のセクションでトリガについて解説しますが、今のところは、従来のカレンダーベースや時間ベースのトリガ以外に、イベントによってもタスクの開始が可能であるとだけ申し上げておきましょう。電子メール アクションの作成方法を次に示します。

CComPtr<IAction> action;
HR(actions->Create(TASK_ACTION_SEND_EMAIL, &action));

CComQIPtr<IEmailAction> emailAction(action);
HR(emailAction->put_From(CComBSTR(L"kenny@example.com")));
HR(emailAction->put_To(CComBSTR(L"karin@example.com")));
HR(emailAction->put_Subject(CComBSTR(L"subject")));
HR(emailAction->put_Body(CComBSTR(L"body")));
HR(emailAction->put_Server(CComBSTR(L"mail.example.com")));

タスク トリガ

タスク スケジューラには、ユーザー操作なしにタスクを開始できる、さまざまなトリガが用意されています。通常の時間ベースやカレンダーベースのトリガのほか、便利なイベントベースのトリガがいくつもあります。

タスクの作成とタスク定義への追加には、タスク定義の Triggers プロパティから返される ITriggerCollection インターフェイス ポインタを使用します。

CComPtr<ITriggerCollection> triggers;
HR(definition->get_Triggers(&triggers));

ITriggerCollection が提供する Create メソッドは、新しいトリガ オブジェクトを作成してコレクションに追加します。トリガ自体は、ITrigger から派生するインターフェイスを通じて公開されます。前のセクションの例で、最適化操作を開始するトリガの例を次に示します。

CComPtr<ITrigger> trigger;
HR(triggers->Create(TASK_TRIGGER_WEEKLY, &trigger));

CComQIPtr<IWeeklyTrigger> weeklyTrigger(trigger);
HR(weeklyTrigger->put_StartBoundary(CComBSTR(
    L"2007-01-01T02:00:00-08:00")));
HR(weeklyTrigger->put_DaysOfWeek(0x01)) // Sunday

新しいタスク スケジューラの初期リリースでは、TASK_TRIGGER_WEEKLY を含む 11 種類のトリガを使用できます。使用可能なトリガの種類と、それぞれに関連付けられた COM インターフェイスを、図 4 にまとめて示します。StartBoundary プロパティは、タスクがトリガされる最も早い日付と、タスクが開始される時刻を表します。DaysOfWeek プロパティは、タスクを実行する曜日を表します。ビットマスクを使用して、任意の曜日を組み合わせることができます。上の例のトリガでは、2007 年の元日以降、毎週日曜日の午前 2 時 (太平洋標準時) にタスクが実行されます。

Figure 4 トリガの種類 (Close Figure 4)

種類 | インターフェイス | 用途とオプション
TASK_TRIGGER_EVENT | IEventTrigger | Windows イベント ログで定義されたイベント。
TASK_TRIGGER_TIME | ITimeTrigger | 時刻、ランダム遅延 (オプション)。
TASK_TRIGGER_DAILY | IDailyTrigger | n 日ごと、ランダム遅延 (オプション)。
TASK_TRIGGER_WEEKLY | IWeeklyTrigger | 指定した曜日、n 週ごと、ランダム遅延 (オプション)。
TASK_TRIGGER_MONTHLY | IMonthlyTrigger | 指定した日付、指定した月、月の最終日 (オプション)、ランダム遅延 (オプション)。
TASK_TRIGGER_MONTHLYDOW | IMonthlyDOWTrigger | 指定した曜日、月の第 n 週、指定した月、月の最終週 (オプション)、ランダム遅延 (オプション)。
TASK_TRIGGER_IDLE | IIdleTrigger | コンピュータのアイドル時。
TASK_TRIGGER_REGISTRATION | IRegistrationTrigger | タスクの作成時または更新時、遅延 (オプション)。
TASK_TRIGGER_BOOT | IBootTrigger | コンピュータの起動時、遅延 (オプション)。
TASK_TRIGGER_LOGON | ILogonTrigger | ユーザーのログオン時、遅延 (オプション)。
TASK_TRIGGER_SESSION_STATE_CHANGE | ISessionStateChangeTrigger | さまざまなセッション イベント。

次のステップ

Windows Vista 以降、新しいタスク スケジューラには、使用せずにいられない理由が数多くあります。Windows の以前のバージョンでタスク スケジューラを使用していた読者は、間違いなくすべての新機能に満足することでしょう。実際、今日のアプリケーション用に記述された多くのカスタムビルドのスケジューラが、間もなくタスク スケジューラに置き換わるものと予想されます。

タスク スケジューラの機能のすべてについては、このコラムで扱いきれませんでした。ぜひ、Windows SDK (msdn2.microsoft.com/en-us/library/aa383614.aspx) のドキュメントも参照してください。XML を直接使用するタスク作成、タスクが実行されるセキュリティ コンテキストに対する制御の強化、実行中のタスクの列挙と管理など、その他の機能について情報を得ることができます。

ご質問は、

Kenny mmwincpp.

まで英語でお送りください。
Kenny Kerr は、Windows のソフトウェア開発を専門にしているソフトウェア設計者です。彼はプログラミングおよびソフトウェア設計に関して執筆を行い、開発者を指導しています。連絡先は weblogs.asp.net/kennykerr です。


Netting C++: EEK! のエコシステムを記述する

Netting C++
EEK! のエコシステムを記述する
Stanley B. Lippman

コンテンツ
環境内の障害と報酬
世界をレイアウトする
まとめ

このコラムの前回の記事では、ネズミとネズミの生息環境を模した EEK! というシミュレーションの作成に着手し、環境 XML ファイルを解析する主要な方式としてファイアホースとドキュメント オブジェクト モデル (DOM) の 2 つを取り上げました。今月は XML ドキュメントの作成から始めます。その後、XML ファイルをプログラムに組み込むもう 1 つの方法として、.NET Foundation Class ライブラリの System::Data 名前空間にある DataSet を使用する方法を紹介します。DataSet では、一意のキー値でスキャンできる一連のリレーショナル テーブルとしてドキュメントを利用します。

まず第一に取り組むべき課題は、シミュレーション環境の記述を生成し、その世界をアプリケーションの一環としてプログラムで再現して操作できるようにすることです。そのためには、シミュレーション内のネズミの生息環境を表す XML ドキュメントをどのように設計するのが最適かを見極める必要があります。

前回作成した環境 (ご存知ない方は 2007 年 6 月のコラム msdn.microsoft.com/msdnmag/issues/07/06/NettingC をお読みください) には固有の制約があります。マクロとミクロの両方のスケールで表示できるようにして、上下にスライドさせてそれぞれの状態を同時に (つまりはシミュレータの操作 1 つで瞬時に) 調べられるようにしなければなりません。たとえば、ミクロ レベルでは、環境内の化学分子の動き (食べ物や猫などの各種実体の匂い) を追跡する必要があります。マクロ レベルでは、それを受けてネズミがどう進み、どのような動作をしたかを追跡する必要があります。ネズミの行動の大部分は化学分子によって決まり、化学分子は関連するプログラム機能のトリガとなります。これは、ネズミは人間とは異なり、視覚ではなく嗅覚によって環境を探索するためです。筆者はこれを SEM (Scalable Environment Map) と呼んでいます。

環境内の障害と報酬

シミュレーション環境自体は単なるマッピングですが、そこには障害と報酬が存在します。シミュレーション環境が表す連続領域の中をネズミが動き回り、巣 (nesting site)、食べ物 (food)、ギザギザ (burr)、電気ショック (e-shock)、匂い (scent) など、記憶に残る実体に遭遇します (図 1 を参照)。ネズミと環境のこうした遭遇は、文字どおり環境を彩ります。

図 1 ニュートラルな世界タイルの属性
図 1 ニュートラルな世界タイルの属性(画像を縮小するには、ここをクリックします)
図 1 ニュートラルな世界タイルの属性
図 1 ニュートラルな世界タイルの属性 (画像を拡大するには、ここをクリックします)

シミュレーションでは、ネズミの環境探索の経験は、パーソナル マップへの反映によって "記憶" されます。パーソナル マップは最初は空白ですが、環境の該当部分でネズミがどのような経験をしたか (ネズミとそこで見つかった実体との相互作用) を表す色が付いていき、類似パターンの認識によって環境の後続領域を識別して評価できるようになります。4 種類の評価に対して 5 つの色が関連付けられています。

緑と濃い青は、報われる経験 (食べ物や水、巣など) を示します。明るい青は恐れを表し、何か恐ろしい物 (このケースでは模擬的な電気ショック) に遭遇したことを示します。赤は怒りやイライラを表し、ネズミの目標探索を阻むなんらかの障害 (筆者が "ギザギザ" と呼んでいる実体) があることを示します。オレンジは、ニュートラルな経験を表します。

シミュレーション画面の 1 つのパネルは、環境が展開されるにつれて色付けされていく様子をリアルタイムで示します。この環境は意図的なものであるため、作成されるマップとネズミの目標到達能力は、ネズミの性質の構成値に基づいて予測することができます。

実際的な観点から言えば、これはシミュレーション内の動く (感覚を持つ) 実体ごとに 1 つの "事実" マップと 1 つの主観マップが存在することを意味します。事実マップは XML ドキュメントから作成され、シミュレーション環境の物理的特性を表します。それぞれの主観マップは探索によって作り上げられ、個体の経験によって色付けされます。主観マップは常に "事実" マップのサブセットになります。ネズミの動因となる第一の探索目標は、主観マップを通じて自分の縄張り (食べ物と水と巣があり、自分で守れる範囲) をマークすることです。シミュレーションに感覚を持つ実体を複数投入して縄張り争いを発生させるまでは、ギザギザと電気ショックが障害と制約になります。図 1 を見ると、環境の世界タイルがどのように実装されているかがわかります。

環境はタイル状に区切られ、ネズミが出入りできる 1 ～ 4 つの扉でつながっています。シミュレーションが始まる最初の部屋には、選ぶべき道を表す 3 つの扉があります。この部屋は、ネズミにとって "居心地の悪い" 部屋です。地面を覆う物はなく、全体が強い白色光にさらされています。パフォーマンス上の理由から、このシミュレーションでは、ネズミが現在探索しているタイルと、そのタイルに扉でつながっている各タイルのみを保持します。これがワーキング セットになります (図 2 を参照)。

図 2 メモリ内の世界タイル
図 2 メモリ内の世界タイル

世界をレイアウトする

XML ドキュメント内で表現する必要のある要素には、一連のタイル (tile)、タイル間をつなぐ扉 (doorto)、およびタイル内に現れる実体があります。

<element name="tile" ...
<element name="doorto" ...
<element name="burr" ...
<element name="e-shock" ...

最初の問題は、要素どうしを関連付ける方法を決めることです。それぞれの要素の属性は種類ごとに固有ですが、ネズミがそれ以前になんらかの事例に遭遇していれば、その性質が反映されています (ネズミが過去の経験の "記憶を呼び起こす" 要素に遭遇するたびに、その過去の経験の色がその新しい要素に移り、これによって環境に対するネズミの反応は時間の経過と共に速くなります)。

XML を構成する 1 つの方法は、それぞれの要素の記述を、その要素が現れるタイル内にネストすることです。図 1 で言えば、2 つのギザギザ、2 つの匂い、電気ショック、巣、そして食べ物と水がタイルの記述内に記述されることになります。この方法では、タイルの定義の解析によって、その内部にあるすべての実体の完全な記述を得ることができます。これには記述が複雑になるという面もあり、ドキュメント自体が人間には判読しにくいものになります。

今回のドキュメントで採用したのはこの方法ではなく、個別の実体をグループ化する方法です。まず、すべてのタイルをグループ化し、続いて扉やギザギザなどをグループ化しています。タイル内の実体はそれぞれ一意の ID とタイル内の場所で表されます。つまり、要素はキーで関連付けられた一連のテーブルとしてドキュメント内に記述されます。

要素を出現先のタイル内に定義しない 2 つ目の理由は、扉の二重籍とでも言うべき性質です。扉は 2 つのタイルをつなぐものであり、実際にはどちらのタイルにも属しません。これは、C++ のフレンド メカニズムを生む一因となった設計上の問題に似ています。Matrix クラスと Vector クラスを作成し、Matrix と Vector を掛け合わせる乗算演算子を作成した場合、どちらにも属さないその演算子をどこに配置すればいいのかという問題が生じますが、パフォーマンス上の理由から両方のクラスのフレンドにします。これが、扉に関する筆者の考え方です。扉は実際のところ、扉でつながっているタイルのどちらにも属さないのです。

デザイン ソリューションのご多分にもれず、このソリューションにも 1 つ問題があります。扉をどのようにしてタイル、ギザギザ、匂いなどの実体に結び付けるかという問題です。筆者はこれを、2 つのエントリ セット間で一意の ID を対応付けることによって実現しました。これは図らずもリレーショナル データベースの表現と同じであり、先に述べたように、Microsoft® .NET Framework の Foundation Class ライブラリの System::Data 名前空間のデータベース サポートを利用することができます。

DataSet は、DataTable インスタンスのコレクションを表します。このケースでは、一連のタイルがメイン テーブルになります。それぞれの環境実体 (ギザギザや扉など) は、追加の DataTable として格納されます。タイル内の各属性について一意の ID を取得し、適切な関連テーブルへの相互参照を設定します。

XML 表現から DataSet への変換は、実のところ非常に単純です。XML ドキュメントとリレーショナル データベースの間には特別な関係があり、ある意味では水と氷の関係に似ています。まったく同じデータを、それぞれ異なるアクティビティに合わせて別形式で表したものと考えることができます。

DataSet には、XML ドキュメントのパス名を受け取って DataSet オブジェクトにデータを格納する ReadXml メソッドがあります。次に例を示します。

DataSet^ ds = gcnew DataSet;
ds->ReadXml( eek_world_xml_path_string );

これで、XML ドキュメントが一連の DataTable として DataSet オブジェクトに保持されるようになりましたが、このままではまだテーブル間のリレーションシップは認識されません。テーブル間のリレーションシップを明確に記述する必要があります。そのためには、DataRelation プロパティを DataSet に追加します。DataRelation を使用すると、2 つのテーブル間のリレーションシップを共通の列 (要素の一意の ID、つまりキー) に基づいて定義できます。

DataSet クラスの Tables プロパティは、DataSet に関連付けられている一連の DataTable オブジェクトを含む DataTableCollection オブジェクトを返します。特定のインスタンスにアクセスする方法は 2 つあります。1 つは、位置でインデックスを指定する方法です。

enum class tableIndex {
    tile, doorto, burr, e-shock, ...
};

DataTable^ world_tile = ds->Tables[tile];

もう 1 つは、アクセスするテーブルの文字列表記を使用して関連付けでインデックスを指定する方法です。

DataTable^ world_tile = ds->Tables[tile];
world_tile = ds->Tables[ worldTile_table_string ];

DataTable には、TableName、Rows、Columns、PrimaryKey など多数のプロパティが関連付けられています。

// returns the name of the table ...
String^ tabName = world_tiles->TableName;
DataRowCollection^    drows = world_tiles->Rows;
DataColumnCollection^ ccols = world_tiles->Columns;
int countRows = drows->Count;
int countCols = ccols->Count;
array< DataColumn^ >^ keys = world_tile->PrimaryKey;

主キー (primary key) は、各レコード エントリに対して一意の値を持つことが保証されている列で、その値はテーブル内のレコードの ID としても機能します (一連の列を主キーとすることも可能であり、そのため PrimaryKey プロパティは DataColumn オブジェクトを 1 つだけではなく配列で返します)。

たとえば、tile テーブルでは、doorto 属性が扉の一意の ID を保持します。doorto テーブルでは、そのキーが doorto エントリを構成する主キーになります。

ここでは、キー ID 値を持つ 2 つのテーブルの列をリンクする DataRelation クラス オブジェクトを定義することによって、このリレーションシップを指定します。

DataRelation^ doorto = gcnew DataRelation("UniqueID",
    ds->Tables["worldTile"]->Columns[ doortoID_string ],
    ds->Tables["doorto"]->Columns[ doortoID_key ]);

1 番目の列は親と呼ばれ、その値は 2 番目の列の対応するエントリを取得するために使用されます。2 番目の列は子と呼ばれます。このリレーションシップを DataSet に追加します。

ds->Relations->Add(doorto);

このようにして、doorto テーブルで明示的な ID を明示的に検索するのではなく、DataRelation オブジェクトを使用して取得を実行します。

まとめ

.NET の最も生産的な側面の 1 つとして、その強力なツールセットが挙げられます。環境の規模が大きくなって領域の制約を受けるようになってくればファイアホース XML のサポートが最も有用になり、そうでなければ DOM で最もデータリッチな XML 表現を実現できます。このケースでは、ドキュメントを DataSet に読み込むことで、ドキュメントのレイアウトのリレーショナルな面を活かしています。そして、これらのソリューションは、あらゆる .NET 準拠の言語で機能します。繰り返し述べているとおり、もはや言語の問題ではなくライブラリの問題なのです。次回のコラムでは、ネズミの実装に取りかかります。

ご意見やご質問は、

Stanley purecpp@microsoft.com.

まで英語でお送りください。
Stanley B. Lippman は、C++ の考案者である Bjarne Stroustrup と共に 1980 年代から 1990 年代にかけてベル研究所で C++ に携わった実績と、現在までに 4 版を重ねている入門書『C++ Primer』(Addison-Wesley、2005 年) の著述で知られています。マイクロソフトの Visual C++ チームのアーキテクトとして、このコラムの焦点である C++/CLI の考案に取り組んだ経歴の持ち主でもあります。


.NET の問題: デッドロック モニタ

.NET の問題
デッドロック モニタ
Stephen Toub

コードのダウンロード : : NETMatters2007_10.exe (156KB)

Q 作成中のアプリケーションで、一連のスレッド上で作業を同期するためにロックを使用しています。ところが、どこかが間違っているらしく、ときどきスレッドがまったく動かなくなります。デッドロックが発生しているのだと思いますが、どうやって見つけたらよいかわかりません。これをプログラムで行う方法はありませんか。デッドロックが発生した場合は例外がスローされるようにしたいのですが。

A まず、スレッド間のデッドロックとその発生条件について理解しておくことが重要です。デッドロックが発生するのは、スレッドどうしが互いにリソースの解放を待機しているが、まさにその待機によってブロックされるために双方がリソースを解放せず、したがって双方がブロックを解除できない場合です。リソースが解放されるまで、これらのスレッドは処理を進めることができません。しかし、処理が進まないのですから、いつまでもリソースは解放されず、スレッドはロックされ、"デッドロック" が起こります。OS に関する多くの教科書が挙げるとおり、デッドロックの発生に必要な条件は次の 4 点です。

  • 特定のリソースの数が限られている。C# (lock キーワードを用いるときに使用) でのモニタの場合、モニタが相互排除ロックである (つまり、一度に 1 つのスレッドだけがモニタを所有できる) ため、この限られた数とは 1 つです。
  • 1 つのリソースを保持したまま別のリソースを要求できる。C# では、1 つのオブジェクトをロックした後、この最初のロックを解放する前に別のオブジェクトをロックする操作に相当します。次に例を示します。

lock(a)
{
    lock(b)
    {
        ...
    }
}

  • 横取り機能がない。C# では、スレッドが、別のスレッドによるロックを強制的に解放できないことを意味します。
  • 循環待機の状態。各スレッドで次のスレッドによるリソースの解放を待機したまま処理を続行できないような、スレッドの循環が存在することを意味します。

これらの条件のいずれかが満たされない場合、デッドロックは発生しません。最初の条件はモニタの特性であり、モニタの使用時には回避できません。2 番目の条件は、一度に 1 つのオブジェクトだけをロックするようにすることで回避できます。しかし、大規模なソフトウェア プロジェクトの場合、あまり現実的な方法ではありません。3 番目の条件は、Microsoft® .NET Framework で、スレッドに必要なリソースを保持している他のスレッドを中止または中断することによって回避できる可能性があります。しかし、a) それにはリソースをどのスレッドが所有しているのかを知る必要があるうえ、b) そのような操作は本質的に危険です (多くの理由については、msdn.microsoft.com/msdnmag/issues/05/10/Reliability を参照)。したがって、デッドロックを回避するには、4 番目の条件を回避 (阻止) することになります。

2006 年 4 月号の記事 (msdn.microsoft.com/msdnmag/issues/06/04/Deadlocks) で Joe Duffy は、ロックのレベル付けなど、デッドロックを回避および検出する複数の方法について論じています。ロックのレベル付けでは、ロックに数値を割り当て、スレッドが、既に取得済みのロックよりも高い数値を持つロックだけを取得できるようにします。このため、循環が発生しなくなります。しかしこれも、今日の一般的なソフトウェア アプリケーションではうまく機能しないことが多く、ロックのレベル付けに従わないロック取得が 1 か所でもあると、デッドロックを招きます。

多くのシステムでは、デッドロックを全面的に回避しようとせず、デッドロックを検出し、発見されるたびに除去しています。たとえば、SQL Server® では、発生時にデッドロックを検出し、循環に含まれているタスクのいずれかを中止することによって、デッドロックを取り除くことができます。Joe の記事では、このようなデッドロック検出機能を持つ共通言語ランタイム (CLR) ホストを、モニタを使用する .NET アプリケーションで構築するという快挙が成し遂げられています。残念ながら、ASP.NET アプリケーションのように既にカスタム ホストを持つ場合もあり、.NET アプリケーションにとってカスタム CLR ホストを使用することが実際的であるとは限りません。そこで、カスタム CLR ホストを使用しなくても類似のデッドロック検出機能を利用できて、これらの種類のデッドロックを実行時に検出できるようになると便利です。そうすれば、開発とテストのフェーズにおいて、ロック誤使用時のコーディング エラーを探し出すのに役立ちます。また、実稼働時に使用して、発生したデッドロックを検出および除去する (循環を完成させるクリティカルな待機の試行からスレッドをブロックすることにより、このスレッドがデッドロックを引き起こすのを防止する) ことも可能です。しかし、一般的なデッドロック検出アルゴリズムは高コストであり、パフォーマンス上の理由から運用システムには適さない場合があります (パフォーマンスについては、このコラムの最後で触れるつもりです)。

このニーズに応えるため、.NET の System.Threading.Monitor クラスに対して、デッドロック検出機能を持つサンプル ラッパーを作成しました。Monitor と同様、この DdMonitor クラスも Enter メソッドと Exit メソッドを提供し、Monitor の同等のメソッドに内部でデリゲートします。それだけでなく、モニタ使用状況の追跡も行い、試行されたロック取得により循環が完成してデッドロックが発生するような場合には例外をスローします。このコラムの残りの部分では、このデッドロック モニタ クラスの実装方法を詳細に説明し、使用方法や、展開のメリットとデメリットに関する追加情報を提供します。

DdMonitor クラスの概要を、図 1 に示します。DdMonitor では最初から、System.Threading.Monitor のパブリック インターフェイスを完全には模倣していません。パブリックな静的メソッド TryEnter、Enter、および Exit については同じメソッドを提供しますが、パブリックな静的メソッド Wait、Pulse、および PulseAll は提供しません。

Figure 1 DdMonitor クラス (Close Figure 1)
class DdMonitor
{
    public static IDisposable Lock(object monitor) { ... }

    public static void Enter(object monitor) { ... }

    public static bool TryEnter(object monitor) { ... }
    public static bool TryEnter(
        object monitor, TimeSpan timeout) { ... }
    public static bool TryEnter(
        object monitor, int millisecondsTimeout) { ... }

    public static void Exit(object monitor) { ... }
}

DdMonitor は、パブリックな静的メソッド Lock も提供します。C# の lock キーワードは Monitor に優れた抽象化を提供するため、DdMonitor にも類似の抽象化を用意すると効果的です。C# 言語を変更できないため、まったく同じ構文を取得することは不可能ですが、次のような似た構文を使用できます。

// with Monitor
lock(obj) { ... }

// with DdMonitor
using(DdMonitor.Lock(obj)) { ... }

この構文の妙技を実現するため、図 2 に示すとおり Lock を実装します。Lock メソッドは、DdMonitor.Enter にデリゲートしてロックを取得します。それだけでなく、IDisposable を実装する DdMonitorCookie オブジェクトのインスタンス化も行います。このオブジェクトの Dispose メソッドは、DdMonitor.Exit を呼び出してロックを解放します。このため開発者は、前に示したように、DdMonitor.Lock を C# の using ステートメント (Visual Basic® の Using、または C++/CLI のスタック割り当て) にラップできます。

Figure 2 Lock メソッドの実装 (Close Figure 2)
public static IDisposable Lock(object monitor)
{
    if (monitor == null) throw new ArgumentNullException("monitor");
    IDisposable cookie = new DdMonitorCookie(monitor);
    Enter(monitor);
    return cookie;
}

private class DdMonitorCookie : IDisposable
{
    private object _monitor;

    public DdMonitorCookie(object obj) { _monitor = obj; }

    public void Dispose()
    {
        if (_monitor != null)
        {
            DdMonitor.Exit(_monitor);
            _monitor = null;
        }
    }
}

Enter メソッドと、3 つの TryEnter メソッドのうちの 2 つは、3 番目の TryEnter メソッド用の単なるラッパーです (図 3 を参照)。これらの実装は、Monitor の Enter メソッドと TryEnter メソッドによる実装と同じ仕様に従うためのものです。Enter を呼び出すと、ロックを取得するまでブロックし (この例の Enter メソッドは設計が少し異なり、いつまでもブロックし続けるのではなく、デッドロックが検出された場合に例外をスローします)、Timeout.Infinite タイムアウトで TryEnter にデリゲートします。Timeout.Infinite は -1 ですが、これはロックを取得するまでブロックするよう TryEnter に指示する特別な値です。所定の時間が経過すると失敗するという意味ではありません。一方、TryEnter のオーバーロードは時間値を受け取らないため、既定でタイムアウト 0 が使用されます。つまり、ロックを直ちに取得できない場合は false が返されます (この例の実装では、ここでも、ロックを取得するとデッドロックが発生するような場合に例外をスローします。ただし TryEnter の場合、この判断には議論の余地があるため、必要に応じて実装を変更してください)。TryEnter は、ロックが取得された場合には true を、それ以外の場合には false を返します。TryEnter の 2 番目のオーバーロードは、指定した TimeSpan タイムアウト値をミリ秒数に変換し、引数を検証し、TryEnter の最後のオーバーロードにデリゲートします。この最後のオーバーロードにおいて、すべての作業が実際に行われます。

Figure 3 Enter の実装 (Close Figure 3)
public static void Enter(object monitor)
{
    TryEnter(monitor, Timeout.Infinite);
}

public static bool TryEnter(object monitor)
{
    return TryEnter(monitor, 0);
}

public static bool TryEnter(object monitor, TimeSpan timeout)
{
    long totalMilliseconds = (long)timeout.TotalMilliseconds;
    if (totalMilliseconds < -1 ||
        totalMilliseconds > Int32.MaxValue)
            throw new ArgumentOutOfRangeException("timeout");
    return TryEnter(monitor, (int)totalMilliseconds);
}

設計は比較的単純ですが、興味深い実装内容もいくつか含まれています。実装では、まず、指定されたパラメータを検証し、どのオブジェクトをロックするのかが実際に指定されていることと、有効なタイムアウト (-1、または 0 以外のミリ秒数) が指定されていることを確認します。

このとき、DdMonitor はある共有状態にアクセスしなければなりません。DdMonitor では、モニタに関する情報 (どのスレッドが所有し、どのスレッドが待機中かなど) を CLR で調査する方法がサポートされていないため、この情報を手動で追跡する必要があります。これを実現するため、システム内で取得されたモニタについて、すべての情報をデータ構造の静的テーブルに保存しておき、モニタで Enter 操作や Exit 操作を実行するたびにこの共有状態を更新します。

これによる影響がいくつかあります。まず、DdMonitor は、ユーザーによって指定されたモニタ オブジェクトを使用して最終的に Monitor にデリゲートしますが、一方で、Monitor のメソッドへの直接的な呼び出しについては関知しないため、内部の状態情報をこうした呼び出しに基づいて更新することができません。つまり、アプリケーションで DdMonitor と Monitor (または lock キーワード) の両方を使用した場合、デッドロック検出が正しく機能せずに誤検知 (false negative) を起こし、一部のデッドロックを回避しない可能性もあります。

この実装におけるもう 1 つの問題点は、この静的テーブルが AppDomain に固有であることです。ロックするオブジェクトが 1 つの AppDomain 内のみに存在するのであれば、問題にはならないはずです。しかし、AppDomain 境界を越えることができる特別な種類のオブジェクト (Type や String など) も存在します。複数ドメインから DdMonitor を使用して、このようなドメイン アジャイル オブジェクトをロックした場合、各ドメインにおいて DdMonitor が管理する静的テーブルには必要な情報の一部しか格納されず、誤検知 (false negative) につながる可能性があります。

この方法の第 3 の問題点は信頼性にあります。C# の lock ステートメントは、直後に try ブロックが続く Monitor.Enter への呼び出しに展開されます。try ブロックの本文は lock ステートメントの本文に相当し、対応する finally ブロックでモニタを解放します。CLR のちょっとした技によって、Monitor.Enter への呼び出しが成功した場合は必ず try ブロックに入ることになっています。このため finally ブロックも、Monitor.Enter が成功した場合は必ず実行されます。これが重要なのは、こうしないと Monitor.Enter を呼び出してから try ブロックに入るまでの間に非同期の例外が発生する可能性があるためです (詳細については、msdn.microsoft.com/msdnmag/issues/05/10/Reliability および www.bluebytesoftware.com/blog/2007/01/30/MonitorEnterThreadAbortsAndOrphanedLocks.aspx を参照)。しかし、この例の DdMonitor 実装では、基になる Monitor.Enter への実際の呼び出しの後、DdMonitor.Enter への呼び出しに続く try ブロックの前に、コードが存在します。したがって、そのような信頼性の保証がこの実装では失われています。

実際のシナリオに危険を及ぼさないかどうか、これらの問題点すべてを考慮してください。

さて、TryEnter の実装に戻って (図 4 を参照)、次は、この共有状態の内部ロックを取得します。取得したら、辞書にアクセスし、Enter するモニタに関する以前のデータがないかどうかを検索します。そのようなデータが存在しない場合、そのモニタを現在所有したり待機したりしているスレッドがないことを意味します。その場合、TryEnter は、このモニタを追跡してテーブルに戻すための新しい MonitorState オブジェクトを初期化します。

Figure 4 TryEnter の実装 (Close Figure 4)
private static Dictionary<object, MonitorState> _monitorStates =
    new Dictionary<object, MonitorState>();

public static bool TryEnter(object monitor, int millisecondsTimeout)
{
    if (monitor == null) throw new ArgumentNullException("monitor");
    if (millisecondsTimeout < 0 &&
        millisecondsTimeout != Timeout.Infinite)
            throw new ArgumentOutOfRangeException("millisecondsTimeout");

    bool thisThreadOwnsMonitor = false;
    MonitorState ms = null;
    try
    {
        lock (_globalLock)
        {
            if (!_monitorStates.TryGetValue(monitor, out ms))
            {
                _monitorStates[monitor] = ms = new MonitorState(monitor);
            }

            if (ms.OwningThread != Thread.CurrentThread)
            {
                ms.WaitingThreads.Add(Thread.CurrentThread);
                ThrowIfDeadlockDetected(ms);
            }
        }

        thisThreadOwnsMonitor = Monitor.TryEnter(
            monitor, millisecondsTimeout);
    }
    finally
    {
        lock (_globalLock)
        {
            if (ms != null)
            {
                ms.WaitingThreads.Remove(Thread.CurrentThread);

                if (thisThreadOwnsMonitor)
                {
                    if (ms.OwningThread != Thread.CurrentThread)
                        ms.OwningThread = Thread.CurrentThread;
                    else ms.ReentranceCount++;
                }
            }
        }
    }

    return thisThreadOwnsMonitor;
}

private class MonitorState
{
    public MonitorState(object monitor) { MonitorObject = monitor; }
    public object MonitorObject;
    public Thread OwningThread;
    public int ReentranceCount;
    public List<Thread> WaitingThreads = new List<Thread>();
}

MonitorState には、モニタに関するいくつかの情報が格納されます。まず、モニタ自体が格納されます。システムはこれを、デッドロックの検出時に診断情報を提供するためにのみ使用します (デッドロックの循環にかかわりを持つモニタが判明すると、デバッグに役立つため)。MonitorState には、モニタを現在所有するスレッドの Thread オブジェクト (現在取得されていない場合、この値は null)、モニタが Enter された回数を示す ReentranceCount (モニタを所有するスレッドは、そのモニタに安全に再度 Enter できるため)、およびこのモニタを現在取得しようとしているスレッドを示す待機スレッドのリストも格納されます。

このモニタの MonitorState 追跡オブジェクトを入手したら、TryEnter は、MonitorState の WaitingThreads リストに現在の Thread を追加することによって、このスレッドがオブジェクトを待機する意図を登録します。この意図が登録されたら、TryEnter はプライベート メソッド ThrowIfDeadlockDetected を呼び出します。このメソッドは名前のとおりの機能を持っています (このメソッドの実装については、この後説明します)。ThrowIfDeadlockDetected は、このロック取得がデッドロックの循環を引き起こすことを検出した場合に、例外をスローして、ロックを取得できないようにします。また、その場合、この呼び出しがラップされている finally ブロックで、待機スレッドのリストから現在のスレッドが削除されます。

デッドロックが検出されなければ、このスレッドでモニタの取得を試行しても安全なので、TryEnter は Monitor.TryEnter にデリゲートして実際のモニタに Enter します。ロックが取得されたかどうかにかかわらず、次に、共有状態のロックが必要となります。ここでは、DdMonitor がデッドロックを起こすのを防止するため、ロックをレベル付けする独自のスキームを使用します (デッドロックを防止するためのツールがデッドロックを起こしたら最悪です)。ユーザー指定のモニタに対するロックを保持したまま _globalLock を取得してもかまいませんが、その逆は許されません。このため、ユーザー指定のモニタを既に所有している可能性がある finally ブロックで _globalLock に対するロックを取得してもかまいませんが、_globalLock を保持したままユーザー指定のモニタに対するロックの取得を試行することはできません。これが、Monitor.TryEnter の呼び出し前にまず _globalLock に対するロックを Exit する理由です。

この時点で、ユーザー指定のモニタの取得に成功したかどうかにかかわらず、モニタの待機リストから現在のスレッドを削除します (モニタを所有しているか待機が失敗したかのどちらかで、いずれにしても待機中ではありません)。次に、モニタが取得された場合は MonitorState の関連フィールドを更新します。

図 5 に示すとおり、ロックの Exit はさらに単純です。引数の検証を除く本文全体が、_globalLock モニタに対するロックにラップされています。ユーザー指定のモニタの MonitorState が取得され (存在しない場合、このモニタはロックされておらず、例外がスローされます)、どのスレッドによって所有されているのかが確認されます。所有されていない場合や、所有者が現在のスレッドでない場合、Exit への呼び出しは不適切であったため、例外がスローされます。それ以外の場合、現在のスレッドがこのモニタに複数回 Enter していれば、ReentranceCount が単純に減らされ、そうでなければ、OwningThread が null に設定されます。この null 値は、このスレッドがモニタに対するロックを解放することを意味します。さらに、このモニタを待機しているスレッドがない場合、MonitorState がテーブルから削除され、ガベージ コレクションの対象になります (これを行わないと、テーブルが無制限に大きくなり続けます)。このメソッドは最後に、Monitor.Exit にデリゲートして実際のモニタを解放します。

Figure 5 Exit の実装 (Close Figure 5)
public static void Exit(object monitor)
{
    if (monitor == null) throw new ArgumentNullException("monitor");

    lock (_globalLock)
    {
        MonitorState ms;
        if (!_monitorStates.TryGetValue(monitor, out ms))
            throw new SynchronizationLockException();

        if (ms.OwningThread != Thread.CurrentThread)
        {
            throw new SynchronizationLockException();
        }
        else if (ms.ReentranceCount > 0)
        {
            ms.ReentranceCount--;
        }
        else
        {
            ms.OwningThread = null;
            if (ms.WaitingThreads.Count == 0)
                monitorStates.Remove(monitor);
        }

        Monitor.Exit(monitor);
    }
}

ここからが肝心な部分です。これまで実装全体を見てきましたが、実際のデッドロック検出アルゴリズムについてまだ触れていませんでした。その中核は、前に DdMonitor.Enter から呼び出されることを説明した ThrowIfDeadlockDetected (図 6 を参照) に実装されています。

Figure 6 待機チェーンの循環の検出 (Close Figure 6)
private static void ThrowIfDeadlockDetected(MonitorState targetMs)
{
    if (targetMs.OwningThread == null) return;

    Dictionary<Thread, List<MonitorState>> locksHeldByThreads;
    Dictionary<MonitorState, List<Thread>> threadsWaitingOnLocks;
    CreateThreadAndLockTables(
        out locksHeldByThreads, out threadsWaitingOnLocks);

    Queue<CycleComponentNode> threadsToFollow =
        new Queue<CycleComponentNode>(locksHeldByThreads.Count);

    threadsToFollow.Enqueue(new CycleComponentNode(
        Thread.CurrentThread, targetMs, null));

    while (threadsToFollow.Count > 0)
    {
        CycleComponentNode currentChain = threadsToFollow.Dequeue();
        Thread currentThread = currentChain.Thread;

        List<MonitorState> locksHeldByThread;
        if (!locksHeldByThreads.TryGetValue(
            currentThread, out locksHeldByThread)) continue;

        foreach (MonitorState ms in locksHeldByThread)
        {
            List<Thread> nextThreads;
            if (!threadsWaitingOnLocks.TryGetValue(
                ms, out nextThreads)) continue;

            foreach (Thread nextThread in nextThreads)
            {
                if (currentChain.ContainsThread(nextThread))
                    throw new SynchronizationLockException(
                        CreateDeadlockDescription(
                            currentChain, locksHeldByThreads));

                threadsToFollow.Enqueue(new CycleComponentNode(
                    nextThread, ms, currentChain));
            }
        }
    }
}

ThrowIfDeadlockDetected メソッドは、パラメータとして、待機しようとするモニタを表す MonitorState インスタンスを受け取ります。デッドロックが発生するには、このモニタ (現在のスレッドが待機しようとしており、その意図を登録済みである) が、直接的または間接的にこのモニタを保持しており、かつ現在のスレッドによって保持されているモニタを待機しているスレッドによって、保持される必要があります。"直接的または間接的に保持" とは、モニタに対するロックを取得したか、このモニタを直接的または間接的に保持するスレッドによって保持されているモニタを待機しているかのどちらかです (繰り返しが多くてすみません)。つまり、このモニタからこれを所有するスレッドへ、そのスレッドが待機しているモニタへ、そのモニタを所有するスレッドへとつながっており、最終的に元のモニタとこれを所有するスレッドに戻るような循環を探すのです。そのような循環が見つかったら、例外をスローして、デッドロックのシグナルを送ります。

実装はこのアプローチに従います。まず、対象の MonitorState が実際にスレッドに所有されていることを確認します。所有されていなければ、それを待機しても問題ありません。次に、どのロックがどのモニタを所有および待機しているのかに関して現在持っているすべての情報に基づき、ルックアップ テーブルを生成します (この情報はすべて _monitorState テーブル内に既にあるため、その情報を 2 つの Dictionary データ構造にリファクタリングするだけです。これによって残りのアルゴリズムを実装しやすくなります)。

ここで、スレッドの調査を開始する必要があります。現在のスレッドから始めます。このスレッドは指定した MonitorState を待機していることがわかっています。この組み合わせを取得するため、単純なヘルパ コンテナ CycleComponentNode を作成します。このコンテナには、このデータを格納するだけでなく、検出された循環内の次のノードへの参照も保持します。アルゴリズムの開始時に判明しているノードは、最初のスレッドと MonitorState だけです。したがって、Next の値は null です。

調査するスレッドがなくなるまで、次に調査するスレッド (正確に言えば、そのスレッドが格納されている CycleComponentNode) をキューから取得します。前に作成したテーブルを使用して、このスレッドが保持しているすべてのロックを取得します。ロックをまったく保持していない場合、デッドロックの一部となることはありません (デッドロックに必要な条件、特に "1 つのリソースを保持したまま別のリソースを要求できる" を思い出してください)。これらのロックをそれぞれ調査し、前に作成したテーブルを再度使用して、これらのロックを待機しているスレッドを検索します。それらのスレッドのいずれかが、作成した現在の待機チェーン (CycleComponentNode によりリスト形式で表される) 内に存在すれば、デッドロックが見つかったことになります。CreateDeadlockDescription メソッドは、チェーン内のすべての CycleComponentNode インスタンスを列挙し、次のような記述 (例外でスローされる) を生成します。

Thread 24 waiting on lockP (39C82F0)
    while holding lockH (36BEF47), lockK (3B3D5E3)
Thread 30 waiting on lockH (36BEF47)
    while holding lockB (2CF7029), lockN (349C40A
Thread 29 waiting on lockB (2CF7029)
    while holding lockP (39C82F0)

スレッドが現在の待機チェーン内に存在しなくても、さらに調べる必要があります。そこで、CycleComponentNode を、このスレッドとこれが待機している MonitorState の近くに作成し、その Next の値を現在の待機チェーンに設定します。このようにして、このスレッドとこれが保持するモニタを待機しているすべてのスレッドを調べ終わったとき、待機チェーンでそれらのスレッドを正しく検索できるようになります。

解決策は以上です。これでニーズを満たしきれない場合は、もちろん必要に応じて変更できます。たとえば、ブロック時に現在のスレッドのスタック トレースを MonitorState に格納しておき、デッドロックが見つかったときにスローされる例外メッセージに、関連するすべてのスレッドのスタック トレースを含めることもできます。また、解決策を拡張して、System.Threading.Mutex などの同期プリミティブを追加でサポートすることもできます。

また、ネイティブ コードを記述していて Windows 同期プリミティブをロックする場合、新しい Wait Chain Traversal API を利用すると、同様の分析をより強力に実行できます。詳細については、MSDN® Magazine 2007 年 7 月号の John Robbins による Bugslayer コラム (msdn.microsoft.com/msdnmag/issues/07/07/Bugslayer) を参照してください。

最後に、この実装のパフォーマンスについて説明します。ここで実装したデッドロック検出コードは、負荷が高く、わかりやすく、かつ簡潔です。このため、デバッグ ビルドでの使用には適していますが、製品ビルドでの使用には望ましくありません。そうするための方法はいくつかありますが、最も簡単なのは、DdMonitor の実装に次のような静的プロパティを追加することです。

private static bool _useDeadlockDetection = false;

public static bool UseDeadlockDetection
{
    get { return _useDeadlockDetection; }
    set { _useDeadlockDetection = value; }
}

これを利用して Enter、TryEnter、および Exit の実装を次のように拡張すると、このプロパティを確認および使用することにより、完全なデッドロック検出アルゴリズムを実装するのか、それとも対応する Monitor のメソッドにデリゲートするのかを決定するようにできます。

public static void TryEnter(object monitor, int millisecondsTimeout)
{
    if (UseDeadlockDetection)
    {
         ... // existing logic here
    }
    else TryEnter(monitor, millisecondsTimeout);
}

コードで UseDeadlockDetection を設定するには、構成ファイルから値を読み取る方法があります。また、ビルド構成に基づいて値を設定する場合は、次のようなコードを使用できます。

public static void Main(string [] args)
{
    DdMonitor.UseDeadlockDetection =
#if DEBUG
        true;
#else
        false;
#endif

    ... // the rest of your app here
}

UseDeadlockDetection を false に設定してコード ベース全体で DdMonitor を使用した場合、その動作とパフォーマンスは、単に Monitor を使用した場合とまったく同じではありませんが、近いものになります。プログラムの冒頭を除いて、UseDeadlockDetection の値は変更しないよう注意してください。他の場所でこれを変更すると、モニタ取得時にデッドロック検出が使用されたりされなかったりする可能性があります。それによる危険性については既に述べたとおりです。Joe Duffy が提案する別の方法では、TryEnter の実装を変更して、まずデッドロック検出コードを使用せず、構成可能なタイムアウト (たとえば 500 ミリ秒、または、ユーザー指定のタイムアウトの方が小さい場合はタイムアウトの値) で Monitor.TryEnter にデリゲートします。その呼び出しが成功した場合は、モニタが正常に取得されたため、デッドロック検出を実行する必要がありません。呼び出しがタイムアウトになり、選択したタイムアウトがユーザー指定のタイムアウトよりも小さかった場合は、完全なデッドロック検出アルゴリズムを実行できます。こうすると、最初の Monitor 取得がタイムアウトになるまでは、検出アルゴリズムのコストを負担することなく、展開されたプログラム内のデッドロックを検出できるようになります。タイムアウトを適切に選択しておけば、デッドロック時にはいずれにせよタイムアウトになるはずです。

ご意見やご感想は、Stephen netqa@microsoft.com.

まで英語でお送りください。
Stephen Toub は、Microsoft の並列コンピューティング プラットフォーム チームのシニア プログラム マネージャです。また、MSDN Magazine の寄稿編集者でもあります。


{ End Bracket }: 結婚にまつわるマッシュアップ

{ End Bracket }
結婚にまつわるマッシュアップ
Michael Richter

MSDN® Magazine と TechNet Magazine の Web サイト マネージャとしての仕事で、数日はかかりそうな単調な手作業を行うか、その作業を自動で行うアプリケーションを記述するかを決めなければならないことがよくあります。もちろん、そのアプリケーションを記述すること自体に数日かかるかもしれないので、自動化の時間と費用の便益がいつも実現されるわけではありません。それでも、アプリケーションを構築することで楽しく仕事に取り組めます。反対に、手作業の繰り返しでは仕事が嫌になることもあります。

私は 10 月に結婚する予定ですが、結婚式の計画作業は同じくらい退屈かもしれないので (婚約者には内緒です)、その作業の一部を自動化することに開発者のスキルを活かそうと決めました。さいわい、計画のほとんどは彼女が行っています。私が頼まれたのは、結婚式用の Web サイトの作成と招待者リストの管理の 2 つです。

(画像を縮小するには、ここをクリックします)

(画像を拡大するには、ここをクリックします)

道順と静的な地図のリンクを入力する代わりに、Virtual Earth™ API (msdn.microsoft.com/msdnmag/issues/06/09/EarthlyDelights を参照) を使用して動的ページを作成しました。2 つのドロップダウン リストに XML ファイルから住所を挿入し、一方のリストを経路の出発地点として使用し、もう一方のリストを経路の目的地点として使用しました。各コントロールには、両方の経路地点が設定されているかどうかを検出し、その住所を VEMap オブジェクトの GetRoute メソッドに送信する onchange イベントがあります。これによって地図が更新され、それらの地点の間の道順が表示されます。

2 つの住所間の経路を描画しているときはうまく機能しましたが、特定の経度座標と緯度座標の間の経路、および座標と住所の間の経路を描画できるようにもしようと考えました。そのため、地図と道順を自動化する簡単なページが、すぐに複雑になってしまいました。

住所が文字列として送信されるので、座標も文字列として GetRoute に送信しようとしましたが、座標は VELatLong オブジェクトとして送信する必要があることに気付きました。結局、次のように、リストの座標の値にフラグを付けて、経路地点の新しい VELatLong を作成することを JavaScript に通知するようにしました。

<option
   value="821 Stewart Avenue, Garden City, NY">Wingate Hotel</option>
<option value="POSITION,40.729877,-74.038788">Holland Tunnel</option>
<option value="POSITION,40.60294,-74.066305">Verazzano Bridge</option>

今回の JavaScript コードでは、POSITION フラグによって経路地点が座標のセットであるかどうかを検出し、文字列の代わりに VELatLong オブジェクトを GetRoute に送信できます。

地図上に招待者の住所をすべて表示しようとも思いました。そうすれば、遠くから来るためホテルの部屋が必要になりそうな招待者や、車に相乗りできるかもしれない招待者を確認できます。もちろん、その地図を私たちの Web サイトで表示すれば格好も良いでしょう。

それには、Virtual Earth のプッシュピンを使用することにしましたが、いずれの住所の座標もありませんでした。VEMap オブジェクトには、バージョン 5 の Virtual Earth コントロールの Find メソッドがあります。Find メソッドには住所を渡すことができ、その座標がコールバック関数に返されます (コールバックの名前をパラメータとして Find メソッドに渡します)。このメソッドは見つける必要のある住所が 1 つの場合にはとても便利ですが、150 の住所を見つける必要がある場合はどうでしょうか。Find メソッドを 150 回呼び出す場合の問題が、コールバック メソッドへの各呼び出しが前の呼び出しを上書きすることであるということは、後からわかりました。したがって、Find メソッドを使用して地図上の多数の住所の座標を取得することは、少なくとも連続してはできません。

そこで、ジオコーディング Web サービスを使用しました。マイクロソフトでは、多数の住所の座標を取得できる機能を含め、さまざまな機能が用意されている MapPoint® Web サービスを提供しています (msdn2.microsoft.com/bb545004 を参照)。

Web ページでこの地図を構築するのではなく、Microsoft® Excel 内で構築することにしました。Josh Trupin の記事「ジオペグ」 (msdn.microsoft.com/msdnmag/issues/07/04/EndBracket を参照) を参考にし、Visual Studio® Tools for Office 2nd Edition を使用して Excel 2007 のカスタム ペイン内で Web ブラウザ コントロールをホストしました。セルのグループを選択すると、アドインによって住所があるかどうかが検出され、ある場合は、その住所の地図が Web ブラウザ コントロールに表示されます。これによって、さまざまな住所を Excel に追加したり、複数の住所を強調表示してすぐにそれらを地図に表示したりできます。最終的なソリューションは、MSDN Magazine の Web サイトからダウンロードできます。

手動で実行する方が速いかもしれない場合でも、作業を自動化するのは、ほとんどの開発者にとって楽しいことです。カスタム アプリケーションのボタンを押すだけでその作業のすべてが行われれば、さらに満足です。私の結婚式の場合は、退屈になったかもしれない作業を引き受け、それらの作業で楽しみました。私も満足できましたが、婚約者も満足できました。それが最も大事なことなのです。

Michael Richter は、MSDN Magazine と TechNet Magazine の Web サイト マネージャです。現在、ニューヨーク市に住んでいます。