October 2008
パラダイム シフト: 並列プログラミングの設計上の考慮事項
この記事では、並列プログラミングを最大限活用するために問題を多くのタスクに切り分ける新しい手法と、その手法を導入する理由について簡単に説明します。 David Callahan
コーディング ツール: 次期バージョンの Visual Studio で強化される並列処理のサポート
次期バージョンの Visual Studio でマネージ コードとネイティブ コードの両方に対して計画されている並列プログラミングのサポートを見ていきます。 Stephen Toub and Hazim Shafi
同時実行の障害: マルチスレッド コードでよく見られる 11 の問題を解決する
この記事では、同時実行プログラミングのいくつかの一般的な課題について説明し、ソフトウェアでそれらの課題に対処するための助言を提供します。 Joe Duffy
ASP.NET AJAX 4.0: データ駆動型 Web アプリケーションに対する新しい AJAX サポート
ここでは、サーバー側とクライアント側のプログラミングの最良の機能を活用して、効率的でユーザー フレンドリなエクスペリエンスを提供する、ASP.NET AJAX のデータ駆動型 Web アプリケーションを紹介します。 Bertrand Le Roy
簡単な非同期: 単純な F# の式から同時実行アプリケーションを構築する
この記事では、F# 言語が、他の任意の .NET 準拠言語からシームレスに呼び出すことができる非同期関数ライブラリの作成にどのように役立つかを解説しています。 Chance Coble
Columns
編集者のメモ: new Thread(ReadEditorsNote).Start(); yourAttention.WaitOne();: 単純にコードの並列化を無分別に行うだけでは、並列処理が約束するメリットを真に得ることができないと Howard Dierking が指摘しています。Howard Dierking | ツールボックス: 現実的なテスト データ、Dare Obasanjo のブログ、色分けコーディング ツール、その他: 今月は Scott が現実的なテスト データを生成する方法、Dare Obasanjo のブログ、判読性を高めるための色分けコーディングを紹介します。Scott Mitchell | CLR 徹底解剖: Silverlight 2 のセキュリティ: CLR チームの Andrew Dai が、Silverlight アプリケーションの特権を持つコードと特権のないコードを厳密に分離する透過性モデルについて説明します。Andrew Dai
基本的な本能: XML リテラルを使用した動的なデータ入力: Windows Presentation Foundation (WPF)、XAML、および Visual Basic の幅広い XML のサポートを利用してユーザー インターフェイスを動的に生成する方法について説明します。Beth Massi | Cutting Edge: WPF および Silverlight 2 でコードを再利用する: Web ベースの Silverlight 2 アプリケーションとデスクトップの WPF アプリケーションには、よく似たところがあります。今回の記事では、両方のアプリケーションのコードを簡単に再利用できるようにします。Dino Esposito | 実践的なパターン: 凝集度と結合度: アプリケーションの柔軟性を高めて再利用しやすくするために、より高い凝集度と緩い結合度を実現できる、いくつかの設計パターンを紹介します。Jeremy Miller
サービス ステーション: WCF ベースのサービスでの承認: Windows Communication Foundation (WCF) では、サービスにおける承認を実装するために、簡単なロールベースのシステムと、さらに強力かつ複雑なクレーム ベースの API を提供します。Dominick Baier and Christian Weyer | 基礎: 永続性のあるサービスで状態を管理する: 今月号で Juval Lowy は、長時間実行されるワークフローの状態管理を改善するために、プロキシやサービスをいつメモリに保持すればよいかなどの疑問に取り組んでいます。Juval Lowy | Windows と C++: 高性能アルゴリズムについて調べる: アルゴリズム、操作対象のデータ、およびデザイン対象のハードウェアを詳細に検討することで、効率が驚くほど向上するようすを紹介します。Kenny Kerr
いろいろな場所へ: Tablet PC 向けのインク対応アプリケーション: Tablet PC SDK、InkEdit ActiveX コントロール、および InkPicture ActiveX コントロールを使用してインク対応アプリケーションを簡単に作成する方法を説明します。Gus Class | .NET の問題: 偽共有: 今月は、マルチコア システムのメモリ アクセスの問題と診断、および並列コンピューティング アプリケーションでの偽共有の回避についてです。Stephen Toub, Igor Ostrovsky, and Huseyin Yildiz | { End Bracket }: 恐れずに処理内容を破棄する: Josh Phillips が、今日の処理能力を活用して投機的な計算 (つまり、前もって処理を実行し、ユーザーが求めていない可能性のある結果も用意しておくこと) を行うことを提案します。Josh Phillips


パラダイム シフト
並列プログラミングの設計上の考慮事項
David Callahan

この記事は、Visual Studio ツールのプレリリース版を基にしています。ここに記載されているすべての情報は、変更される場合があります。

この記事では、次の内容について説明します。
  • 並列コンピューティング
  • 同時実行プログラミング
  • パフォーマンスの向上
 この記事では、次のテクノロジを使用しています。
マルチスレッド
目次
同時実行と並列処理
構造化マルチスレッド
データ並列処理
データ フロー
ストリーミング並列処理
単一のプログラム、複数のデータ
同時実行データ構造
まとめ
1986 年ごろから 2002 年にかけて、マイクロプロセッサの処理能力は年 52% の割合で向上しました。この驚くべきテクノロジの進化は、ムーアの法則どおりの継続的なトランジスタのコスト低下と、プロセッサ ベンダの優れたエンジニアリングという 2 つの要素のたまものです。この組み合わせのことを、マイクロソフトの研究員 Jim Larus は "ムーアの配当" と呼んでいます。彼は、この "ムーアの配当" が、今日のソフトウェア産業の誕生およびコンピュータの普及に果たした役割について考察しています (go.microsoft.com/fwlink/?LinkId=124628 を参照)。
ソフトウェアを開発する側では、この状況を "フリー ランチ (無料で支給される昼食)" と呼んでいます。ハードウェアをアップグレードするだけでアプリケーションのパフォーマンスが向上するからです (このテーマの詳細については、ddj.com/184405990 で公開されている「A Fundamental Turn Toward Concurrency in Software (ソフトウェアにおける同時実行への大きな転換)」を参照してください)。
ただし、このモデルは変化しつつあり、近ごろはプロセッサを追加することによってパフォーマンスの改善が図られています。いわゆるマルチコア システムが広く使用されるようになりました。もちろん、マルチコアのアプローチでパフォーマンスを向上させるには、複数のアクティビティを同時に実行できるソフトウェアが必要です。そこで、逐次処理の手法で問題なく動作する機能も、複数のプロセッサを使用して実行できるように記述する必要があります (ただし、マルチプロセッサ コンピュータで実行した場合にパフォーマンスの向上が見込める場合)。

同時実行と並列処理
随分以前から、プログラマは並列処理 (同時実行) に関連するプログラミング上の課題に取り組まざるを得なくなっています。応答性を維持するため、入力イベントに応答するスレッドから待ち時間の長いアクティビティをオフロードする手段が必要だったからです。かつては、オフロード対象のアクティビティのほとんどがファイル入出力に関係するものでした。ところが最近は、Web サービスとの対話に関係するアクティビティであるケースがますます増えてきています。
この一般的なプログラミング上の懸念を解消するため、Microsoft .NET Framework には、非同期プログラミング モデルとバックグラウンド ワーカーの概念が用意されています。並列プログラミングにおける複雑な処理の多くについて、同時実行プログラミングにも類似の処理が存在しますが、その基本的なパターンと目的は異なります。マルチコア プロセッサを導入しても、同時実行プログラミングの必要性が抑えられるわけではありません。むしろ、その目的はパフォーマンスの向上にあり、システムで実行されるバックグラウンド アクティビティとその他の計算を最適化する手法として使用できます。
また、同時実行の手法が取り入れられている身近な例として、サーバー アプリケーションがあります。Web サーバーなどのアプリケーションには、独立した要求のストリームが渡されます。各プログラムでは、多数の要求を同時に実行してシステムのスループットを向上させるために、通常、要求ごとに個別のスレッドを使用します。ただし、それぞれの要求は逐次処理されます。結果として、1 秒あたりに処理できる要求の数は増えますが、個別の要求の待ち時間 (要求ごとの秒数) は改善されません。
これらのアプリケーションも、"フリー ランチ" の恩恵に浴してきました。マルチコア処理によって追加のコストが抑えられ、スループットが向上するため、しばらくはその状態が続くでしょう。とはいえ、サーバー環境においても、待ち時間の影響を大きく受ける要求のパフォーマンスを許容範囲まで引き上げるために、ゆくゆくは並列プログラミングの手法を使用しなければならなくなるはずです。
ご存知のとおり、同時実行プログラミングは専門家にとっても難しい手法です。論理的に独立した要求がさまざまなリソース (ディクショナリ、バッファ プール、データベース接続など) を共有する場合、プログラマはその調整を行う必要がありますが、その際に新たな問題が生じます。これらの問題 (データ競合、デッドロック、ライブロックなど) は、通常、同時実行タスクがプログラムの同じデータ オブジェクトを操作しようとしたときに生じるさまざまな不確定動作に起因しています。Rahul Patil と Boby George による最近の記事「同時実行の問題を特定するためのツールと手法」(MSDN Magazine 2008 年 6 月号、msdn.microsoft.com/magazine/cc546569) で述べられているように、これらの問題が原因で、テストやデバッグなどの基本的なソフトウェア開発作業はきわめて難しくなります。
並列プログラミングが同時実行プログラミングと異なるのは、論理的には単一のタスクであるもの (これは、すべての主要な言語でサポートされている一般的な逐次構造体を使用して表現できます) を利用して同時実行の機会を確保する必要があるという点です (このような機会を確保するためのさまざまなアプローチについては、この記事の後半で説明します)。ただし、同時実行の機会がデータ オブジェクトを共有するサブタスクと共に得られた場合は、ロックと競合に注意する必要があります。したがって、並列プログラミングにおいては、逐次プログラムに伴う正確性とセキュリティの課題に加えて、並列処理と共有リソースへの同時アクセスに伴う難しい課題に対処しなければなりません。
追加の概念、新たなエラー処理、テストの難しさが組み合わさると、どんな開発者も尻込みしてしまいます。開発者は本当にこのようなものを求めているのでしょうか。答えは明らかにノーです。しかし、必要なパフォーマンスを確保するために、多くの開発者がこの泥沼にはまり込む羽目になります。マイクロソフトは主要な問題の一部に対処できるソリューションの開発に努めていますが、生産性の高いソリューション セットはまだ完成していません。
昨年、マイクロソフトは、並列プログラムの作成方法と効率的な実行方法を示すと共に、ムーアの配当を顧客への価値に転換できる次世代アプリケーションの作成を奨励するために、並列コンピューティング イニシアティブ (go.microsoft.com/fwlink/?LinkId=124050) を発表しました。並列処理の考え方については、後ほど検討します。今月号に掲載されている Stephen Toub と Hazim Shafi の記事「次期バージョンの Visual Studio で強化される並列処理のサポート」では、これらのアプローチをサポートするためにマイクロソフトが提供しているライブラリとツールの一部が紹介されています。同じく今月号の Joe Duffy の記事「マルチスレッド コードでよく見られる 11 の問題を解決する」では、同時実行アプリケーションの安全性を高めるための手法とアプローチについて説明されています。
リファレンス

スタンフォード大学の Norman Jouppi 氏によるレクチャー「The Future Evolution of High-Performance Microprocessors (高パフォーマンス マイクロプロセッサの今後の進化)」
stanford.edu/class/ee380/Abstracts/060927.html

Shekhar Borkar 氏のレクチャー「Thousand Core Chips: A Technology Perspective (1000 個のコア チップ : テクノロジの展望)」
videos.dac.com/44th/papers/42_1.pdf
先ほど "同時実行の機会" というフレーズを使いましたが、これは、並列プログラミングと同時実行プログラミングの違いをさらに明確にするものです。開発者が非同期プログラミング パターンやバックグラウンド ワーカーを使用する場合、またはサーバーで同時要求を処理する場合、すべてのスレッドによる処理が進行することを想定しています。オペレーティング システムのスケジューラは、すべてのスレッドによってリソースが公平に共有されるようにします。ただし、これは並列プログラミングにおいては役に立ちません。特に、新しいハードウェア システムに合わせて拡張できるアプリケーションを開発する場合には、適切な動作とは言えません。
"フリー ランチ" の恩恵に浴する (つまり、ハードウェアをアップグレードするだけでソフトウェアのパフォーマンス向上を達成する) には、将来にわたって利用できるような方法で並列処理の機会を提供する必要があります。並列処理の実装におけるこのようなシフトと、それに対する筆者の考え方 (筆者は、並列プログラミングに取り組む開発者は、問題をこれまでよりもずっと多くのタスクに切り分ける必要があると考えています) を強調するため、この記事では、"スレッド" の代わりに "タスク" という用語を使用します。
並列プログラミング システムの実装では、必要に応じて、それらのタスクをシステム スレッドとプロセッサにマッピングしなければなりません。並列プログラムを効率的に実行するためにシステム リソースをオペレーティング システムから取得し、プロセス内で管理する方法は、根本から異なっています。これに気が付くのは、ごくわずかなプログラマだけです。これはスレッド プールの強化版のようなものですが、並列プログラムによって指定されたとおりに単純にスレッドを管理することではなく、アプリケーションにおける並列処理の機会を現在のハードウェアで使用できるリソースと一致させることに重きが置かれています。
同時実行プログラミング (特にサーバー関連のプログラミング) における問題のほとんどは、実行時間の長いスレッドによる共有変数へのアクセスをロックなどのツールを使用して調整する際に生じます。タスクを利用する並列プログラミングにシフトすると、新しい概念を使用できるようになります。この概念は、"タスク A の後に実行されるタスク B" という処理の流れと、コーディネーション プリミティブを用いて説明できます。この概念によって、プログラマは作業のスケジュールについて考えることができるようになります。通常、このスケジュールは、プログラムのアルゴリズム構造に必然的に合致し、並列プログラミングの抽象化の構造化された使用に基づきます。プログラムの抽象化と並列アルゴリズムが適合していれば、従来の同時実行メカニズム (ロックやイベントなど) の必要性が大幅に減り、同時実行プログラムに伴う多くのリスクを (完全に取り除くことはできませんが) 回避することができます。
次に、並列プログラミングに対するいくつかの主要なアプローチについて説明し、その利用方法を開発中の抽象化を通じて解説します。特に、C++ PPL (Parallel Pattern Library) と C# を使用した Parallel Extensions to .NET (go.microsoft.com/fwlink/?LinkId=124621 からダウンロード可能) を取り上げます。

構造化マルチスレッド
私たちが最も頻繁に利用する問題解決パターンは、分割統治です。分割統治とは、大きな問題を明確なやり取りを通じて小さな問題に分割し、個別に対処してから、その結果を組み合わせて元の問題を解決する手法です。この手法は、大企業の事業から、近所の一品持ち寄りパーティーに至るまで、さまざまな場面の問題を解決するために使用されています。もちろん、この考え方を再帰的に適用したものが並列プログラミングの基盤にもなっています。
構造化マルチスレッドとは、ブロック構造化された主要な逐次実行ステートメントを並列化したものを提供することを意味します。具体的には、"A が評価されてから B が評価される" というセマンティクスを持つ逐次実行型の複合ステートメント { A; B; } を、A と B を同時に評価できるようにすることによって、並列ステートメント化するような場合を言います。ただし、この場合は構造体全体の処理は完了しません。両方のサブタスクが終了するまでは、制御が次の構造体に移ります。これは古くからある概念で、これまで並列実行 (cobegin) ステートメントとして教えられてきました。構造を強調するために、"fork と join の並列実行" と呼ばれることもあります。この基本的な考え方は、各反復処理によって、タスクを他のすべての反復処理と同時に評価できるように定義されるループにも当てはめることができます。このような並列ループが完了するのは、すべての反復タスクが完了したときです。
分割統治の概念の身近な例が、有名なクイック ソート アルゴリズムです。ここでは、このアルゴリズムの単純な並列処理について、C++ 構造体を使用して説明します。この基本的なアルゴリズムは、データの配列を受け取り、最初の要素を基準として使用します。そしてデータを 2 つに分け、基準値よりも小さい値が前に、基準値よりも大きい値が後にくるように配列を変更します。さらに、この処理を再帰的に行ってデータを並べ替えます。対象データが少なくなると、オーバーヘッドを抑えるために、この処理を中止して挿入ソートなどの非再帰アルゴリズムが使用される場合もあります。
図 1 に、マイクロソフトで開発中の 2 つの機能を示します。図中の最初の機能は、新しい C++ ラムダ構文です。これを使用することで、式またはステートメント リストを関数オブジェクトとして非常に簡単にキャプチャできます。次の構文は、呼び出されたときに中かっこ内のコードを評価する関数オブジェクトを作成します。
[=] { ParQuickSort(data, mid); }
図 1 クイック ソート
// C++ using the Parallel Pattern Library
template<class T>
void ParQuickSort(T * data, int length, T* scratch) {
  if(length < Threshold)  InsertionSort(data,length)
  else {
    int mid = ParPartition(data[0], data,
                length, scratch, /*inplace*/true);
    parallel_invoke(
                [=] { ParQuickSort(data, mid); },
                [=] { ParQuickSort(data+mid, length-mid); });
  }
}
先頭の [=] でラムダ式であることを示すと共に、式で参照される外のスコープの変数をオブジェクトにコピーし、ラムダ式本体のそれらの変数への参照でそれらのコピーを参照するように指定しています。
parallel_invoke はテンプレート アルゴリズムです。この場合は、2 つの関数オブジェクトを受け取り、それぞれを個別のタスクとして評価することで、それらのタスクを同時に実行できるようにしています。両方のタスクが完了すると (この場合は両方の再帰的並べ替えが完了すると)、parallel_invoke が値を返し、並べ替えが完了します。
この例の並列処理は、分割統治型並列処理の再帰的な適用に基づいています。各レベルで生成される子タスクは 2 つだけですが、計算の末端に近づくにつれ、並べ替えているデータのサイズに比例して多数のタスクが生成されるようになります。このコードは同時実行のすべてを表しています。このようなプログラムは、より大きな問題に対処する必要が生じた場合や、コアの数が増加した場合に、直ちに拡張することができます。ただし、問題の規模が一定であっても、オーバーヘッドの生じる実際のコンピュータには、スケーラビリティの制限が必ず存在します。これは、有名なアムダールの法則の結果です。そこで、こうしたオーバーヘッドを継続的に削減することが、プラットフォーム ベンダのエンジニアリング上の重要な目標となります。これは、システムが変わると、Threshold (図 1 を参照) のような値の選択が難しくなる場合があることと、時間の経過と共に予測がさらに困難になる可能性があることを示しています。これらのプログラムは、過剰なオーバーヘッドが生じないように十分大きくなければなりませんが、将来の拡張の妨げとならない程度にしておく必要もあります。
このテンプレート アルゴリズムは、今月号の Stephen Toub と Hazim Shafi の記事で紹介されている PPL に含まれています。PPL には、parallel_invoke に加えて、標準テンプレート ライブラリ (STL) の for_each に似た parallel_for_each も用意されています。parallel_for_each の場合、すべての反復処理が他の反復タスクと同時に実行できる個別のタスクとなり、すべての反復タスクが完了すると、parallel_for_each が値を返します。また、共通のタスク グループに関連付けられている個別のタスクを作成し、それらすべてが完了するのを待機する、それほど構造化されていない手法も利用できます。これは Cilk 生成プリミティブ (supertech.csail.mit.edu/cilk) と同様の基本的な機能を提供しますが、標準的な C++ 機能に基づいて構築されています。
並列ループは、レイトレーシングの問題を処理するためのコードで使用されることがあります。このような問題は各出力ピクセルで並列的であり、図 2 のようなコードでキャプチャできます。これは、Parallel Extensions to .NET の Parallel.For メソッドを使用して表したものです。Parallel Extensions to .NET には、C++ 開発者向けの PPL でサポートされているパターンと同様の、マネージ開発者向けの基本的なパターンが含まれています。ここでは、入れ子になったシンプルなループを使用して、長方形の画面上の各ピクセルに対応するタスクの空間を説明します。このコードは、ループ本体で呼び出されるさまざまなメソッドが同時実行に対して安全であることを前提にしています。
図 2 並列ループ
// C# using Parallel Extensions to the .NET Framework
public void RenderParallel(Scene scene, Int32[] rgb)
{
    Parallel.For(0, screenHeight, y =>
    {
        Parallel.For (0, screenWidth, x =>
        {
            Color color = TraceRay(new Ray(camera.Pos,
                GetPoint(x, y, scene.Camera)), scene, 0);
            rgb[x + y*screenWidth] = color.ToInt32();
        });
    });
}
同時実行の安全性に関する Joe Duffy の記事をもう一度参照してください。並列プログラミングの黎明期には、開発者はこうした懸念事項に頭を悩ませることになります。用心を怠らないようにしてください。
構造化マルチスレッドは、データ構造が自然な場合 (再帰的な構造をしていることが少なく、不規則な構造をしていることはほとんどない場合) の並列処理の操作に最適です。並列処理はこうした構造を反映できます。また、問題の至る所にデータ フローがある場合にも対応可能です。次の例では、グラフを位相的順序でトラバースすることで、すべての先行要素が見つかるまではノードに到達しないようにしています。各ノードには、先行要素の数に初期化されるカウント フィールドがあります。ノードに到達すると、後続要素の数がデクリメントされます (複数の先行タスクがこの操作を同時に実行しようとすることがあるため、安全性を確保しておいてください)。カウントがゼロになったら、そのノードに到達できます。
// C++
void topsort(Graph * g, void (*action)(Node*)) {
    g->forall_nodes([=] (Node *n) {
        n->count = n->num_predecessors();
        n->root = (n->count == 0);
    });
    g->forall_nodes([=] (Node *n) {
        if(n->root) visit(n, action);
    });
}
ここでは、関数オブジェクトによってパラメータ化されるメソッドをグラフがエクスポートすることを想定しています。このメソッドは、グラフ内のすべてのノードを並列でトラバースし、関数を適用します。その関数は、PPL を内部で使用してその同時実行を実装します。この場合、次の 2 段階の処理を行います。まず、先行要素をカウントし、ルート ノードを特定します。次に、各ルートから深さ優先の検索を開始します。この検索で後続要素の数がデクリメントされ、最終的に後続要素に到達します。その関数は図 3 のようになります。
図 3 深さ優先の検索
// C++ using the Parallel Pattern Library
// Assumes all predecessors have been visited.
void visit(Node *n, void (*action)(Node*)) {
    (*action)(n);
    // assume n->successors is some kind of STL container
    parallel_for_each(n->successors.begin(),
         n->successors.end(),
         [=](Node *s) {
           if(atomic_decrement(s->count) == 0) // safely does "-- s->count"
            visit(s, action);
    });
}
parallel_for_each メソッドは、後続要素の一覧をトラバースし、関数オブジェクトをそれぞれに適用し、それらの操作を並列に実行できるようにします。ここには、同時アクセスを調整するための方法を使用する atomic_decrement 関数は示されていません。ここで、こうした新しい並列アルゴリズムにも、従来からある同時実行に関する問題が忍び寄ってくることにお気付きになるでしょう。これらの問題は、レイトレーシングの例のように、要素の操作が複雑化するに従っていっそう厄介になります。
このアルゴリズムの構造では、"action" にそのパラメータへの排他アクセスが与えられるので、アクションがそれらのフィールドを更新する場合に追加のロックは必要ありません。また、すべての先行要素が既に更新されており、変更中ではないことと、後続要素は更新されておらず、このアクションが完了するまでは変更されないことが保証されます。適切な並列アルゴリズムを構築するには、どの状態が安定しており、どの状態が同時に更新される可能性があるかを推測する能力が欠かせません。
構造化マルチスレッドの場合、並列処理の機会 (部分的に順序付けられている計算の機会も含む) を簡単に表現できます。作業をワーカー スレッドにマッピングするための多数のメカニズムによって、基本的なアルゴリズムがわかりにくくなることはありません。これが構造化マルチスレッドの強みです。こうした特徴により、ここで紹介した強力な構成モードが実現されます。この構成モードでは、データ構造でいくつかの基本的な並列トラバーサル メソッド (Graph::forall_nodes など) を提供できます。これらのメソッドは、より高度な並列アルゴリズムを構築する際に再利用可能です。さらに、並列処理を完全に記述する方が、2 つまたは 4 つのプロセッサに十分な量を模索するよりも簡単です。そのうえ、来年のコンピュータには 8 つのプロセッサが搭載される可能性がありますが、それに合わせて自然に拡張できるようになります。これこそ "フリー ランチ" の恩恵です。

データ並列処理
データ並列処理とは、データの集計に対する一般的な操作を応用したものです。新しい集計データを生成したり、集計をスカラ値まで減らしたりするために使用されます。並列処理では、同じ論理操作を周囲の要素から独立した各要素に対して実行します。集計操作をさまざまなレベルでサポートしている言語は数多くありますが、最も成功を収めているのは、データベースで使用される SQL です。LINQ は、C# と Visual Basic の両方で SQL スタイルの演算子を直接サポートしており、LINQ で表現されたクエリを ADO.NET などのデータ プロバイダに渡したり、オブジェクトのメモリ内コレクションや XML ドキュメントに対して評価したりできます。
Parallel Extensions to .NET の一部は、クエリの並列評価を含む LINQ to Objects と LINQ to XML の実装です。この実装は PLINQ と呼ばれます。これを使用すると、データの集計を簡単に操作できます。次の例は、統計的クラスタリングのための標準的な K-means アルゴリズムの中心概念を示しています。各ステップにおいて、領域内にクラスタ中心の候補である K 個のポイントが存在します。すべてのポイントを最も近いクラスタにマッピングし、クラスタ内のポイント (同じクラスタにマッピングされているすべてのポイント) の位置の平均を計算することで、そのクラスタの中心を再計算します。この計算は、収束条件が満たされるまで、つまりクラスタ中心の位置が安定するまで繰り返されます。図 4 に示すように、このアルゴリズム記述の重要なループは、PLINQ にかなり直接的に変換されています。
図 4 中心を探す
// C# using PLINQ
var q = from p in points.AsParallel()
        let center = nearestCenter(p, clusters)
                     // "index" of nearest cluster to p
        group p by center into g
        select new
        {
             index = g.Key,
             count = g.Count(),
             position = g.Aggregate(new Vector(0, 0, 0),
                    (accumulated, element) => accumulated + element,
                    (accumulated1, accumulated2) =>
                                     accumulated1 + accumulated2,
                    (accumulated) => accumulated
                    ) / g.Count()
        };
var newclusters = q.ToList();
LINQ と PLINQ の違いは、データ コレクション ポイントの AsParallel メソッドにあります。この例は、LINQ にはマッピングを減らす重要なパターンが含まれていること、また、主流言語に完全に統合されていることも示しています。この例で難しいポイントの 1 つが、Aggregate 演算子の動作です。3 番目のパラメータは、部分和を結合するためのメカニズムを提供するデリゲートです。このメソッドが提供されると、チャンクへの入力をブロックし、各チャンクを並列的に減らしてから、部分ごとの結果を結合することで、実装が並列的に行われます。
データ並列処理には大きな強みがあります。それは、データ構造の想定が複雑化することのある構造化マルチスレッドのアプローチを適用した場合に比べて、通常は、適切なアルゴリズムをはるかに簡潔かつ読みやすい形で表現できるという特徴です。また、より抽象的な記述を行うことでシステムを最適化する機会が得られます (同じ方法を手動で行うとアルゴリズムが完全に不明確になります)。さらに、こうした高レベルの表現によって、実行対象 (マルチコア CPU、GPU、またはクラスタへのスケールアウト) と共に優れた柔軟性が提供されます。また、nearestCenter などのリーフ関数を使用する限り副作用がなく、スレッド指向プログラミングに伴うデータ競合やデッドロックなどの問題に一切悩まされずに済みます。

並列処理を活用するためによく使用されるのが、パイプライン処理を使用する手法です。このモデルでは、データ アイテムがパイプラインのさまざまなステージ間を移動し、各ステージで検証および変換されてから次のステージに渡されます。データ値にグラフのノード間を移動させ、入力データを使用できるかどうかに基づいて計算をトリガするというアイデアを一般化したものが、データ フローです。各ノードを同時に実行するか、1 つのノードを異なる入力データに対して複数回アクティブ化することで、並列処理が行われます。
Parallel Extensions to .NET は、個別のタスク (Task 型) を明示的に作成し (これは、構造化マルチタスクを実装するための基本的なメカニズムです)、最初のタスクの完了時に実行が開始された 2 番目のタスクを特定する機能をサポートしています。フューチャの概念は、命令型プログラミングの世界とデータ フロー型プログラミングの世界をつなぐものとして利用されています。フューチャは、計算によって最終的に生成される値の名前です。このような区別があるため、値が判明する前にその値の処理方法を定義することができるのです。
フューチャの continueWith メソッドは、タスク (フューチャ値を使用できる場合に実行されるタスク) を作成するために使用されるデリゲートによってパラメータ化されます。continueWith の呼び出しの結果は、デリゲート パラメータの結果を識別する新しいフューチャです。命令型プログラミングでは、タスクの評価を通じて副作用が調べられることが多いため、continueWith は Task のメソッドとしても使用できます。
このスタイルの例として、Strassen 行列乗算アルゴリズムにおける並列処理について考えてみます。これは、基本的な行列乗算アルゴリズムのブロック指向バージョンです。2 つの入力行列はそれぞれ 4 つのサブブロックに分割され、その後、出力行列のサブブロックを作成するために代数的に結合されます (詳細については、Strassen アルゴリズムに関する Wikipedia の記事 (wikipedia.org/wiki/Strassen_algorithm) を参照してください)。
これらのタスクの 1 つは、次のようになります。
// C# using Parallel Extensions to the .NET Framework
var m1 = Future.StartNew(() => (A(1,1)+B(1,1))*(A(2,2)+B(2,2));
計算がわかりやすいように、コードの代わりにデリゲート内部で数学的な表記を使用しました。A と B は入力行列を、A(1,1) は行列 A の左上のサブブロックを表します。加算には行列で一般的な方法を、乗算には Strassen アルゴリズムを再帰的に適用する方法を使用しています。出力は、この式の結果に相当する一時的な行列です。
最初の 7 つのサブタスクは独立していますが、最後の 4 つは入力として使用される最初の 7 つのサブタスクに依存しています。基本的なデータ フローのグラフは、図 5 のようになります。このとき、タスク c11 は、タスク m2 およびタスク m3 の結果に依存しています。入力を使用できる場合にそのタスクを実行できるようにしてみましょう。C# では、これを次のように表現できます。
var c11 = Task.ContinueWhenAll(delegate { ... },  m2, m3);
図 5 サブタスク (クリックすると拡大画像が表示されます)
これは、中粒度のデータ フローと呼ばれるデータ フローを表しています。各演算が単一の算術演算ということもあり得る細粒度のデータ フローとは対照的に、中粒度のデータ フローでは、数百から数千の演算が計算の単位となります。"when all (すべての...が...したとき)" および "when any (いずれかの...が...したとき)" という概念を処理できるいくつかの便利なメソッドを使用するとそのコードがわかりやすくなりますが、Stephen Toub のブログ記事 (blogs.msdn.com/pfxteam/archive/2008/07/23/8768673.aspx を参照) で説明されているように、これらのメソッドは基本的なメカニズムに基づいて実装することができます。
筆者は、構造化マルチスレッドの場合と同じように、並列処理の機会を特定しました。シングル プロセッサしかない場合は、サブタスクは作成順に逐次実行するとよいでしょう。ただし、追加リソースがある場合は、サブタスク間の実行順序の制約をキャプチャしておくことで、追加リソースのメリットを享受するチャンスが生まれます。構造化マルチスレッドの場合とは異なり、基本的に 1 つのタスクがデータ フロー グラフを列挙できるようにし、個々のサブタスクがその結果の使用方法と結合方法に関知しないようにしておくと便利です。これは、並列アルゴリズムの処理に対する一般的なメカニズムの別の面です。
アプリケーションに流れ込んだデータを、そのフロー中に処理しなければならないことはよくあります。この場合、データが移動するパスはかなり安定しています。そこで、タスクを他のタスクの完了に関連付けずに、次のデータ アイテムの可用性と関連付けることをお勧めします。このモデルは、異なるレートで受信したセンサ データのストリームが合流する地点でアルゴリズムによる決定が行われる、ロボット工学の特定の領域で特に役立ちます。
Microsoft Robotics SDK (go.microsoft.com/fwlink/?LinkId=124622) はこのアプローチを採用しており、その中核となる概念には、データのストリーム (ポートと呼ばれます) と、データ (メッセージ) の着信時にアクティブ化されるタスクのバインドが含まれています。当然のことながら、今ここで考察しているのは、マルチコアへのシフトではなく Web サーバーなどへのシフトに起因する問題です。Web サーバーの重要な特徴は同時実行です。これには、アプリケーションのアーキテクチャ全体の一部分として対処しなければなりません。同様の懸念事項は、ロボット工学の領域に含まれていない分散アプリケーションにも当てはまりますが、ここでの話題には直接関係ありません。

ストリーミング並列処理
複数のコアだけでなく、複数層のメモリ階層 (レジスタ、1 つ以上のレベルのオンチップ キャッシュ、DRAM メモリ、およびディスクに対するデマンド ページング) もコンピュータ アーキテクチャの重要な特徴です。さいわいなことに、ほとんどのプログラマは、システム アーキテクチャのこの側面についてはあまり詳しくありません (少なくとも関心を払っていません)。作成するプログラムがそれほど大きくなく、ほとんどのメモリ参照がすぐに返されるキャッシュにそれらが十分収まるためです。ただし、データ値がオンチップ キャッシュに含まれていない場合、そのデータ値を DRAM から取得するのに数百サイクル費やしてしまうことがあります。このデータを提供する際の遅延により、プロセッサがデータを長時間待機するため、プログラムの実行速度が低下しているように見えます。
一部のプロセッサ アーキテクチャは、物理的な処理コアごとに複数の論理プロセッサをサポートしています。これは一般に (ハードウェア) マルチスレッドと呼ばれ、その一部が主流のプロセッサで使用されてきました (たとえば、Intel では一部の製品でこれをハイパースレッドと呼んでいます)。マルチスレッドの目的は、メモリ アクセスの遅延を許容することにあります。マルチスレッドの場合、ある論理ハードウェア スレッドがメモリで待機しているときに、他のハードウェア スレッドから命令を出すことができます。この手法は現在の GPU にも取り入れられていますが、CPU の場合と比べて、より積極的に使用されています。
処理コアの数が増加すると、メモリ システム上でより多くの要求を行うことができるようになるため、帯域幅の制限などの新たな問題が発生します。プロセッサが 1 秒あたりに DRAM メモリとの間で実行できる転送処理は限られています。この制限に達した場合、並列処理をそれ以上使用しても効果はありません。単に追加のスレッドが追加のメモリ要求を生成し、その要求はキューの最後尾に登録され、メモリ コントローラによる処理を待機することになります。通常、GPU はより高い総帯域幅 (ギガバイト/秒で測定) をサポートするメモリ サブシステムを備えています。GPU ははるかに多くの並列処理をサポートしており (また、実際に実装するものと思われます)、帯域幅を追加することでパフォーマンスの向上を期待できるためです。
現在のマルチコア チップ アーキテクチャでは、メモリ帯域幅を広げるよりもコアの数を増やす方が簡単です。したがって、データ セットがメモリに収まらない事態に対処する場合は、メモリ階層の使用を検討することが重要です。こうした不均衡を解消するために活用できるのが、ストリーム処理と呼ばれるプログラミング スタイルです。このプログラミング スタイルでは、データ ブロックをオンチップ キャッシュ (またはプライベート メモリ) に展開し、そのデータに対してできるだけ多くの操作を実行してから次のブロックに置き換えます。それらの操作は、複数のコアを使用して内部で並列的に処理されるか、または、データ フロー スタイルでパイプライン処理されますが、重要なのは、データがキャッシュ内にある間に最大限の作業を行うことです。
専用言語を使用することで、ストリーミング アルゴリズムを指定し、その実行を綿密に計画できるようにする案もありましたが、多くの場合、慎重なスケジューリングでもこれを実現することができます。たとえば、この手法は QuickSort の例に適用できます。並べ替えるデータ セットのサイズが大きすぎてキャッシュに収まらない場合、単純な Work-stealing (ワーク スティーリング) のアプローチでは、最も大きく粒度が粗い下位の問題を異なるコアに分けてスケジュールする傾向にあります。コアでは独立したデータ セットを処理し、共有オンチップ キャッシュは活用しません。
ただし、アルゴリズムを変更してキャッシュに収まるデータ セットにのみ並列処理を行うようにすると、図 6 に示すように、ストリーミングのメリットを得ることができます。この例でも大きな問題をより小さな問題に分割していますが (また、分割ステップで並列処理を使用しています)、下位の問題が両方同時にキャッシュに収まらない場合は逐次実行します。つまり、キャッシュに小さなデータ セットが配置された場合、そのデータ セットがキャッシュ内にある間に使用可能なすべてのリソースを使って完全に並べ替え、メモリには渡しません。並べ替えが終わってから、次のデータ セットの処理を開始します。
図 6 より小さなデータ セットでの並列処理の使用
// C++ using the Parallel Pattern Library
template<class T>
void ParQuickSort(T * data, int length, T* scratch, int cache_size) {
    if(length < Threshold)  InsertionSort(data,length)
    else {
           int mid = ParPartition(data[0], data,
                length, scratch, /*inplace*/true);
           if(sizeof(*data)*length < cache_size)
                parallel_invoke(
                  [=]{ ParQuickSort(data, mid, cache_size); },
                  [=]{ ParQuickSort(data+mid, length-mid, cache_size);});
           else {
                  ParQuickSort(data, mid, cache_size);
                  ParQuickSort(data+mid, length-mid, cache_size);
           }
    }
}
このコードは、キャッシュのサイズで明示的にパラメータ化する必要がありました。これは実装固有の特徴です。これは好ましくない特徴ですが、計算でシステムを他のジョブまたは同じジョブ内の他のタスクと共有している場合には問題になります。これは、複雑な並列システムのパフォーマンスを引き出す際の難しさの一部を如実に表しています。すべての問題にこうした厄介な点があるわけではありません。しかし、マルチコア チップでのパフォーマンスを向上させるためには、開発者は、一部のケースにおける並列処理とメモリ階層の相互作用を十分に考慮する必要があります。

単一のプログラム、複数のデータ
高パフォーマンス コンピューティングの領域では、1980 年代に行われた数多くの取り組みをベースに、技術および科学の分野のアプリケーションで並列処理が使用されてきました。問題の種類はデータの配列に対する並列ループに依存します。このとき、一般にループ本体のコード構造はかなり単純です。
その例として、前述のレイトレーシング フラグメントが挙げられます。登場した主流の並列処理モデルは、"単一のプログラム、複数のデータ" と呼ばれました。これはよく SPMD と略されます。この用語は、Michael Flynn によるコンピュータ アーキテクチャの古典的な分類 ("単一の命令、複数のデータ (SIMD)"、"複数の命令、複数のデータ (MIMD)" など) が基になっています。このモデルでは、プログラマは、単一の問題に論理的に関与するが作業を共有するプロセッサのセットとして各プロセッサ (ワーカー、スレッド) をとらえて、その動作について考えます。一般に、作業とは、配列の異なる部分を処理する独立したループの反復のことを表します。
SPMD スタイルにおける作業の共有の概念は、C、C++、および Fortran に対する拡張セットである OpenMP (openmp.org) の心臓部です。OpenMP の中核概念は、アクティビティの単一のスレッドがスレッドのグループにフォークし、それらが協調して共有ループを実行する並列領域です。このグループを調整するためにバリア同期メカニズムが使用され、グループ全体をループの入れ子から次のループの入れ子に移動させることで、グループのメンバよるデータ値の計算が終わる前にそのデータ値が読み取られないようにします。並列領域の最後にグループ全体が戻り、次の並列領域が開始されるまでは元の単一のスレッドが継続されます。
このスタイルの並列プログラミングは、メッセージ渡しをベースに構築されているノード間通信がデータをコピー (コピー先は、そのデータが必要なフェーズ境界) するために使用される、非共有メモリ システムでも使用できます。この手法は、膨大な数のプロセッサ ノードを使用することで、気候モデリングや薬剤設計などの問題への対処に必要なきわめて高いパフォーマンスを実現する場合に利用されています。
構造化マルチスレッドの場合と同じように、OpenMP の並列領域は関数の内部に位置することがあります。したがって、その関数の呼び出し元は、実装における並列処理の使用を認識している必要はありません。ただし、SPMD モデルでは、問題の作業がグループ内のワーカーにどのようにマッピングされているかを注意深く確認する必要があります。
負荷が均等ではない場合、たとえば、あるワーカーに他のグループ メンバのタスクよりも処理に時間がかかるタスクが割り当てられているような場合は、そのワーカーの処理が完了するまで他のグループ メンバには待機状態が強いられます。システム リソースが他のジョブと共有されることのある環境においても、同様の事態が考えられます。たとえば、OS が他の何らかのタスクを実行するために、あるワーカーの処理を中止した場合、残りのワーカーにこうした負荷の不均衡による影響が及びます。
将来のハードウェア システムには、種類の異なるプロセッサ コアが搭載される可能性があります (大量の電力を消費するが、単一のスレッドの処理が速い大きなコアと、消費電力が少なく、並列操作に最適化されている小さなコアの組み合わせなど)。こうした環境では、ワーカーへの作業のマッピング方法がきわめて複雑になるため、SPMD モデルを使用するのは非常に困難です。これらの問題は構造化マルチスレッドのアプローチによってうまく回避することができますが、その分、スケジューリングのオーバーヘッドが若干増え、OpenMP が適しているコードのメモリ階層を制御できなくなる可能性があります。

同時実行データ構造
ここまでは、もっぱら制御並列処理に重点を置いて、使用可能な複数のコアにマッピングできる独立したタスクの特定方法と記述方法を説明してきましたが、データ側に焦点を当てた並列処理もあります。タスクによってデータ構造が変更される場合、たとえば値がハッシュテーブルに挿入されるような場合には、その操作は同時実行タスクから論理的に独立している可能性があります。
データ構造の標準的な実装は同時アクセスをサポートしておらず、わかりにくく予測が不可能な、再現の難しいさまざまな方法で分割されることがあります。単純に単一のロックをデータ構造全体の周囲に配置すると、すべてのタスクがシリアル化されるようなボトルネックがプログラムに生まれ、並列処理が行われなくなることがあります (同時に使用されるデータ位置があまりに少なくなるため)。
そのため、並列制御の抽象化に加えて、一般的なデータ構造 (ハッシュテーブル、スタック、キュー、さまざまなセット表現) の新しい同時実行バージョンを作成することが重要です。これらのバージョンについては、同時に呼び出すことのできるサポート対象のメソッドのセマンティクスが既に定義されています。また、複数のタスクによってアクセスされたときにボトルネックが生じないように設計されます。
例として、接続されているグラフ コンポーネントを並列的に探すときの標準的なアプローチを考えてみましょう。基本的な方針として、グラフに対して深さ優先のトラバーサルを多数開始し、衝突が起きているノードを特定し、より小さいグラフを作成します。最上位の関数の基本的な構造は、図 7 のようになります。
図 7 深さ優先のトラバーサル
// C++
// assign to each Node::component a representative node in
// the connected component containing that node
void components(Graph * g) {
  g->forall_nodes([=] (Node * n) {
    n->component = UNASSIGNED;
  });
  Roots roots;
  EdgeTable edges;
  g->forall_nodes([&roots, &edges] (Node * n) {
    if(atomic_claim(n, n)) {
      roots.add(n);
      component_search(n,n, &edges);
    }
  });
  // recusively combine reduced graph (roots, edges)
       ...
}
各ノード ポイントに代表要素をポイントさせることで、コンポーネントを暗黙的に表現してみます。入力はグラフです。そのグラフは、構造化マルチスレッドの手法を使用してグラフをトラバースするために使用できる forall_nodes メソッドをサポートしているものとします。このメソッドは、各ノードに適用される関数オブジェクトによってパラメータ化されます。このインターフェイスでは並列アルゴリズムをグラフの構造の詳細から切り離していますが、構造化マルチスレッドの主要なプロパティは保持しています。同時実行はメソッドの内部に位置し、高度に構造化できます。
まず、コンポーネント フィールドを識別値に初期化します。次に、すべてのノードから並列的な深さ優先の検索を論理的に開始します。各検索では、最初に開始ノードを要求します。ノードに到達する検索は 1 つではないので、この要求は、そのノードに最初に到達する検索を特定するためのアトミック テストであると言えます。その関数は次のようになります。
// C++
// atomically set n->commponent to component if it is UNASSIGNED
// return true if and only this transition was made.
bool atomic_claim(Node * n, Node * component) {
    n->lock();
    Node * c = n->component;
    if(c == UNASSINGED) n->component = n;
    n->unlock();
    return c == UNASSIGNED;
}
筆者はノードごとのロックを想定しましたが、このシンプルな例では、Windows プリミティブを使用して、ポインタ値にインタロックされた比較交換を実行することもできます。ここでの重要な問題は、制御並列処理を十分確保していても、複数のタスクからの同時アクセスを防ぐための単一のグローバル ロックによってデータ並列処理が無効になり、並列処理の試みが妨げられることがあるということです。
必ずしもノードごとに 1 つのロックが必要というわけではありません。ノードをより少ないロックにマッピングすることもできますが、現在のプロセッサ数に合った数のロックを使用することで、スケーリングのボトルネックを招いてしまわないように注意する必要があります。そこで、明示的なロックを使用する代わりに、Jim Larus が提唱した "トランザクション メモリ" を使用することを検討してください。"トランザクション メモリ" のねらいは、同時に実行されるコードから独立させるべきコード区間を開発者が単純に宣言できるようにすることです。その実装では、これらのセマンティクスを保証するために、必要に応じてロックが導入されます。
新しいルート コンポーネントを特定したら、それを共有データ構造のルートに追加します。これは、論理的にはアルゴリズムの最初のステップで見つかったすべてのルート コンポーネントのセットです。ここでも、このコンテナのインスタンスが論理的に 1 つしかない場合は、並列的に追加できるようにその実装を最適化する必要があります。マイクロソフトは、PPL と Parallel Extensions to .NET の一部として、ビルディング ブロックとして使用できるベクタ、キュー、およびハッシュテーブルの適切な実装を提供する予定です。
あるノードから開始された深さ優先の検索は、隣接するノードに対して反復実行されます。また、そのコンポーネントに追加するノードの要求を試みます。これが成功した場合は、そのノードを再帰処理します (図 8 を参照)。
Figure 8 再帰検索
// C++ using the Parallel Pattern Library
// a depth first search from "n" through currently
// unassigned nodes which are
// set to refer to "component". Record inter-component edges in "edges"
void component_search(Node * n, Node * component, EdgeTable * edges) {

  parallel_for_each(n->adjacents.begin(),
    n->adjacents.end(),
    [=] (Node * adj) {
    if(atomic_claim(adj, component)
      component_search(adj, component, edges);
    else if(adj->component != component) {
      edges->insert(adj->component, component);
    }
  });
}
要求が失敗した場合、つまりこのタスクまたは別のタスクがそのノードに既に到達している場合は、それが別のコンポーネントに追加されていないかどうかを確認する必要があります。別のコンポーネントに追加されていた場合は、その 2 つのコンポーネントを共有データ構造 (EdgeTable) に記録します。このテーブルは、情報の重複を避けるために、同時ハッシュテーブルを使用して作成できます。この場合も、この構造は論理的に共有されるため、競合を防ぎ、効果的な並列処理を実行できるように、同時アクセスが適切にサポートされていることを確認する必要があります。
2 つの構造、ルート、およびエッジによって、当初の推定のコンポーネント間の接続を記録する論理グラフが構成されます。アルゴリズムを完了するには、この論理グラフ上で接続されているコンポーネントを探し、そのノード レベルの情報を最終的な代表要素 (ここでは示しません) で更新します。

まとめ
並列プログラミングの際には、パフォーマンスに関連する数多くの驚くべき事態が発生します。当然のことながら、データ並列性の損失はその 1 つにすぎません。この新しい問題に対する初めての取り組みが不可解な理由で失敗したり (原因の 1 つとして、ロックの使い忘れが挙げられます)、逐次実行の場合と比べて処理速度が遅くなったり (タスクが小さすぎる、ロックが多すぎる、キャッシュの有効性が失われている、メモリ帯域幅が十分でない、データが競合しているなどの理由が考えられます) したとしても、驚くに値しません。
マイクロソフトが並列プログラミングへの投資を続けていくうちに、抽象化だけでなく、関連して起こる問題の診断や回避に役立つツールの質も着実に高まるでしょう。また、ハードウェア自体の性能が向上し、さまざまなコストを抑える機能が組み込まれるものと思われますが、それには時間がかかります。
並列処理へのシフトは、ソフトウェア業界にとっての転換点です。新しい手法の採用を避けて通ることはできません。開発者は、アプリケーションの特定の部分 (現時点で時間の制約のある部分や、将来的により大きなデータ セットで実行されると予測される部分) に並列処理を取り入れる必要があります。この記事では、アプリケーションにおける並列処理の考え方とその使用方法をいくつか紹介し、幅広い並列コンピューティング イニシアティブの一環としてマイクロソフトで開発が進められている新しいツールを使用して、それらを具体的に説明しました。

インサイト : "次の 100 倍" へ
1975 年以降、マイクロプロセッサの処理能力は、クロック周波数とトランジスタ数の指数関数的な増加 (それぞれ 3,000 倍と 300,000 倍) に支えられて、10 年で 100 倍の割合で成長を続けてきました。命令の "効力" は 8 ～ 100 倍になり (8 ビットの ADD と SSE4.1 DPPS (dot-product of packed-4-SP-float-vectors) を比べてみてください)、ダイ上のキャッシュはかつてのハード ディスクほどの大きさになっています。事業として、次から次に押し寄せる波のごとく魅力的な新しいエクスペリエンスを提供するために、私たちは 100 倍の努力を続けてきました。こうした処理能力の向上は、船の帆を膨らませる風に例えることができます。
ただし、"次の 100 倍" を実現するためには、別の方角に向けて船を出さなければなりません。私たちは、2 年のサイクルでダイ (32、22、16、11 nm のノード) あたりのトランジスタの数が 16 倍になることで得られる "ムーアの法則の配当" を、まだまだ期待できます。とはいえ、私たちは、いくつかの成長曲線 (特に、電圧スケーリングと消費電力 (消費電力の壁)、命令レベルの並列性 (複雑性の壁)、メモリ待機時間 (メモリの壁)) が漸近的に落ち込んでいくポイントに立っていることに気が付いています。
消費電力の壁 マイクロプロセッサの動的消費電力は、NCV2f (トランジスタの開閉の数 × コンデンサの開閉 × 電圧の 2 乗 × 周波数) に比例します。各リソグラフィ ノードが小さくなると、ダイあたりのトランジスタの数はおそらく倍増し (↑N)、トランジスタのサイズは小さくなり (↓C)、開閉電圧は低くなります (↓V)。今日、電源電圧は 15V から 1V にまで下がっており、開閉電力は 1/100 以下に減っています。残念なことに、CMOS の最小しきい値電圧は 0.7 V なので、節約できるのは多くても (1.0/0.7)2=2X となります。こうした節約にもかかわらず、マイクロプロセッサの複雑性 (↑N) と 周波数 (↑f) は増したため、ダイの消費電力は、数平方センチメートルのシリコンごとに 1 W から 100 W に増え、今や実用的な冷却装置の限界に達しています。これ以上の余裕はありません。消費電力は、進行中のゼロサム ゲームと言えます。クロック周波数は、かつてのペースで増加することはありません。この領域では、"次の 100 倍" を実現することはできません。
複雑性の壁 高パフォーマンス マイクロプロセッサは、スレッドにおける命令レベルの並列性を活用するために、大胆なアウト オブ オーダー実行を使用しています。ただし、このアプローチからさらに大きな効果を得るのには、現実的に限界があります。連続しているコード自体と、そのデータの依存関係により、利用できる並列処理は制限されます。ハードウェアの側では、N 個の操作を並列して行うには、最大 N2 個の回路が余分に必要になることがあります。したがって、事前の設計と検証のコストがそれに比例して高くなります。並列処理ソフトウェアが登場したと仮定して、最も重要な点に触れましょう。消費電力の壁が示しているのは、エネルギー効率の良いマイクロアーキテクチャこそが優れたアーキテクチャであるということです。これは、"ナノ秒あたりの結果" よりも "ジュールあたりの結果" の方が重要視されることを表しています。
メモリの壁 DRAM アクセスの待機時間 (遅延) の改善は比較的ゆっくりとしたペースで行われています。そのため、CPU はキャッシュを使用することで DRAM の使用を避けています。ただし、キャッシュは負荷の大きい処理です。現在、フルキャッシュ ミスの場合は 300 クロックが費やされることがあります。経験から言うと、キャッシュ サイズを 4 倍にするとミスの割合は半分になります。CPU コアの複雑さの大部分は、時間のかかる予測不可能なメモリ アクセスに対処するためのものです。より簡単な手段は、メモリ帯域幅を拡張する方法です。そこで、メモリレベルの並列処理を適用できます。並列処理ソフトウェアのスレッドは、一度に多数の同時メモリ アクセスを発行します。各スレッドは、アクセスできるまで長時間待機しなければならない場合もありますが、並列計算の全体的なスループットは非常に高くなります。
今後 10 年の間、逐次プロセッサの処理能力は向上し続けるでしょうが、その勢いはこれまでと比べて著しく低下します。賢明な CPU デザイナとコンパイラ ライタは、どうにかして 5% や 10% の向上を実現するさまざまな手段を考え出すでしょう。これは好ましい取り組みです。数々の有用なソフトウェアは逐次処理型であり、それらにはアムダールの法則を適用できるためです。ただし、これでは私たちが真に求める "次の 100 倍" を実現するには至りません。そこで、並列処理を活用するソフトウェアが必要なのです。十分に魅力的なソフトウェアが登場したら、プロセッサ ベンダはすぐさま多数のコアを搭載した並列処理プロセッサと高帯域幅メモリ ソリューションを提供することでしょう。並列処理プロセッサの登場は、私たちの肩にかかっています。
—マイクロソフトの並列コンピューティング プラットフォーム チームのソフトウェア アーキテクト、Jan Gray

David Callahan は、Visual Studio の並列コンピューティング プラットフォーム チームの Distinguished Engineer 兼リード アーキテクトです。彼は、スーパーコンピュータ製造企業である Cray Inc. からマイクロソフトに移ってきました。並列化コンパイラ、並列アルゴリズム、並列アーキテクチャ、および並列言語を専門としています。


コーディング ツール
次期バージョンの Visual Studio で強化される並列処理のサポート
Stephen Toub および Hazim Shafi

この記事は、Visual C++、Visual Studio、および .NET Framework のプレリリース版に基づいています。ここに記載されているすべての情報は、変更される場合があります。

この記事では、次の内容について説明します。
  • マネージ コードで並列処理を表現する
  • ネイティブ コードで並列処理を表現する
  • 並列アプリケーションをデバッグする
  • 並列アプリケーションをプロファイリングする
 この記事では、次のテクノロジを使用しています。
Visual Studio、Multithreading
目次
マネージ コードで並列処理を表現する
ネイティブ コードで並列処理を表現する
並列アプリケーションをデバッグする
並列アプリケーションをプロファイリングする
今後の展望
ローカル コンピュータ ストアに移行することで多くのソフトウェアのパフォーマンス ボトルネックが解決されたのは、それほど昔のことではありません。18 ～ 24 か月ごとに新しいコンピュータを購入する手段と動機を持つ消費者と企業は、コンピュータを新たに購入するたびに、計算集約的なアプリケーションの実行速度が以前の 2 倍になっていることに気付きました。
悲しいことに、このような古きよき時代は過ぎ去ってしまったようです。CPU 速度が指数関数的に向上した日々は過去のことです。その主な理由は、私たちが速さに慣れるのに近いペース、また、最近のソフトウェアの進歩による要求に近いペースでハードウェア メーカーが経済的かつ環境にやさしくクロック速度を拡張するのを妨害する厄介な物理学の法則にあります。一方、ムーアの法則で示されるトランジスタの到来によって、チップ メーカーは市販ハードウェアで使用可能なコアの数を指数関数的に拡大できます。現在、米国でシングルコア コンピュータが売られているのを見かけることはめったになく、デュアルコアが標準になっています。来年に販売される一般的な消費者コンピュータはクアッドコアになり、その後間もなく 8 コアが標準となるでしょう。
残念ながら、今日のソフトウェアの大多数は、このようなプロセッサ数の指数関数的な増加を自動的には利用しません。これを利用するには、ソフトウェアがその構成要素の安全で効果的な分割をサポートするような方法で記述されている必要があります。これらの個々の部分を並列に計算し、すべてのコアに分散できるようにして、マルチコアおよびメニーコア システムで保証されている潜在能力を実現する必要があります。
今日の卓越したプログラミング言語、フレームワーク、および開発環境でこのような並列処理を表現することは簡単ではありません。最も平凡なシステムを除いて、マルチスレッド プログラミングは今日信じられないほど難しく、エラーを引き起こしやすくなっています。並列パターンの実装は普及しておらず、よく知られた簡単なものではありません。同時実行を利用するようにアプリケーションを構築できる場合でも、最もよく知られた競合状態やデッドロックから、より不明瞭なキャッシュ コヒレンシーのオーバーヘッド、ロック コンボイ、優先順位の逆転にいたるまで、機能の正確性とパフォーマンスの問題が頻繁に発生します。
さらに、開発者がこのようなパターンを克服できたとしても、それにかかる時間と労力はビジネスに負担をかけ、最良の (そして最も待遇のよい) 開発者は、パフォーマンス中心の業界で競争力を維持するためのコストにすぎない同時実行の問題よりも、純利益に影響するビジネスの問題に集中することを期待されています。これらの問題が著しく重なり合い、開発者が並列アプリケーションのデバッグと分析に使用できるツールはせいぜい基本的なものでしかありません。
その結果、ソフトウェア開発のビッグ プレーヤーは、すべての開発者が採用できるように並列処理をはるかに簡単にする道に踏み出し、マイクロソフトはそれを先導しています。次期バージョンの Visual Studio の計画には、並列処理の堅牢な基盤が含まれ、コア プラットフォームとツール サポートの両方に、ネイティブ コードとマネージ コードに対する大幅な改良が等しく含まれています。

マネージ コードで並列処理を表現する
次の問題提起について考えます。赤ちゃんの誕生情報の一覧があり、特定範囲の年に特定の州で生まれた特定の名前の赤ちゃんを検索し、その結果を返します。次に、C# での典型的なソリューションがどのようになるかを示します。
IEnumerable<BabyInfo> Search(
    IEnumerable<BabyInfo> babies, QueryInfo qi) {
  foreach (var baby in babies) {
    if (baby.Name == qi.Name && baby.State == qi.State &&
        baby.Year >= qi.YearStart && baby.Year <= qi.YearEnd) {
      yield return baby;
    }
  }
}
これは主に "厄介な並列" の問題です。理論上は使用可能なすべてのプロセッサに作業を分割することで処理の並列化を簡単に行うことができるようにして、個々の赤ちゃんを独立して分析できます。あいにく、この理論は常に実践できるわけではありません。
図 1 に、このソリューションの並列化の試行を示します。コード図の長さからわかるように、今日の Microsoft .NET Framework では、並列処理中心の問題の説明で示唆されるほどタスクは簡単ではありません。このコードをもってしても、ソリューションは理想からかけ離れています。手動パーティション化を採用して、複数のコアに処理を分散しています。スレッド間で調整する方法を理解するには、同期プリミティブの詳細な知識が必要です。列挙子での競合と、クリティカル領域の内部と外部で実行される作業の比率により、ロックのオーバーヘッドが実装の多くを占める可能性があります。他にも多くの問題があります。
図 1 ネイティブの手動並列処理
IEnumerable<BabyInfo> Search(
    IEnumerable<BabyInfo> babies, QueryInfo qi) {
  var results = new List<BabyInfo>();
  int partitionsCount = Environment.ProcessorCount;
  int remainingCount = partitionsCount;
  var enumerator = babies.GetEnumerator();
  try {
    using (ManualResetEvent done = new ManualResetEvent(false)) {
      for (int i = 0; i < partitionsCount; i++) {
        ThreadPool.QueueUserWorkItem(delegate {
          while(true) {
            BabyInfo baby;
            lock (enumerator) {
              if (!enumerator.MoveNext()) break;
              baby = enumerator.Current;
            }
            if (baby.Name == qi.Name &&
                baby.State == qi.State &&
                baby.Year >= qi.YearStart &&
                baby.Year <= qi.YearEnd) {
              lock(results) results.Add(baby);
            }
          }
          if (Interlocked.Decrement(ref remainingCount) == 0)
            done.Set();
        });
      }
      done.WaitOne();
    }
  }
  finally {
    IDisposable d = enumerator as IDisposable;
    if (d != null) d.Dispose();
  }
  return results;
}
このソリューションを見たときにさらにがっかりするのは、並列定型コードとメソッドのビジネス ロジックを実際に実装しているコードの比率です。開発者が差し迫った問題提起に集中できるようにすべての定型をシステムで処理できれば、はるかに有効になります。
IEnumerable<BabyInfo> Search(
    IEnumerable<BabyInfo> babies, QueryInfo qi) {
  var results = new ConcurrentStack<BabyInfo>();
  Parallel.ForEach(babies, baby => {
    if (baby.Name == qi.Name && baby.State == qi.State &&
        baby.Year >= qi.YearStart && baby.Year <= qi.YearEnd) {
      results.Push(baby);
    }
  });
  return results;
}
鋭い読者は、この問題提起が LINQ-to-Objects を使用した実装にもかなり役立つことに気付くでしょう。年で並べ替えられた順序で赤ちゃんの情報を返すように元の問題提起を補強したと想像してください。この追加要件によって、並べ替えを並列で行う場合には特に、前の手動実装にかなりのコードが追加されます。LINQ では、そのソリューションを図 2 の先頭に示すようにコーディングできます。ここで、図 2 の下部で示すように AsParallel を babies 列挙に付加するだけで、LINQ クエリを自動的に並列処理できると想像してください。
図 2 PLINQ による並列処理
逐次
IEnumerable<BabyInfo> Search(
    IEnumerable<BabyInfo> babies, QueryInfo qi) {
  return from baby in babies
         where baby.Name == qi.Name && baby.State == qi.State &&
               baby.Year >= qi.YearStart && baby.Year <= qi.YearEnd
         orderby baby.Year ascending
         select baby;
}
並列
IEnumerable<BabyInfo> Search(
    IEnumerable<BabyInfo> babies, QueryInfo qi) {
  return from baby in babies.AsParallel()
         where baby.Name == qi.Name && baby.State == qi.State &&
               baby.Year >= qi.YearStart && baby.Year <= qi.YearEnd
         orderby baby.Year ascending
         select baby;
}
さいわいなことに、これらの構成要素はまったく架空のものではありません。一部の読者は、Parallel.ForEach を TPL (Task Parallel Library) の一部として認識している可能性があります。TPL は、2007 年 12 月から .NET Framework に対する Parallel Extensions の CTP (Community Technology Preview) の一部として正式に使用可能になっています。その他の読者は、AsParallel を PLINQ (Parallel LINQ) の一部として認識している可能性があります。これも Parallel Extensions CTP を通じて使用可能です。
次期バージョンの Visual Studio では、これらはコア .NET Framework の一部になり、.NET を使用しているすべての開発者が .NET 準拠の言語から使用できるようになります。逐次 LINQ 実装に並んで、Parallel.ForEach は mscorlib.dll の一部として、AsParallel は System.Core.dll の一部として使用できるようになります。
System.Threading および System.Threading.Tasks 名前空間を通じて公開される TPL は、2007 年 10 月発行の MSDN Magazine (msdn.microsoft.com/magazine/cc163340 を参照) で世界中に紹介されてからかなりの変更が加えられましたが、中核となる原理は変わりません。for ループを並列化するための Parallel.For、前の例で示した foreach ループを並列化するための Parallel.ForEach、ステートメントのセットを並列化するための Parallel.Invoke など、今日の逐次アプリケーションで使用される上位レベルの構成要素に代わるものが提供されます。これらのメソッドは、並列で実行するコードを表現するデリゲートをすべて受け入れるため、匿名メソッドとラムダ式をサポートする言語では特にうまく機能します。
たとえば、図 3 のコードは、Luke Hoban (マイクロソフトの F# のプログラム マネージャ) が F# で作成したレイ トレーサからの抜粋です。このコードは、Parallel.For メソッドを利用してシーンのレンダリングを並列化しています。2 つの実装の主な違いは 1 行に収まることに注意してください。元のコードのその行を次に示します。
for y = 0 to screenHeight - 1 do
図 3 F# から Parallel.For を使用する
逐次
member this.Render(scene, rgb : int[]) =
  for y = 0 to  screenHeight - 1 do
    let stride = y * screenWidth
    for x = 0 to screenWidth - 1 do
      let color = TraceRay(
        {Start = scene.Camera.Pos;
         Dir = GetPoint x y scene.Camera },
        scene, 0)
      let intColor = color.ToInt ()
      rgb.[x + stride] <- intColor
並列
member this.Render(scene, rgb : int[]) =
  Parallel.For(0, screenHeight, fun y ->
    let stride = y * screenWidth
    for x = 0 to screenWidth - 1 do
      let color = TraceRay(
        {Start = scene.Camera.Pos;
         Dir = GetPoint x y scene.Camera },
        scene, 0)
      let intColor = color.ToInt ()
      rgb.[x + stride] <- intColor)
並列バージョンを次に示します。
Parallel.For(0, screenHeight, fun y ->
シーン レンダリングを構成する反復の分割および実行は、TPL によって自動的に処理されます。
別の例として、次の逐次コードはツリーの各ノードのデリゲートを実行します。
static void WalkTree<T>(Tree<T> tree, Action<T> handler) {
  if (tree == null) return;
  handler(tree.Data);
  WalkTree(tree.Left, handler);
  WalkTree(tree.Right, handler);
}
Parallel.Invoke を使用して、このツリー探索の実行を簡単に並列化できます。
static void WalkTree<T>(Tree<T> tree, Action<T> handler) {
  if (tree == null) return;
  Parallel.Invoke(
    () => handler(tree.Data),
    () => WalkTree(tree.Left, handler),
    () => WalkTree(tree.Right, handler));
}
TPL におけるこれらの上位レベルの構成要素は、タスクベースの並列処理の基盤を形成する下位レベルの構成要素セットの上に構築されています。これには、実行をスケジュールできる個々の作業項目を表す Task クラス、将来の時点での計算値を表す Future<T> クラス、タスクと計算を将来実行するために使用されるスケジューラを表す TaskManager クラス、およびタスクを効果的に構成および管理するためのいくつかの型サポートが含まれます。これらのソリューションにより、非常に複雑な並列システムを、現在必要とされるよりもはるかに少ない労力で構築できます。
mscorlib に組み込まれている新しい同時実行スケジューラのおかげで、これらはより強力なハードウェアでより効率的に実行することもできます。このスケジューラは、前述の TaskManager クラスを通じてパブリックに表されます。TaskManager クラスにより、分離されたタスクに実行方法の個別ポリシーを付与する個々のスケジューラを作成できます。共有リソース マネージャは、スケジューラ インスタンス間を調整して、過剰をできるだけ制限する方法でシステムの処理リソースが効果的に利用されることを保証します。スケジューラは、コンピュータのキャッシュをより適切に利用するための再帰アルゴリズムの有効化など、きめ細かな並列処理を使用しているときに通常行われるソリューションの最適化でデザインされています。
.NET Framework は、その誕生以来、マルチスレッド開発をサポートしてきたことに注意してください。System.Threading 名前空間は、モニタ、ミューテックス、セマフォ、イベント、スレッドローカル記憶域など多数の低レベル同期プリミティブのうち、その多くをバージョン 1.0 から提供しています。C# や Visual Basic などの言語は、それぞれ lock キーワードおよび SyncLock キーワードなどの構成要素を通じてサポートを公開します。ただし、TPL などのライブラリで並列システムを効果的に作成するには、多くの場合に上位レベルの調整と同期タイプが必要です。
その結果、次期バージョンの .NET Framework にも、マルチスレッド アプリケーションの作成を簡単にする追加の構成要素で肉付けされた System.Threading 名前空間が見られます。たとえば、遅延初期化は、最初に必要になったときにのみデータを読み込んで初期化するためにアプリケーションでよく使用される手法です。通常この処理は、処理時間がより目立たない時点に処理を遅延させるため (たとえば、起動時間を向上させるために、起動時に行われるはずの作業をオフロードするため)、または遅延初期化したデータがめったに使用されず、一部のアプリケーション実行ではまったく必要ない場合に、処理を完全に排除するために行われます。どちらの場合も、この手法をマルチスレッド アプリケーションで実装する場合は、複数のスレッドがデータの初期化でしばしば競合するため、手動でコーディングするとエラーが発生しやすく反復的になる可能性があります。
この問題を解決するために、.NET Framework には、スレッドセーフ遅延初期化の最も一般的なパターンを簡単にする LazyInit<T> 型が含まれます。LazyInit<T> の Value プロパティにアクセスすると、初期化がまだ行われていない場合は初期化が強制的に行われ、初期化されたデータが返されます。アプリケーションは、複数のスレッドによる Value の取得が競合する場合でも、その正確さを維持します。
次に、LazyInit<T> を複数の方法で利用する Visual Basic コードを示します。
Private _d1 As LazyInit(Of MyData)

Private _d2 As LazyInit(Of MyData) = New LazyInit(Of MyData)( _
  Function() MyData.Load())

Private Shared _d3 As LazyInit(Of MyData) = New LazyInit(Of MyData)( _
  Function() MyData.Load(), LazyInitMode.EnsureSingleExecution)

Private _d4 As LazyInit(Of MyData) = New LazyInit(Of MyData)( _
  Function() MyData.Load(), LazyInitMode.ThreadLocal)
_d1 変数は、遅延初期化するインスタンス メンバです。この例では、LazyInit<T> が Activator.CreateInstance を使用して MyData のインスタンスを作成できるようにする、パブリックの、パラメータなしコンストラクタが MyData 型にあることを想定します。
_d2 変数もインスタンス メンバです。ただし、この例では、変数をアクセス時に初期化するためのコードとして、開発者が提供するラムダ式を使用しています。
_d2 と同様に、_d3 変数もカスタム デリゲートで初期化されています。ただし、今回は変数が静的です (Visual Basic の Shared)。また開発者は、Value プロパティにアクセスする複数スレッドの競合がある場合でも、提供されるデリゲートが 1 回のみ実行されることを保証するために、既定の初期化モードをオーバーライドすることを選択しました (既定のモードでは、デリゲートを複数回実行できますが、1 つの結果のみが Value を通じてパブリックに公開されることが保証されます)。
最後に、_d4 変数では ThreadLocal モードを使用しています。ThreadStaticAttribute に精通している読者は、アクセスする各スレッドがデータのコピーを独自に取得することが保証される点で LazyInitMode.ThreadLocal が似ていることに注目してください。ただし、ThreadStatic は実行時の最適化により効率が上がる可能性が高くなりますが、LazyInit<T> は機能の点で ThreadStaticAttribute より優れています。たとえば、ThreadStaticAttribute は静的変数でのみ使用できますが、LazyInit<T> はスレッドローカルをインスタンス メンバとして持つことができます。
また、ThreadStaticAttribute は使用を試みる多くの開発者を悩ませる可能性があります。次のコードについて考えます。
[ThreadStatic]
private static MyData _d5 = new MyData();
アプリケーションの他のコードで _d5 変数が設定されておらず、_d5 にアクセスしたときにその値が null であった場合は驚くでしょうか。この状況は完全に可能です。ここでの問題は、C# コンパイラが _d5 の初期化を、含まれる型の静的コンストラクタに浮上させることです。型が必要になる直前にのみ静的コンストラクタが実行される場合 (遅延初期化の優れた例)、そのコンストラクタは 1 回のみ実行されます。このため、型に最初にアクセスするスレッドでは _d5 が初期化されていることが検出されますが、他のスレッドからは値が null に見えます。LazyInit<T> は ThreadLocal モードでこの状況に対処し、LazyInit<T> の Value プロパティが正しく初期化されていることを、このプロパティにアクセスするすべてのスレッドが検出できるようにします。
これは、次期バージョンの .NET Framework で使用可能になる新しいスレッド構成要素の一例にすぎません。他にも、CountdownEvent、Barrier、SpinWait などがあります。
また、新しいサポートには、共有データの操作を支援するスレッドセーフ コレクションの主なセットも含まれます。これらのコレクションは System.Collections.Concurrent 名前空間にあり (以前の CTP では System.Threading.Collections 名前空間にありました)、さまざまな状況に対処します。ConcurrentQueue<T> や (前の例で示した) ConcurrentStack<T> などの型は、汎用的な Queue<T> および Stack<T> のスレッドセーフでスケーラブルな代替型であり、BlockingCollection<T> などの型は、プロデューサ/コンシューマ シナリオのブロックを簡単に実装できるようにします。プロデューサ/コンシューマ シナリオでは、コンシューマ スレッドのホストによって消費されるデータ、および並列に実行できる一連のフィルタを通じてデータが流れるパイプライン パターンが複数のスレッドによって生成されます。
既に述べたように、PLINQ は次期バージョンの .NET Framework の中核部分です。大量のデータを処理する読み取り専用クエリの場合、PLINQ は単純で宣言的なパスを並列処理に提供します。前の赤ちゃんの名前の例で示したように AsParallel をデータ ソースに追加することで、開発者は .NET 標準クエリ演算子の PLINQ 実装に切り替えます (この切り替えは、開発者に並列処理サポートの選択を強制するために明示的に行われます。ほとんどの LINQ クエリが読み取り専用であっても、いくつかが変換を実行する場合に、このことが重要になります。これらの変換で PLINQ を使用する場合は、並列処理を安全に行うために変換をスレッドセーフにする必要があります)。
実際には、PLINQ は従来のデータ並列処理手法を使用して入力データを分割し、それを複数スレッドで処理し、データの生成方法と使用方法に適した方法でそれらのデータを再びマージします。PLINQ の詳細については、PLINQ を初めて詳細に説明した 2007 年 10 月発行の MSDN Magazine (msdn.microsoft.com/magazine/cc163329) を参照してください。

ネイティブ コードで並列処理を表現する
過去数年にわたって、数人の開発者から、ネイティブ C++ に対するマイクロソフトの傾倒について疑問を投げかけられました。その懸念は理解しますが、マイクロソフトの開発部門でのネイティブ コードに向けたあらゆる作業を見てきて、このような心配事は対処されていると確信を持って言うことができます。たとえば、Visual C++ チームは最近、Technical Report 1 (TR1) の完全サポート実装と、MFC に対する大幅な更新をリリースしました。どちらも、Visual C++ 2008 Feature Pack (go.microsoft.com/fwlink/?LinkId=124366 を参照) の一部として入手できます。同時実行に関して、ネイティブ C++ アプリケーションの並列処理のサポートは最も頻繁に要求される機能の 1 つであり、そのサポートの提供は、Visual C++ チームと並列コンピューティング プラットフォーム チーム両方の主要目標です。これは、次期バージョンの Visual Studio および Visual C++ で反映されます。
マネージ コードと同様に、きめ細かな並列処理と逐次アプリケーションから並列アプリケーションへの移行を可能にすることは、Visual C++ での同時実行サポートの中核です。興味をそそる単純な行列乗算の例について考えてみましょう。
void MatrixMult(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];
      }
    }
  }
}
このメソッドの並列化の試みを図 4 に示します。
図 4 MatrixMult を並列化する
void MatrixMult(int size, double** m1, double** m2, double** result) {
  int N = size;
  int P = 2 * NUMPROCS;
  int Chunk = N / P;
  HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
  long counter = P;
    for (int c = 0; c < P; c++) {
        std::thread t ([&,c] {
            for (int i = c * Chunk;
                 i < (c + 1 == P ? N : (c + 1) * Chunk);
                 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];
                }
              }
            }
            if (InterlockedDecrement(&counter) == 0) SetEvent(hEvent);
        });
    }
    WaitForSingleObject(hEvent,INFINITE);
    CloseHandle(hEvent);
}
このコードは、非同期パターンの開発を大幅に簡略化する C++0x std::thread 型やラムダ式のサポートなど、次期バージョンの Visual C++ に導入される新しい有用な機能を示しています。
ただし、これらの機能が追加されても、このソリューションはまだ不十分です。静的分割を使用して、すべての使用可能なプロセッサに作業が分割されますが、計算に不均等がある場合、または分割の処理速度に違いが生じる原因となるその他の何らかの問題がコンピュータで発生した場合には、多くのスレッドが、作業の担当部分の完了に必要な 1 つ以上のスレッドを待機する (また、支援を提供しない) ことになる可能性があります。
ソリューションにはイベントなどの同期プリミティブの知識が依然として必要ですが、ドキュメントを見ずに、これらの CreateEvent パラメータ NULL、TRUE、FALSE、および NULL が同期にどのように影響するかを言える読者は何人いるでしょうか。
ソリューションはエラーを起こしやすく、分割ステップで検出が難しい 1 つ違い (off-by-one) のエラーが発生する可能性が高くなります。明示的にインターロックされた操作は、未処理スレッドの数を追跡する必要があります。ソリューションでは、MatrixMult を呼び出して処理を行うスレッドを利用せずに、代わりにこれをブロックして貴重なシステム リソースを浪費する方を選びます。これはほんの一例に過ぎません。
より簡単なソリューションが必要であり、次期バージョンの Visual C++ ではこれが提供されます。
void MatrixMult(int size, double** m1, double** m2, double** result) {
  parallel_for(0, size, 1, [&](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 メソッドも使用しています。parallel_for メソッドは、Visual C++ ランタイム ライブラリで出荷される PPL (Parallel Pattern Library) の一部です。parallel_for は、parallel_for_each、parallel_accumulate、parallel_invoke などの他の関数と共に開発されているいくつかの上位レベル並列処理関数の 1 つです。これらのメソッドの多くは、標準テンプレート ライブラリ (STL) に相当する直接並列処理を提供し、最小限のコードの書き直しで逐次メソッドを並列メソッドで簡単に置換できるようにするため (当然、並列で実行される操作はスレッドセーフであると想定されます)、なじみやすいものです。
たとえば、ゲームの敵を表すオブジェクトの std::vector を反復し、能力と場所に基づいてそれぞれの敵のポジションを更新する for_each 呼び出しについて考えます。
for_each(enemies.begin(), enemies.end(), [&](AIEnemy& cur) {
  cur.updatePosition();
})
問題のベクタに重複が含まれない限り、また、updatePosition メンバ関数がローカル オブジェクトのみを更新する限り、メソッド呼び出しの前に parallel_ を付加するだけでこのメソッドを並列化できます。
parallel_for_each(enemies.begin(), enemies.end(), [&](AIEnemy& cur) {
  cur.updatePosition();
});
マネージ コードで System.Threading.Parallel クラスがタスクの並列性を処理するために下位レベルの型の上に構築される方法と同様に、ネイティブ ソリューションでも parallel_ メソッドが使用されます (ネイティブ ソリューションとマネージ ソリューションは似ているように感じられるため、開発者は一方の分野の知識をもう一方の分野に簡単に応用できます)。PPL の基盤として機能する 2 つの中核となる型は task_handle と task_group です。この両方を図 5 に示します。
図 5 task_handle と task_group
template <class _Func>
class task_handle {
public:
  task_handle(const _Func& f);
};

  class task_group {
public:
  task_group ();
  task_group(tr1::function<void(exception_ptr)> Fn);
  ~task_group ();
  template <class _Func> void run(const _Func& Fn);
  template <class _Func> void run(task_handle<_Func>& t);
  void cancel();
  task_group_status wait();
  void copy_exception(exception_ptr);
};
これらの構成要素がどのように使用されるかを示す例として、今月号の MSDN Magazine (msdn.microsoft.com/magazine/cc872852 で入手できます) の David Callahan の記事で示されているクイックソートの実装について考えます。David のソリューションは、典型的なクイックソート パターンに従っています。並べ替えるデータの長さが十分に小さい場合にはオーバーヘッドの少ない並べ替えを使用し、データを 2 等分し、クイックソートを再帰的に使用して各部分を並べ替えます。彼が示すソリューションでは parallel_invoke を使用していますが、parallel_invoke は task_group の上に構築され、これを直接使用する実装は次のようになります。
template<class T>
void QuickSort(T * data, int length, T* scratch) {
  if(length < THRESHOLD) InsertionSort(data, length);
  else {
    int mid = Partition(data[0], data, length, scratch, true);
    task_group g;
    g.run([&]{QuickSort(data, mid);});
    g.run([&]{QuickSort(data+mid, length-mid);});
    g.wait();
  }
}
実装を有効にするために必要な定型は非常にわずかでした。task_group を単純に作成し、各再帰呼び出しを別々のタスクとして実行し、両方のタスクの完了を待機します。待機操作は実際のカーネル待機ではない可能性があり、スレッドのリソースを浪費する代わりに、スケジュールされたタスクを現在のスレッドにインライン化することを選択しています。
その他にも中核となる型が提供されています。たとえば、前に示した task_group 型は、関連性のない複数のスレッドでタスクを同時に実行するようにスケジュールできるという点でスレッドセーフです。親子関係にあるタスクのみが同じグループに対して同時にスケジュールできる構造化並列処理の場合、structured_task_group 型は同じ機能に対して最適化されたサポートを提供します。
マネージ側と同様に、タスクは基礎となる効率的なスケジューラで実行されます。スケジューラでは、work-stealing アルゴリズムを使用して、きめ細かい操作の効率的な実行を可能にすること、リソースの効率的な再利用を可能にする協調ブロッキングをサポートすること、その操作に対してスケジューラが採用するポリシーを詳細に制御することなどを実現します。また、スケジューラには、きめ細かなタスク実行よりもプロセス間メッセージングに焦点を当てたプリミティブの組み込みサポートがあります。
次期バージョンの Visual C++ で提供される優れた同時実行機能の 1 つは、メッセージングと非同期エージェントのサポートです。これにより、開発者には、大まかなコンポーネントを定義してそれらを対話させることができる並列アプリケーションを作成するアプローチが提供されます。この対話はプロセス内メッセージング インフラストラクチャを通じて実行されるため、アプリケーションは遅延に耐えることができ、コンポーネントの作成をサポートし、スレッドの安全性のためのメカニズムとして分離を利用できます。
コア サポートは、いくつかのメッセージング関数 (send、receive、asend、および try_receive) と、送受信をサポートする ISource および ITarget インターフェイスを中心としています。ライブラリには、unbounded_buffer、overwrite_buffer、single_assignment、choice、call、transform、timer など、これらのインターフェイスを実装するいくつかのメッセージング ブロックが含まれます。
また、システムは、開発されるシステムに必要であるために実装されるカスタム メッセージング ブロックを促進するようにデザインされました。メッセージング ブロックでは、メッセージを格納する機能と、他のメッセージ ブロックにメッセージを転送する機能がサポートされます。ブロックは相互にリンクでき、メッセージが 1 つのブロックに到着した場合、そのブロックはセマンティクスに適した方法でそれらのメッセージを処理してから、ネットワーク内でリンクされているブロックに結果のメッセージを転送します。
たとえば、図 6 のコードは、2 つの unbounded_buffer を示しています。どちらも join ブロックにリンクされ、join ブロックは transform ブロックにリンクされ、transform ブロックは call ブロックにリンクされています。データが両方の unbounded_buffer に到着した場合、そのデータは join ブロックを通じて transform ブロックに渡され、transform ブロックは開発者が提供する関数を使用してデータを操作し、結果を call ブロックに送信できます。call ブロックは、別の開発者提供メソッドにそのデータを提供します (ここで示すメッセージング コードのサンプルでは、現在指定されている構文を使用していますが、この構文は初期プレビュー リリースで使用可能な構文とは若干異なる場合があります)。
図 6 受け渡しメッセージング ブロック
unbounded_buffer<int> unboundedBuf1;
unbounded_buffer<int> unboundedBuf2;
join<int,int> join1;

transform<tr1::tuple<int,int>,int>
  transform1([](tr1::tuple<int,int>& input){
  return tr1::get<0>(input) * tr1::get<1>(input);
};
call<int> call1([](int& input){
  cout << "result: " << input << endl;
};

unboundedBuf1.link_target(join1);
unboundedBuf2.link_target(join1);
join1.link_target(transform1);
transform1.link_target(call1);
また、エージェントベースのアプリケーションでは、新しい agent_task クラスを利用できます。このクラスは実際には、メッセージを送受信することで他のタスクと対話することを目的としたスケジューラからの専用スレッドです。agent_task は、プロデューサ/コンシューマ デザインでかなり役立つ場合があります。
たとえば、図 7 の上部のコードについて考えます。このコードは、ログ ファイルからテキスト行を連続的に読み取って処理する単純なログ ファイル パーサーです。その処理は、図の下部に示すように、1 つのスレッドをファイルの読み取り専用とし、別のスレッドをそのコンテンツの処理専用とすることで、ファイルの読み取りの I/O と重ねることができます。unbounded_buffer は、リーダーからパーサーに送信されるメッセージを格納するために作成され、パーサー エージェントは、バッファから継続的に受信して結果のメッセージを処理するために作成されます。一方、逐次の例のように、ParseFile メソッドはファイルを開いて行を読み取りますが、各行を直接解析する代わりに、その行を使用するパーサー エージェントの unbounded_buffer に各行を単純に送信します。ファイルを完全に読み取ると、終了メッセージをパーサー エージェントに送信し、エージェントの完了を待機します。
図 7 エージェントでファイルを解析する
逐次
HRESULT LogChunkFileParser::ParseFile() {
  HRESULT hr = S_OK;
  WCHAR wszLine[MAX_LINE] = {0};
  hr = reader->OpenFile(pFileName);
  if (SUCCEEDED(hr)){
    while(!reader->EndOfFile()){
      hr = reader->ReadLine(wszLine, MAX_LINE);
      if (SUCCEEDED(hr)){
        Parse(wszLine);
      }
    }
  }
  return S_OK;
}
並列
HRESULT LogChunkFileParser::ParseFile() {
  unbounded_buffer<AgentMessage> msgBuffer =
    new unbounded_buffer<AgentMessage>();
  agent_task* pParserAgent = agent_task::start([&] {
    AgentMessage msg;
    while((msg = receive(msgBuffer))->type != EXIT) {
      Parse(msg->pCurrentLine);
      delete msg->pCurrentLine;
    }
  });
  HRESULT hr = reader->OpenFile(pFileName);
  if (SUCCEEDED(hr)){
    while(!reader->EndOfFile()) {
      WCHAR* wszLine = new WCHAR[MAX_LINE];
      hr = reader->ReadLine(wszLine, MAX_LINE);
      if (SUCCEEDED(hr)) {
         send(msgBuffer, AgentMessage(wszLine));
      }
    }
    send(msgBuffer, AgentMessage(EXIT));
    hr = agent_task::wait_for_completion(pParserAgent);
  }
  return hr;
};
ご覧のとおり、ネイティブ コード開発者とマネージ コード開発者の両方にとって、開発プラットフォームに役立つ機能が多数あります。ただし、これらの新しい構成要素のすべてが万能薬ではないということを指摘しておきます。このサポートの大部分は、開発者がアプリケーションで並列処理をより簡単に表現できるようにすること、そのために以前に必要だった定型を取り除くこと、および逐次プログラミングの構成要素を並列プログラミングの構成要素で置換するのを支援することに焦点を当てています。parallel_for や AsParallel などの構成要素によって、自動的にコードが同時実行コンテキストで安全に実行されるようになるわけではありません。競合条件、デッドロックなどはまだ発生する可能性があり、並列アプリケーションの開発をはるかに簡単にすることで、このような問題がより一般的になる可能性があります。

並列アプリケーションをデバッグする
並列プログラミングの課題は、プログラミング モデルに限りません。ツール サポートは、並列プログラミングのプラットフォームを成功させる重要な要素です。その理由は、並列アプリケーションのデバッグとパフォーマンス チューニングで開発者が直面する複雑さが増しているためです。このため、Visual Studio では、レガシ プログラミング モデルと新しいプログラミング モデルの両方で、並列プログラミングのツール サポートの改善に投資しています。
Visual Studio 2008 のツールがまだマルチスレッドのサポートを提供していないということではありません。実際に、Visual Studio 2008 はマルチスレッド アプリケーションの開発に役立つ有用な機能をいくつか備えています。
たとえば、並列アプリケーションのデバッグ ツールについて興味深いジレンマは、複数の実行コンテキストに関連するときのブレークポイント セマンティクスの性質です。具体的な問題は、すべての実行コンテキストを停止するか、ブレークポイントが検出されたコンテキストのみ停止するかということです。既定の動作では、通常はブレークポイントですべてのスレッドが停止しますが、他のスレッドがブレークポイントにあるときでも、スレッドなどの他の 1 つ以上の実行コンテキストが処理を続行できるようにすることが望ましい場合があります。すべてをブロックするデバッガ実装では競合条件が明らかにならない状況で、このような機能が非常に役立つと想像できます。デバッガは、ユーザーの必要に応じてスレッドを停止および再開する機能を提供します。最初は、ブレークポイントにヒットするとすべてのスレッドが停止します。この時点で、ユーザーはデバッグ セッションに進む前にスレッドのサブセットを再開することを選択できます。ユーザーは、必要に応じてスレッドを停止することもできます。
ただし、並列アプリケーションを開発およびデバッグする一方で、生産性を向上させるために行う必要のあることが他にも数多くあります。TPL や PPL によって提供されるようなプログラミング モデルでは、パフォーマンスの優れた並列アプリケーションの開発に対する複雑さのしきい値が低くなります。これらのプログラミング モデルに対する共通のテーマは、分割作業の詳細を抽象化し、それらをスケジュールし、適切な順序制約を満たすために必要な同期を追加するランタイム システムの存在です。実際に、これらのモデルの主な目標は、開発者がスレッド レベルでの並列処理について考える必要をなくし、きめ細かいタスク レベル以上で並列処理について考えるようにすることです。
また、自動並列処理を実行するランタイムがあると、定型コードをすべて作成する必要はなくなるため開発者の負担は軽減されますが、スレッドなどの実行の抽象化にマップされる多くの仮想プロセッサ間に分割される何千ものタスクに作業を分割する方法が、ランタイムによって選択されることにもなります。開発者の観点からは、個々のタスクの実行をデバッグする必要がある場合に、コードの特定の部分に導かれる実行パスをトレースすることは、不可能とは言わないまでも困難な場合があります。これにより、プログラミング モデルとデバッガの間に二極分化が作成されます。現在、これはスレッドに焦点を当てています。
次期バージョンの Visual Studio の 2 つの新機能である MultiStack ビューとタスク一覧ビューは、並列プログラミング ランタイムを利用するアプリケーションのデバッグの負担を軽くすることで、この困難に対処することを目的としています。
図 8 に示すように、MultiStack ビューは、子タスクのスタックフレームが親にリンクされる論理呼び出しスタックを、すべてのタスク (または選択したタスク セット) についてグラフ化します。ビューは共通スタック プレフィックスを単一のノードにまとめ、プレフィックスを共有するタスクの数でノードにタグを付けます。このため、グラフはタスクのセットの実行状態のグローバル ビューを表示します。ユーザーは、ブレークポイントで実行中タスクのコール スタックをたどり、エディタでメソッドの特定のインスタンスを表示するように切り替えることができます。
図 8 MultiStack (クリックすると画像が拡大表示されます)
図 9 のタスク一覧は、ブレークポイントでランタイム システムが認識しているすべてのタスク、それぞれの実行状態、および現在実行されているメソッドを、引数およびその値の一覧と共に示しています。一覧は任意の列で並べ替え、グループ化、または検索でき、開発者はタスク間をすばやく切り替えたり、他のさまざまなデバッガ機能を使用して実行状態をさらに調べたりできます。
図 9 タスク一覧 (クリックすると画像が拡大表示されます)
特に共有メモリ プラットフォームにおける並列プログラミングでもう 1 つの課題となる領域は、通常の式評価機能を効率的に提供しながら、複数の実行コンテキスト内のメモリ コンテンツを同時に検証する機能です。次期バージョンの Visual Studio には、このニーズに対処する MultiWatch ビューが含まれます。この機能により、開発者は変数名と式をウォッチ ウィンドウに追加できます。ウォッチ ウィンドウでは、各デバッガ ステップまたはブレークポイントで、またシステムのすべてのスレッドまたはタスクについて式が評価されます。これにより、問題の領域と発見しにくいバグの原因を見つけるために、スレッド間でのデータの不整合をはるかに簡単に特定できるようになります。

並列アプリケーションをプロファイリングする
並列処理の本質は (応答性かスループットかにかかわらず) パフォーマンスであるため、パフォーマンス分析ツールの優れたセットがプラットフォームの成功に不可欠です。コンピュータ処理中心の操作の並列処理を活用することは難しい場合がありますが、複雑なクライアント アプリケーションの並列処理タスクよりも確実に難易度は低くなります。複雑さの追加部分は、リッチ クライアント アプリケーションにはディスクやネットワーク I/O などのさまざまなボトルネックのフェーズが含まれるという事実によるものです。
開発者の観点からは、このようなアプリケーションの並列化の鍵は、その動作を十分に理解して並列処理の機会を識別することです。これを行った後、開発者はジョブを実行するために使用可能なプログラミング パターンと抽象化の多くを適用できます。このフェーズで覚えておく必要のある重要なポイントは、同時実行を、コンピュータ処理の高速化だけでなく遅延の隠蔽の手法としても利用できることです。アプリケーションが作成された後は、パフォーマンスに優れたソリューションを実現するために理解し、測定し、チューニングする必要のある、非効率性に関する追加のソース (同期など) があります。
Visual Studio 2008 のパフォーマンス分析ツールでは、マネージ アプリケーションとネイティブ アプリケーションの開発者のチューニング作業を支援するために非常に多くの機能が提供されます。既存のプロファイラは、サンプリング ベースおよびインストルメンテーション ベースのプロファイリングをサポートします。サンプル プロファイリングは、ユーザー定義の間隔でアプリケーション プログラムをインターセプトし、スレッドごとにコール スタックをキャプチャし、実行を継続します。分析時には、すべてのコール スタックが収集されて関連付けられ、大部分のサンプルが取得された関数がアプリケーション パスと共に特定されます。
この手法の 1 つの欠点は、同期呼び出しや I/O 呼び出しによってアプリケーションのスレッドがブロックされている時間が考慮されないことです。このため、サンプル プロファイリングは CPU バウンドのアプリケーションにのみお勧めします。
予定どおりの間隔でサンプリングする以外に、Visual Studio プロファイラでは、ページ フォールトやマイクロプロセッサ パフォーマンス モニタ カウンタ (たとえば、L2 キャッシュ ミス) などの他の重要なイベント時のサンプリングを実行することもできます。これらの機能は、キャッシュやメモリ ページの局所性など、アプリケーションの動作の重要な側面を理解するために非常に貴重なツールになる場合があります (このトピックの詳細な説明については、msdn.microsoft.com/magazine/cc872851 にある MSDN Magazine 今月号の「.NET の問題」のコラムを参照してください)。
インストルメンテーションベースのプロファイリングは、名前が示すように、ターゲット アプリケーションまたはモジュール内の各関数のコードを補強することで動作します。このようなインストルメンテーションは、他のメソッドへの呼び出しサイトを含め、通常は関数の入り口と出口のポイントに挿入されます。このインストルメンテーション レベルにより、プロファイラはアプリケーションの実行ですべての遅延を考慮できますが、実行時間とトレース ロギングの非常に高いオーバーヘッドという犠牲を払うことになります。これらのオーバーヘッドは、実行を大幅に混乱させ、データの分析に必要な時間が増えることがあります。このようなオーバーヘッドを削減するために、ユーザーは Visual Studio プロファイラを使用して、関数またはモジュール (DLL) の細分化レベルでアプリケーションを部分的にインストルメント化できます。また、プロファイラは、実行時にプログラマがパフォーマンス データ コレクションを調整できるようにする API を提供します。
並列プログラマの観点から見ると、現在のプロファイラには、新しい並列プログラミング ランタイムの上位レベル サポートなどの望ましいいくつかの機能と、同期などの一般的な並列パフォーマンスの問題に対処する特定のシナリオが不足しています。次期バージョンの Visual Studio では、プロファイラはマルチスレッド アプリケーション開発者のニーズに対処するツールで補強されています。
並列開発に不可欠なステップは、同時実行の程度と、アプリケーションで並列処理を使用してパフォーマンスが向上する可能性を理解することです。これは、並列処理の実装前とチューニング フェーズの両方で重要になります。
並列処理のターゲットであるアプリケーションを検証するときに、並列処理の候補となるアプリケーションのフェーズを判断する必要があります。通常、これらのフェーズは CPU バウンドですが、現在のところ、このような実行のフェーズを簡単に判断し、アプリケーション コードにマップするツールはありません。
図 10 に示すように、次期リリースの Visual Studio には同時実行分析のサポートが含まれます。x 軸が時間を示し、y 軸がシステムのコアの数を示すグラフには、対象のアプリケーションの時間によってコアの利用率がどのように変化するかが示されます (緑色で示されています)。ユーザーはこのビューを使用して、CPU バウンドであるアプリケーションのフェーズ、その相対的な期間、およびシステム コア利用率のレベルを判断し、そのフェーズにズームインして、これらのフェーズ中にアプリケーションで実行されていた内容を判断し、可能性のある並列処理の形式とそれらの利用方法の検討を開始できます。パフォーマンス チューニング フェーズ中に、このビューでは、期待される同時実行が実際に実行時に実現されているかどうかを確認できます。
図 10 同時実行分析 (クリックすると拡大画像が表示されます)
また、基本的なタイミング測定を行って、前の実行または逐次実行と比較した速度の向上を推定できます。最後に、同時実行ビューでは、問題のアプリケーションとシステムの他のプロセス、またはカーネルとの間の相互作用についても確認できます。ビューでは、アイドル時間は黒で表示され、他の非カーネル プロセスは黄色で表示されます。カーネル アクティビティ (この例では最小) は赤で表示されます。
開発者は、同時実行ビューを使用して対象のフェーズを識別した後、図 11 に示す [Thread Blocking] (スレッド ブロック) ビューを使用してアプリケーションの動作をさらに分析できます。このビューは、アプリケーション内の各スレッドの動作に関する豊富な情報を提供します。
図 11 スレッドの実行とブロックの分析 (クリックすると拡大画像が表示されます)
最初に、各スレッドの実行を分類した棒グラフが生成され、各スレッドの有効期間のうち、コードの実行またはブロックに費やされた部分が示されます。ブロック遅延は、I/O や同期などの各種カテゴリにさらに分類されます。
2 番目に、x 軸に時間を表示し、y 軸にスレッドおよび物理ディスク I/O をレーンとして表示するタイムライン ビューが提供されます。ディスク I/O の場合、画面には、ディスク上でその時点に行われた読み取りと書き込み、およびアクセスされたファイルがツールヒントを使用して表示されます。スレッドの場合は、スレッドの実行時刻、ブロックされた時刻、および対象の遅延カテゴリが色を使用して示されます。
3 番目に、コール スタック分析により、開発者はタイムライン ビューのブロック セグメントにマウスのカーソルを合わせて、各スレッドがブロックされたときにそのスレッドで何が行われていたかを詳細に理解できます。図 11 のスクリーンショットは、EnterCriticalSection の呼び出しにより約 83 ms の長さになっている同期ブロック セグメントの上にマウス カーソルが置かれた場合の例を示しています。ブロックがクリティカル セクションの取得を試行する原因となった行番号を含むコール スタックも表示されます。このような詳細情報は、開発者が非効率性の根本的な原因を調べ、それに対処するために不可欠です。その他の機能としては、詳細な実行、ブロック、およびディスク I/O 統計と、下側のタブで使用可能なレポートがあります。
並列パフォーマンス分析の別の重要な側面は、キャッシュ ワーキング セットとの対話によるスレッドの移行です。図 12 に、コア実行およびスレッド移行のビューを示します。このビューでは、各スレッドに一意の色が関連付けられ、システムのプロセッサ コアでスレッドの実行がどのようにスケジュールされているかが表示されます。スレッド移行イベントは、コア間での色の移り変わりによってグラフィカルに示されます。関心のある各スレッド移行イベントについて、アプリケーション フェーズでそのイベントを特定し、関連するスレッド上にマウス カーソルを合わせることでスレッド ID をメモし、スレッド ブロック ビューに切り替えることができます。スレッド ブロック ビューは、スレッド移行の前のブロック イベントの性質を示し、アプリケーションでイベントが発生したタイミングを示すことができます。開発者は、ブロック イベントのそのクラスを削減するか、アプリケーション実行環境で許容されると思われる場合はアフィニティを実施することで、この種類の問題を最適化するかどうかを決定できます。
図 12 コア実行とスレッド移行 (クリックすると拡大画像が表示されます)

今後の展望
この記事の執筆時点では、前述の同時実行サポートをすべて含む次期バージョンの Visual Studio のリリース日はまだ発表されていません。しかし、間もなくマネージ開発者とネイティブ開発者は同じように、マルチコア システムが提供する必要のあるすべてのコンピュータ処理能力を効率的に利用するアプリケーションを効果的に開発できるようになります。このようなシステムは企業と家庭ですぐに標準になるため、今こそこのようなサポートについて学習を始める時期です。
この記事を支援してくれた Rick Molloy、David Callahan、Paul Maybee の各氏に感謝の意を表します。

Stephen Toub は、マイクロソフトの並列コンピューティング プラットフォーム チームのシニア プログラム マネージャ リードであり、同時実行のプログラミング モデルに関する作業に集中しています。また、MSDN Magazine の寄稿編集者でもあります。

Hazim Shafi は、マイクロソフトの並列コンピューティング プラットフォーム チームでパフォーマンス ツールを担当している主席アーキテクトです。マイクロソフトで並列プログラミングに関するコースの講師も務めています。


同時実行の障害
マルチスレッド コードでよく見られる 11 の問題を解決する
Joe Duffy

この記事では、次の内容について説明します。
  • 同時実行の基本的な概念
  • 同時実行の問題と対策
  • 安全を実現するためのパターン
  • 横断的な概念
 この記事では、次のテクノロジを使用しています。
マルチスレッド、.NET Framework
目次
データ競合
忘れられた同期
誤った粒度
読み取りと書き込みの分裂
ロックなしの並べ替え
再入
デッドロック
ロック コンボイ
スタンピード
2 ステップ ダンス
優先順位の逆転
安全を実現するためのパターン
不変性
純粋性
分離
同時実行はあらゆるところで行われます。サーバー側プログラムは長い間、基本的に同時実行のプログラミング モデルを扱ってきましたし、マルチコア プロセッサがより一般的になるにつれ、クライアント側プログラムもそのようなモデルを扱うことが必要になってきます。同時実行の導入に合わせて、安全性を確保する責任も求められるようになります。言い換えれば、大量の論理的な同時実行や、さまざまなレベルの物理的なハードウェア並列処理を扱う一方で、プログラムは一定レベルの堅牢性と信頼性を実現する必要があります。
正しく設計された同時実行コードは、逐次コードと比べて、特別ないくつかのルールを遵守する必要があります。メモリからの読み取りや書き込み、および共有リソースへのアクセスは、競合が発生しないように、同期を使用して制御される必要があります。また、多くの場合、特定のジョブを完了させるためには、スレッド間の協調が必要になってきます。
これらの追加要件の直接的な結果として、スレッドの処理の進行が絶えず一貫性を持ち適切であることが重要となります。同期と協調はタイミングに大きく依存しますが、タイミングは非決定的であり、予測やテストが困難です。
これらの特性が難しいものに思えるのは、単に考え方の変化を求められるからです。学習できる API は 1 つもなく、コピーして貼り付けられるようなコード スニペットもありません。ただし、学ぶだけでなく、慣れ親しんでおく必要のある一連の基本的な概念は存在します。特定の言語やライブラリによって、時間の経過と共にいくつかの概念を隠蔽できる可能性はありますが、同時実行に現在取り組んでいるのであれば、そのようなぜいたくは言っていられません。この記事では、知っておく必要のあるいくつかの一般的な課題について説明し、ソフトウェアでそれらの課題に対処するための助言を提供します。
最初に、同時実行プログラムでしばしば失敗を招くいくつかの種類の問題について見ていきます。著者はこれらを "安全性障害" と呼んでいます。なぜなら、大変につまづきやすい問題であり、一般にかなり望ましくない結果になるからです。これらの障害が起こると、クラッシュまたはメモリの破損によってプログラムの実行が中断されます。

データ競合は、競合状態とも呼ばれ、データが複数のスレッドから同時にアクセスされたときに発生します。具体的には、1 つ以上のスレッドがデータを書き込んでいるときに、別の 1 つ以上のスレッドがそのデータを読み取っていると、競合が発生します。この問題が発生するのは、Windows プログラム (C++ でも Microsoft .NET Framework でも同様) が基本的に、共有メモリの概念に基づいており、プロセス内のすべてのスレッドが、同じ仮想アドレス空間に存在するデータにアクセスする可能性があるためです。共有には、静的変数やヒープ割り当てを使用できます。
たとえば、次のような標準的な例を考えます。
static class Counter {
    internal static int s_curr = 0;
    internal static int GetNext() {
        return s_curr++;
    }
}
Counter の目的は、GetNext の呼び出しごとに新しい固有の数値を渡すことであると思われます。ただし、プログラム内の 2 つのスレッドが両方同時に GetNext を呼び出した場合、2 つのスレッドに同じ数値が与えられる可能性があります。その理由は、s_curr++ が 3 つの個別の手順にコンパイルされるためです。
  1. 共有変数 s_curr の現在の値をプロセッサ レジスタに読み取る。
  2. そのレジスタをインクリメントする。
  3. レジスタの値で共有変数 s_curr を書き換える。
この同じシーケンスを実行する 2 つのスレッドが、両方とも s_curr から同じ値 (たとえば、42) をローカルで読み取り、それをインクリメント (たとえば、43) して、同じ結果値を発行する可能性があります。その場合、GetNext は、両方のスレッドに対して同じ数値を返し、アルゴリズムは失敗します。s_curr++ という単純なステートメントはアトミックに見えますが、それはまったくの誤りです。

忘れられた同期
これは最も単純な種類のデータ競合であり、同期を取ることがまったく忘れられています。まれな状況では、この種類の競合が問題ない、つまり正しいということもありますが、ほとんどの場合は基本的な正確性に問題が生じます。
このような問題は、常に顕著ではありません。たとえば、あるオブジェクトが何らかの複雑で大きなオブジェクト グラフの一部であり、このグラフがたまたま静的変数でアクセスできたり、または新しいスレッドの作成時やスレッド プールへの作業のキューイング時にオブジェクトをクロージャの一部として渡すことで、オブジェクト グラフが共有されていたりする場合があります。
オブジェクト (グラフ) がプライベートから共有へいつ遷移するのかに注意することが不可欠です。これはパブリック化と呼ばれ、後で分離に関するセクションで説明します。この逆はプライベート化と呼ばれます。これは、オブジェクト (グラフ) が共有から再度プライベートに戻ることを意味します。
解決策は、適切な同期を追加することです。カウンタの例では、単純なインターロックを使用できます。
static class Counter {
    internal static volatile int s_curr = 0;
    internal static int GetNext() {
        return Interlocked.Increment(ref s_curr);
    }
}
これが機能するのは、更新がメモリ内の 1 つの場所に制限されていて、また、アトミックにしようとしているソフトウェア ステートメントと等価なハードウェア命令 (LOCK INC) が (便利なことに) 存在するためです。
または、本格的なロックを使用することもできます。
static class Counter {
    internal static int s_curr = 0;
    private static object s_currLock = new object();
    internal static int GetNext() {
        lock (s_currLock) {
            return s_curr++;
        }
    }
}
このロック ステートメントは、GetNext にアクセスしようとしているすべてのスレッド間で相互排除を実現するものであり、CLR System.Threading.Monitor クラスを使用しています。C++ プログラムでは、同じ目的に CRITICAL_SECTION を使用します。この特定の例に対してロックを使用する必要性はありませんが、複数の操作がかかわる場合には、それらを 1 つのインターロック操作に組み込むことはほとんど不可能です。

誤った粒度
共有状態へのアクセスが適切な同期によって行われても、結果の動作が正しくない場合があります。アトミックに見える必要のある操作が領域内にカプセル化されるように、粒度は十分に大きい必要があります。領域が小さい方が、他のスレッドが同時に到着したときに待つ時間が短くなるため、正確さと領域を小さくすることはトレードオフの関係にあります。
たとえば、図 1 に示す銀行口座の抽象化を見てください。すべてが良好であり、オブジェクトの 2 つのメソッド Deposit および Withdraw は安全に同時実行できるように見えます。いくつかの銀行取引アプリケーションでは、同時実行アクセスによって残高が間違う心配をせずに、それらのメソッドを使用できます。
図 1 銀行口座
class BankAccount {
    private decimal m_balance = 0.0M;
    private object m_balanceLock = new object();
    internal void Deposit(decimal delta) {
        lock (m_balanceLock) { m_balance += delta; }
    }
    internal void Withdraw(decimal delta) {
        lock (m_balanceLock) {
            if (m_balance < delta)
                throw new Exception("Insufficient funds");
            m_balance -= delta;
        }
    }
}
しかし、Transfer メソッドを追加する場合はどうでしょうか。単純な (そして誤った) 方法では、Deposit と Withdraw は分離した状態では安全なので、それらを簡単に結合できると仮定してしまいます。
class BankAccount {
    internal static void Transfer(
      BankAccount a, BankAccount b, decimal delta) {
        Withdraw(a, delta);
        Deposit(b, delta);
    }
    // As before
}
これは誤りです。実際には、Withdraw 呼び出しと Deposit 呼び出しの間には間隔が空いているため、その間は引き出した金額が完全に失われています。
正しい実装では、a と b の両方に対してあらかじめロックを取得しておいてから、メソッドを呼び出す必要があります。
class BankAccount {
    internal static void Transfer(
      BankAccount a, BankAccount b, decimal delta) {
        lock (a.m_balanceLock) {
            lock (b.m_balanceLock) {
                Withdraw(a, delta);
                Deposit(b, delta);
            }
        }
    }
    // As before
}
このアプローチは、粒度の問題を解決する一方で、デッドロックに陥りやすいことがわかっています。後で、その修正方法を説明します。

読み取りと書き込みの分裂
前述したように、問題のない競合の場合は、同期なしで変数にアクセスできます。32 ビット プロセッサでの 32 ビット (4 バイト) や 64 ビット プロセッサでの 64 ビット (8 バイト) など、境界に揃えられた自然なワード サイズの読み取りや書き込みは、アトミックです。スレッドが単に 1 つの変数を読み取っているときに、その変数に他のスレッドが書き込む場合、複雑な不変性が関係することがなければ、この保証によって同期を完全にスキップできる場合もあります。
しかし注意してください。これを境界に揃っていないメモリ位置や、自然なサイズでないメモリ位置で行おうとすると、読み取りまたは書き込みの分裂が発生する可能性があります。分裂は、そのような位置への読み取りや書き込みに、実際には複数の物理メモリ操作が含まれるために起こります。その合間に同時更新が行われると、結果の値が、更新前後の値が混ざり合ったものになってしまう可能性があります。
例として、ループ内に ThreadA があり、0x0L および 0xaaaabbbbccccddddL のみを 64 ビット変数 s_x に書き込むとします。ThreadB は、それを読み取るループ内にあります (図 2 を参照)。
図 2 分裂が発生する状況
internal static volatile long s_x;
void ThreadA() {
    int i = 0;
    while (true) {
        s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL;
        i++;
    }
}
void ThreadB() {
    while (true) {
        long x = s_x;
        Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL);
    }
}
ThreadB のアサートが発生する可能性があることに気付いて驚かれたかもしれません。その理由は、ThreadA の書き込みが上位 32 ビットと下位 32 ビットの 2 つの部分から成り、その順序がコンパイラによって異なるためです。ThreadB の読み取りにも同じことが言えます。そのため、ThreadB は 0xaaaabbbb00000000L または 0x00000000aaaabbbbL の値を見ることになります。

ロックなしの並べ替え
ときには、より高いスケーラビリティと信頼性を実現する手段として、ロックなしのコードを書くことも魅力的です。それには、対象とするプラットフォームのメモリ モデルを深く理解することが必要です。詳細については、Vance Morrison の記事「メモリ モデル : マルチスレッド アプリケーションにおける低ロック テクニックの影響について」(msdn.microsoft.com/magazine/cc163715) を参照してください。これらのルールを理解せずに注意を払わないと、メモリの並べ替えのバグが発生する可能性があります。そのようなバグは、プロセス中または最適化の実行中に、コンパイラとプロセッサがメモリ操作を自由に並べ替えられるために発生します。
例として、次に示すように、s_x と s_y の両方が値 0 に初期化されると仮定します。
internal static volatile int s_x = 0;
internal static volatile int s_xa = 0;
internal static volatile int s_y = 0;
internal static volatile int s_ya = 0;

void ThreadA() {
    s_x = 1;
    s_ya = s_y;
}

void ThreadB() {
    s_y = 1;
    s_xa = s_x;
}
ThreadA と ThreadB の両方の実行が完了した後、s_ya と s_xa の両方に値 0 が格納されている可能性はあるでしょうか。これは一見ばかげたことに思えます。s_x = 1 と s_y = 1 のいずれかが最初に実行され、そのとき残りのスレッドはその更新を確認し、それぞれの変数を更新します。少なくとも理論的にはそうです。
残念ながら、プロセッサは、読み込みが実質的に書き込みよりも前に実行されるように、このコードの順序を自由に並べ替えることができます。これは、明示的なメモリ バリアによって回避できます。
void ThreadA() {
    s_x = 1;
    Thread.MemoryBarrier();
    s_ya = s_y;
}
.NET Framework にはこの特定の API が用意されており、C++ には _MemoryBarrier などのマクロがあります。しかし、ここで言いたいのは、そこらじゅうにメモリ バリアを挿入しなさいということではありません。メモリ モデルについて習熟するまでは、ロックなしのコードの使用は避け、もしも使用する場合には十分に注意する必要があります。

Windows では、Win32 および .NET Framework を含めて、ほとんどのロックが再帰的な取得をサポートしています。これは単に、現在のスレッドが既にロックを保持しているときに再度それを取得しようと試みた場合、その要求は満たされるということです。それにより、小さなアトミック操作から大きなアトミック操作を合成することが簡単になります。実際、前に示した BankAccount の例でも、再帰的な取得を利用していました。Transfer は Withdraw と Deposit の両方を呼び出し、Withdraw と Deposit のそれぞれが、Transfer が既に取得しているロックを冗長的に取得します。
ただしこれは、再帰的な取得が予想外に発生するような状況になると、問題が起きる原因となります。そのような状況の 1 つが再入であり、再入は動的なコードへの明示的なコールアウト (仮想メソッドやデリゲートなど) によって、または暗黙的に再入されるコード (STA メッセージ ポンプや非同期プロシージャ コールなど) によって発生します。これを含めたいくつかの理由により、ロック領域内から動的メソッドを呼び出すのは避けることをお勧めします。
たとえば、不変性を一時的に破ってデリゲートを呼び出す次のようなメソッドを考えます。
class C {
    private int m_x = 0;
    private object m_xLock = new object();
    private Action m_action = ...;

    internal void M() {
        lock (m_xLock) {
            m_x++;
            try { m_action(); }
            finally {
                Debug.Assert(m_x == 1);
                m_x--;
            }
        }
    }
}
C のメソッド M は、m_x が変化することを防いでいます。しかし、m_x が再度デクリメントされる前の短い時間の間に 1 インクリメントされています。m_action へのコールアウトは、特に問題ないように見えます。しかし残念ながら、このコールアウトが C クラスのユーザーから受け付けられたデリゲートである場合、これは何でも好きなことを実行する任意のコードを表します。それには、同じインスタンスの M メソッドへのコールバックも含まれます。その場合、finally 内のアサートが真になる可能性が高くなります。直接指定していなくても同じスタック上で M への複数の呼び出しがアクティブになり、その結果明らかに、m_x が 1 より大きな値を保持する可能性がでてきます。

複数のスレッドがデッドロックに陥ると、システムは単純に応答を停止します。著者の記事「ハングを解消する : .NET アプリケーションのデッドロックを回避および検出するための高度な手法」(msdn.microsoft.com/magazine/cc163618) および 2007 年 10 月号の Stephen Toub による「.NET の問題」コラム (msdn.microsoft.com/magazine/cc163352) を含め、MSDN Magazine のいくつかの記事で、デッドロックの発生するしくみやそれを許容するためのいくつかの方法が解説されていますので、ここでは簡単に触れる程度にとどめます。まとめると、ThreadA が ThreadB の保持するリソースを待ち、ThreadB も ThreadA の保持するリソースを再帰的に (場合によっては、第 3 のスレッド ThreadC またはさらに他のスレッドを待つことで間接的に) 待っているなど、循環待機チェーンが形成されたときには、すべての処理の進行が停止してしまう可能性があります。
この問題の一般的な原因の 1 つは、互いに排他的なロックによるものです。実際に、前に示した BankAccount の例でもこの問題が発生します。ThreadA が口座 #1234 から口座 #5678 へ $500 を送金し、同時に ThreadB が #5678 から #1234 へ $500 を送金しようとすると、コードはデッドロックに陥る可能性があります。
図 3 に示すように一貫した取得順を使用すると、デッドロックを回避できます。このロジックは、同期ロック取得と呼ばれる概念に一般化できます。同期ロック取得では、複数のロック可能なオブジェクトが何らかの順序に従ってロック間で動的に並べ替えられ、2 つのロックを同時に保持する必要のある場合には一貫した順序で取得されるようになります。ロックのレベル付けと呼ばれる別の方法を使用すると、一貫していない順序で行われる可能性の高いロック取得は拒否できます。
図 3 一貫した取得順
class BankAccount {
    private int m_id; // Unique bank account ID.
    internal static void Transfer(
      BankAccount a, BankAccount b, decimal delta) {
        if (a.m_id < b.m_id) {
            Monitor.Enter(a.m_balanceLock); // A first
            Monitor.Enter(b.m_balanceLock); // ...and then B
        } else {
            Monitor.Enter(b.m_balanceLock); // B first
            Monitor.Enter(a.m_balanceLock); // ...and then A
        }
        try {
            Withdraw(a, delta);
            Deposit(b, delta);
        } finally {
            Monitor.Exit(a.m_balanceLock);
            Monitor.Exit(b.m_balanceLock);
        }
    }
    // As before ...
}
ただし、デッドロックの原因はロックだけではありません。別の状況としてはウェイクアップの見逃しがあり、何らかのイベントが見逃され、スレッドが永遠にスリープ状態のままになります。これは、Win32 の自動リセット イベントや手動リセット イベントのような同期イベント、CONDITION_VARIABLE、および CLR Monitor.Wait、Pulse、PulseAll の各呼び出しなどで一般に発生する可能性があります。ウェイクアップの見逃しは通常、同期が不適切であることや待ち条件の再テストが行われていないことを示していたり、または、ウェイクオール プリミティブ (WakeAllConditionVariable または Monitor.PulseAll) の方が適切なときにウェイクシングル プリミティブ (WakeConditionVariable または Monitor.Pulse) を使用したことなどを示しています。
この問題のもう 1 つの一般的な原因は、自動リセットおよび手動リセット イベントでのシグナルの消失です。そのようなイベントは、シグナル状態か非シグナル状態のいずれか一方でしか発生しないため、イベントを設定する冗長な呼び出しがあっても事実上無視されます。コードで、2 つの呼び出しの設定で常に 2 つのスレッドがアクティブになると仮定されている場合、結果としてウェイクアップの見逃しが生じる可能性があります。

ロック コンボイ
ロックへの到着率が、ロックの取得率と比較して常に高い場合は、ロック コンボイが発生する可能性があります。極端な場合には、処理可能な数よりも多くのスレッドがロックで待つことになり、壊滅的な事態に至ります。これは、サーバー側のプログラムでより一般的に見られ、ほとんどのクライアントで必要とされるデータ構造を保護する特定のロックが、異常なほど頻繁に利用される場合に発生します。
たとえば、次のシナリオを考えてみましょう。平均して、100 ミリ秒ごとに 8 つの要求が到着するとします。この要求の処理に 8 つのスレッドを使用します (8 CPU のコンピュータであるため)。この 8 つの各スレッドが何か意味のある作業を行う前に、ロックを取得して 20 ミリ秒間保持する必要があります。
しかし、このロックへのアクセスはシリアル化される必要があるため、8 つすべてのスレッドがロックに入って出るまでに 160 ミリ秒かかります。最初のスレッドがロックを出てから、9 つ目のスレッドがロックにアクセスできるまで、140 ミリ秒の時間があります。この方法は本質的にスケーリングできず、要求のバックアップは継続的に増大します。時間が経過して、到着率が低下しない場合には、クライアント要求がタイムアウトし始め、障害につながります。
ロックの公平性がコンボイの原因となることが知られています。その理由は、ロックを取得できた可能性のある期間が人為的にブロックされ、その結果、選択されたロック所有スレッドがアクティブ化され、コンテキスト切り替えを行い、ロックを取得して解放できるまでの間、到着したスレッドは待つ必要があるからです。Windows では、この問題に対処するため、すべての内部ロックが時間の経過と共に不公平になるよう変更され、CLR モニタも不公平となっています。
基本的なコンボイ問題の唯一の真の解決策は、ロックの保持時間を短くし、頻繁なロックがあったとしても非常に少なくなるようにシステムを分解することです。これは言うほど簡単ではありませんが、スケーラビリティに対してはきわめて重要です。

スタンピードとは、多くのスレッドがアクティブになり、それらのすべてが同時に Windows スレッド スケジューラから注目されるよう争っている状態です。たとえば、100 個のスレッドが 1 つの手動リセット イベント上でブロックされているときに、そのイベントを設定すると、特にそれらのスレッドの大部分が再度待つ必要がある場合には、大変厄介な自体になる可能性があります。
ブロッキング キューを実装する 1 つの方法は、キューが空のときに非シグナル状態になり、空でないときにシグナル状態となる、手動リセット イベントを使用することです。しかし残念ながら、0 要素から 1 要素への遷移中に多数の待ちスレッドがある場合は、スタンピードが発生する可能性があります。これは、ただ 1 つのスレッドだけがその 1 つの要素を取得し、それによってキューが再び空に遷移し、必然的にイベントがリセットされるためです。100 個のスレッドが待っている場合、そのうちの 99 個は、アクティブになってコンテキストを切り替え (そして多数のキャッシュ ミスを招き) ますが、結果として再度待たされることになります。

2 ステップ ダンス
ロックを保持しながら、イベントを通知する必要がある場合もあります。これは、イベントによってアクティブになるスレッドが、その保持されたロックを取得する必要がある場合には、不運な結果となります。アクティブになっても、再度待つ羽目になるだけです。これは無駄であり、全体のコンテキスト切り替え回数を増加させます。この状況は 2 ステップ ダンスと呼ばれ、多くのロックとイベントが関係する場合には、2 ステップよりもずっと多くなる可能性があります。
Win32 および CLR の条件変数サポートは、本質的にこの 2 ステップ ダンス問題に苦しんでいます。これは多くの場合不可避であり、その回避は、ほとんど禁止されていると言ってよいくらい困難です。
2 ステップ ダンス問題は、シングルプロセッサのコンピュータではさらに深刻です。イベントが関連する場合、カーネルはアクティブ化されるスレッドに対して優先順位を引き上げます。これにより、ロックを解放する機会の前に、イベントを設定しているスレッドを横取りできることがほぼ保証されます。これは 2 ステップ ダンスの極端な場合であり、設定を行っている ThreadA がコンテキスト切り替えで除外されるため、アクティブ化される ThreadB がロックの取得を試みることができます。これはもちろん取得できないため、コンテキスト切り替えで除外され、再度 ThreadA が実行可能になります。最終的に ThreadA はロックを解放し、それによって再度 ThreadB の優先順位が引き上げられ、実行できるように ThreadA を横取りします。これでおわかりのとおり、多くの無駄なコンテキスト切り替えが行われています。

優先順位の逆転
スレッドの優先順位の変更は、通常はトラブルの元です。優先順位の異なる多くのスレッドが同じロックおよびリソースへのアクセスを共有していると、優先順位の逆転が起こり、優先順位の低いスレッドが優先順位の高いスレッドの進行を実際に無限にブロックする可能性があります。したがって、スレッドの優先順位の変更はできるだけ避けることが重要です。
優先順位の逆転の極端な例を次に示します。優先順位の低い ThreadA がロック L を取得するとします。その後、高い優先順位を持つ ThreadB が到着します。ThreadB は L を取得しようとしますが、ThreadA が保持しているために取得できません。これが "逆転" の部分です。これは、単に ThreadB で必要とするロックを ThreadA が保持しているというだけの理由で、ThreadA が人為的および一時的に ThreadB よりも高い優先順位を与えられたようなものです。
この状況は、いずれ ThreadA がロックを解放したときに自動的に解消されます。しかし、ここで中間の優先順位を持つ ThreadC が到着するとどうなるかを考えてください。ThreadC はロック L を必要としませんが、それが単に存在することで ThreadA がまったく実行できなくなり、間接的に高優先順位の ThreadB も実行できません。
最終的には、Windows Balance Set Manager スレッドがこの状況を検知します。ThreadC が永久に実行可能のままであっても、ThreadA はやがて (4 秒後に) OS によって一時的に優先順位を引き上げられます。この優先順位は、実行を完了してロックを解放するために十分であることが期待されます。しかし、ここでの遅延 (4 秒間) はかなり長いため、ユーザー インターフェイスが関係している場合には、アプリケーションのユーザーが確実に問題に気付くと思われます。

安全を実現するためのパターン
次から次へと問題を追いかけてきたところで、よいお知らせです。これまで述べた問題の多く (特に正確性の障害) の発生をかなり抑えることができるデザイン パターンが存在します。ほとんどの問題の核心は、複数のスレッド間で状態が共有されるということにあります。さらに悪いことに、この状態は自由に操作され、一貫した状態から一貫しない状態へと遷移し、また (うまくいけば) 一貫した状態に戻るといった遷移が、驚くほど規則正しく発生します。
開発者がシングルスレッド プログラムのコードを書く場合は、これはすべて理に適っています。有効な最終目的に達するまでの過程で、共有メモリを一種のスクラッチ パッドとして使用するのは簡単です。C スタイルの命令型プログラミング言語では、かなり長い年月にわたってそのように作業してきました。
しかし、同時実行の使用が増えるに従って、そのような習慣に対して注意する必要が生じています。また、Haskell、LISP、Scheme、ML、および F# (新しい .NET 準拠言語) などの関数型プログラミング言語から手掛かりを得て、不変性、純粋性、および分離を第一級のデザイン概念として取り入れることもできます。

不変性
不変なデータ構造とは、構築後も変化しないデータ構造のことです。これは同時実行プログラムにとってすばらしい特性です。なぜなら、データが変化しなければ、多くのスレッドが同時にアクセスしても競合のリスクがないからです。これは、安心して同期を実行できることを意味します。
不変性は、C++ では const、C# では読み取り専用修飾子によってある程度サポートされています。たとえば、読み取り専用フィールドだけを持つ .NET の型は、浅い不変性を持ちます。F# では、可変修飾子を使用しない限り、既定で浅い不変性を持つ型が作成されます。もう一歩進んで、これらの各フィールド自体が、すべて読み取り専用のフィールドを持つ別の型を参照している (そして深い不変性を持つ型のみを参照する) 場合は、その型は深い不変性を持ちます。それにより、オブジェクト グラフ全体が変化しないことが保証され、これは実際に大変有用です。
ここまではすべて、不変性を静的な特性として述べてきました。オブジェクトは、"原則的に" 不変であるということも可能です。これは、特定の期間内は状態が変化しないという保証があることを意味します。これは動的な特性です。Windows Presentation Foundation (WPF) の Freezable 機能はまさにこれを実現しており、同期なしでの同時実行アクセスも可能にしています (ただし、静的なサポートの場合と同じ方法ではチェックできません)。多くの場合、動的な不変性は、有効期間中を通じて不変と可変の間で遷移するオブジェクトに対して有用です。
不変性には、いくつかの欠点もあります。何かを変更する必要があるときには、元のオブジェクトのコピーを作成して、その変更を適用する必要があります。また、オブジェクト グラフ内のサイクルは一般に不可能です (動的な不変性を使用した場合を除きます)。
たとえば、図 4 に示すような ImmutableStack<T> を使用するとします。可変である Push および Pop メソッドのセットの代わりに、適用された変更を含む新しい ImmutableStack<T> オブジェクトをそれらから返す必要があります。いくつかのケースでは、(スタックの場合と同様な) 賢明な技法を使って、インスタンス間でメモリを共有できます。
図 4 ImmutableStack<T> の使用
public class ImmutableStack<T> {
    private readonly T m_value;
    private readonly ImmutableStack<T> m_next;
    private readonly bool m_empty;
    public ImmutableStack() { m_empty = true; }
    internal ImmutableStack(T value, Node next) {
        m_value = value;
        m_next = next;
        m_empty = false;
    }
    public ImmutableStack<T> Push(T value) {
        return new ImmutableStack(value, this);
    }
    public ImmutableStack<T> Pop(out T value) {
        if (m_empty) throw new Exception("Empty.");
        return m_next;
    }
}
ノードがプッシュされると、各ノードに対して新しいオブジェクトを割り当てる必要があります。スタックの標準的なリンク リスト実装では、いずれにしてもそれが必要となります。ただし、スタックから要素をポップするときには、既存のオブジェクトを使用できます。これは、スタック内の各ノードが不変であるためです。
既に、広く自然に使用されている不変型があります。CLR の System.String クラスは不変であり、新しい値の型はすべて不変にするというデザイン ガイドラインもあります。ここではガイダンスとして、それが可能であり自然に感じられる場合には不変性を使用し、単に現在の世代の言語で便利だからという理由で変化を実行するのは避けることをお勧めします。

純粋性
不変のデータ型を使用する場合でも、プログラムで実行されるほとんどの操作はメソッド呼び出しです。そしてメソッド呼び出しは副作用を持つ場合があります。副作用は何らかの形の変化を暗示するため、同時実行コードでは問題となります。ほとんどの場合、これは共有メモリへの単純な書き込みですが、データベース トランザクション、Web サービスの呼び出し、ファイル システム操作など、物理的な変化を伴う操作の場合もあります。多くの状況では、同時実行障害につながるという不安なしにいくつかのメソッドを呼び出せることが望まれます。その良い例は、System.Object の GetHashCode や ToString のような単純なメソッドです。ほとんどの人々は、それらが副作用を伴うとは考えていません。
純粋なメソッドは、同期の追加なしでも、常に同時実行設定で実行できます。純粋性に対する一般的な言語のサポートはありませんが、純粋なメソッドはごく単純に定義できます。
  1. 共有メモリのみから読み取り、不変または一定の状態のみを読み取る。
  2. もちろん、ローカル変数に書き込むことができる。
  3. 他の純粋なメソッドのみを呼び出すことができる。
したがって、純粋なメソッド内で可能なことは極端に限られています。しかし、不変な型と組み合わせることで、純粋性を実現でき、便利に利用できます。いくつかの関数型言語は既定で純粋性を仮定しています。最も顕著なのが Haskell であり、すべてのものが純粋です。副作用を起こすものはすべて、モナドと呼ばれる特別な要素にラップする必要があります。ただし、私たちのほとんどは Haskell を使用しないので、"原則的な純粋性" で間に合わせることにします。

分離
パブリック化とプライベート化については前に述べましたが、これらは非常に重要な問題の核心を突いています。一般的に状態は複数のスレッド間で共有されるため、同期は必須であり、不変性と純粋性は考慮に値します。しかし、状態が 1 つのスレッド内に閉じ込められていれば、同期の必要はありません。これにより、より自然にスケーラブルなソフトウェアを実現できます。
実際、状態が分離されている場合、変化は自由に発生できます。変化はほとんどの C スタイル言語に組み込まれた基本的な特性であるため、これは便利です。プログラマも変化に慣れています。ほぼ完全な関数型のスタイルでプログラミングするには修練が必要であり、ほとんどの開発者にはかなり難しいものです。したがって、試してみることはお勧めしますが、世界が一夜のうちに関数型に染まるといった考えは持たないでください。
所有状態を特定するのは難しいものです。オブジェクトはいつ共有されるのでしょうか。オブジェクトの初期化時には、それは 1 つのスレッドによって実行され、オブジェクト自体はまだ他のスレッドからはアクセスできません。そのオブジェクトへの参照がいったん静的変数や、スレッド作成時またはキューイング時に共有されているどこかの場所、またはそのような場所から過渡的にアクセス可能なオブジェクトのフィールドに格納されると、オブジェクトは共有されます。開発者はこのようなプライベートと共有の間の遷移を具体的に監視し、すべての共有状態を注意深く扱うことが不可欠です。

Joe Duffy は、マイクロソフトの Parallel Extensions to .NET の開発担当者です。ほとんどの時間をコードのハッキング、ライブラリのデザインの監督、およびすばらしい開発者チームの管理に費やしています。彼の新しい著書は、『Concurrent Programming on Windows』です。


ASP.NET AJAX 4.0
データ駆動型 Web アプリケーションに対する新しい AJAX サポート
Bertrand Le Roy

コードのダウンロード : MSDN Code Gallery (188 KB)
オンラインでのコードの参照

この記事は、ASP.NET のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。

この記事では、次の内容について説明します。
  • サーバー側のデータ操作
  • UpdatePanel とクライアント
  • ポストバックとペイロードの低減
  • クライアント側のテンプレート レンダリング
 この記事では、次のテクノロジを使用しています。
ASP.NET AJAX 4.0
目次
ポストバック ベースのマスタ/詳細ページ
UpdatePanel バージョン
純粋な AJAX バージョン
AJAX バージョンのテンプレート
イベント バブル
[戻る] ボタンの管理
完成した製品
AJAX は、さまざまな点で魅力的な Web プラットフォームです。AJAX により、従来はサーバーで実行されていた多くのタスクがブラウザで実行されるようになることで、サーバーへのラウンド トリップ回数が少なくなり、消費帯域幅が減少し、Web UI の速度や応答性が向上します。これらの利点は作業負荷の多くをクライアントに移した結果として得られるものですが、それでもやはり、サーバー アプリケーションの力と柔軟性を自在に活用することを求める多くの開発者は、ブラウザを実行環境としては選択しません。
これまでは、ソリューションとして UpdatePanel コントロールが使用されてきました。それにより開発者は、すべてのサーバー ツールを維持しながら AJAX アプリケーションを開発できました。しかし UpdatePanel は、従来のポストバック モデルから多くの重みを引きずっており、UpdatePanel 要求は依然として完全なポストバックです。実際、UpdatePanel を使用すると、(ViewState を含む) フォーム全体がサーバーにポストされ、ページのライフサイクルのほぼ全体がそこで実行され、レンダリングはサーバーで行われます。このような方法では明らかに、AJAX に移行する主な理由の 1 つが相殺されてしまいます。ここで唯一の真のメリットは、通常の HTTP POST 要求の代わりに XmlHttpRequest が使用され、ページの更新部分と ViewState だけがクライアントに返されることです。したがって、応答のサイズはずっと小さくなりますが、要求はそうではありません。
純粋な AJAX アプローチは、ほとんどの状況で UpdatePanel アプローチよりも優れています。純粋な AJAX のソリューションでは、レンダリングがクライアント上で行われ、サーバーはデータだけを返します。これは同等な HTML よりも通常はかなり小さなデータとなります。また、このアプローチでは、ネットワーク要求の数を大幅に減らすことができます。クライアント上にデータを保持することで、アプリケーションの UI ロジックの多くをブラウザで実行できます。
ただし、純粋な AJAX アプローチの主な問題は、データを HTML に変換するツールがブラウザに存在しないことです。そのためにすぐに利用できるのは、2 つの大まかな機能である、要素のコンテンツ全体を指定した HTML 文字列で置き換える innerHTML と、それよりやや低速な機能として、タグおよび属性に対して動作するドキュメント オブジェクト モデル (DOM) API (抽象レベルに関して HtmlTextWriter と同様なもの) だけです。
この記事では、同じ 1 つのページを、古典的なポストバック、UpdatePanel、および純粋な AJAX の 3 つの方法でそれぞれ記述したものを示し、サーバーで使用される手法が状況によってはクライアントで実行した方が優れていることを説明します。最初の 2 つの例は現在、一般に提供されている ASP.NET 3.5 SP1 を使用して構築できますが、3 つ目の例は ASP.NET 4.0 の新しいクライアント機能のいくつかを使用します。

ポストバック ベースのマスタ/詳細ページ
ここで構築するページは、製品のリストを表示し、ユーザーが製品を選択するとその製品の詳細説明をリストの右側のパネルに表示します。ここで使用する AdventureWorks サンプル データベースは、go.microsoft.com/fwlink/?LinkId=124953 からダウンロードできます。データ層はこの記事の中心的な話題ではないので、LINQ to SQL を使用して基本的なデータ層だけを作成します。
最初に、アプリケーションの App_Data フォルダに AdventureWorks.mdf ファイルを追加します。次に、単純に新しい "LINQ to SQL classes" .dbml ファイルを追加し、サーバー エクスプローラから Product、ProductPhoto、ProductProductPhoto の各テーブルと vProductModelCatalogDescription ビューをデザイン画面にドロップします。結果のデータ層を図 1 に示します。
図 1 データ層のアーキテクチャ (クリックすると拡大画像が表示されます)
ページは 2 つのビュー ウィンドウで構成され、1 つは製品のリスト、もう 1 つは製品の詳細を示します。図 2 に、レンダリングされたページを示します。ページに対する最初の要求では、次のコードを使用して製品リストがデータにバインドされます。
private void BindProductList() {
    ProductList.DataSource = from p in AdventureWorksContext.Products
                             where p.ProductSubcategoryID == 1
                             //Mountain bikes
                             orderby p.Name
                             select p;
    ProductList.DataBind();
}
図 2 自転車のリストと詳細 (クリックすると拡大画像が表示されます)
このリスト自体とそのテンプレートを図 3 に示します。データベースに対してクエリを実行するコードは、ダウンロード可能なプロジェクト内に含まれており、その内容はかなり単純です。データベースに対して Mountain Bike カテゴリの製品を取得するクエリを実行し、その結果に対して ListView コントロールをバインドします。もちろん、データ ソース コントロールを使って同じ処理を行うこともできますが、コードによるアプローチの方が柔軟で予測可能です。デザイン ビューを重視する開発者であれば、違った結果になるかもしれません。
データセットから HTML マークアップを作成する実際の作業はすべて ListView コントロールで処理されるため、非常に便利で手間がかかりません。必要なのは、その HTML のテンプレートを LayoutTemplate および ItemTemplate プロパティに指定することだけです (図 3 を参照)。このテンプレートは、製品のリストを記号付きリスト (UL および LI タグ) 内のリンクとしてレンダリングします。
図 3 順不同の製品リスト
<asp:ListView ID="ProductList" runat="server"
    DataKeyNames="ProductId"
    OnSelectedIndexChanging="ProductList_SelectedIndexChanging"
    OnSelectedIndexChanged="ProductList_SelectedIndexChanged">
    <LayoutTemplate>
        <ul ID="itemPlaceholderContainer" runat="server">
            <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
        </ul>
    </LayoutTemplate>
    <ItemTemplate>
        <li><asp:LinkButton runat="server" ID="Select" CommandName="Select"
                                         Text='<%# Eval("Name") %>' /></li>
    </ItemTemplate>
</asp:ListView>
これらのリンク自体は通常のリンクでなく LinkButton コントロールであることに注意してください。これは、別のページに移動する代わりにページにポストバックすることを意味します。これらは実質的に、ボタンの機能を持つリンクです。もちろん、これらの LinkButton コントロールを適切なスタイルの通常のボタン コントロールで置き換えれば、JavaScript なしでページのアクセシビリティを簡単に改善できます。
ここで使用している LinkButton の主な特徴は、各ボタンにイベント ハンドラをアタッチする代わりに、CommandName プロパティを "Select" に設定していることです。その結果、ボタンがクリックされると、それを理解するコントロールによって処理されるまで、コマンドがコントロール ツリーを上にたどっていきます。これは非常に強力な機能であり、任意の UI 要素がその親コントロールにコマンドを送信する際に、親コントロールから期待されるコマンドと引数以外、多くの情報を知っている必要はありません。その結果、ListView などの強力なデータ コントロールを使用して、開発者がマークアップを完全に制御できます。後でこのページの純粋な AJAX バージョンを構築するときに、これが同様なブラウザ概念にどのように変換されるのかを示します。
この段階では、何もコードを記述する必要なしに、選択をサポートする製品リストが得られています。DataSource コントロールと ControlParameters を使用して、リスト内の選択されたデータ キーを詳細ビュー内の選択されたデータ キーに結び付ければ、引き続きコードなしで進むこともできますが、ここではそれをコードで行うことにします。次は、リストの SelectedIndexChanged イベントを処理し、関連する製品 ID を使用して BindProductDetails メソッドを呼び出します。
protected void ProductList_SelectedIndexChanged(object sender,
                                                 EventArgs e) {
    var productId = (int)ProductList.SelectedDataKey.Value;
    BindProductDetails(productId);
}
BindProductDetails は、データベースに対して製品の情報および写真のクエリを実行した後、それらを詳細ビュー内の対応するコントロールにバインドします。
写真は、データベースからクエリでイメージ バイトを取得してそれを応答のバイナリ ストリームにコピーする、単純なハンドラによって処理されます (図 4 を参照)。このハンドラは、ページの 3 つのバージョンのそれぞれで使用されます。これで、命令型と宣言型の混在したサーバー コードで全体が記述された、製品のデータ駆動型マスタ/明細ビューができ上がりましたが、まだいくつか改良できる点があります。
図 4 製品の写真の取得
public void ProcessRequest (HttpContext context) {
    int id;
    if (int.TryParse(context.Request.QueryString["id"], out id)) {
        context.Response.ContentType = "image/gif";
        AdventureWorksDataContext dc = new AdventureWorksDataContext();
        var bytes = dc.ProductPhotos
            .Where(p => p.ProductPhotoID == id)
            .Single().LargePhoto;
        context.Response.OutputStream.Write(bytes.ToArray(), 0, bytes.Length);
    }
    else {
        throw new HttpException(404, "Image not found");
    }
}
これは、状態の大きさに関してステートフルであるという点で、非常に典型的な Web フォーム ページです。ブラウザにレンダリングされたソースをざっとチェックすると、異なる各ビューがその内部状態をすべて覚えており、ポストバックごとにそれを保持していることから、ViewState が約 4 KB であることがわかります。
同時に、ページはその現在の状態に関して手掛かりを提供しません。ページ上で何を行っても、ブラウザのナビゲーション バーに表示される URL は "1_WebForm.aspx" のままです。ユーザーがページをブックマークすると、常に、製品詳細のないページが最初に表示されます。
この単純な例では、製品リスト内のリンクを LinkButton コントロールから通常のリンクに変更し、選択ポストバックを詳細ページへの単純なナビゲーションで置き換えることにより、上記の問題は修正できます (ViewState をオフにして、ラウンドトリップごとに 2 回、約 4 KB ずつ節約することも可能です)。最近の ASP.NET Model View Controller (MVC) ライブラリ (msdn.microsoft.com/magazine/cc337884 を参照) についてご存知の方は、これが MVC アプローチを活用できる典型的なケースの 1 つであることにお気付きではないでしょうか。
また、ナビゲーション ベースのアプローチを用いても、サイトの検索機能を大きく向上させることができます (これだけで、まったく別の記事のテーマとなる価値があります)。しかし、一般的なデータ駆動型アプリケーションは、この単純な例よりもずっと複雑であり、単純なナビゲーションでは UI フローの構築に適切な方法とは言えません。したがって、この単純なアプリケーションについても、ポストバックと Web フォームの概念がどのように AJAX に変換され、それによってどのように改善されるのかを詳しく示していきます。
ポストバックもリンク ナビゲーションも、思った以上にユーザー エクスペリエンスに悪影響を与える可能性があります。ポストバックおよびナビゲーション中には、UI が無効になり、サーバーが新しいコンテンツで応答するまでの間、他のユーザー操作は不可能になります。新しいコンテンツはドキュメント全体を置き換えてレンダリングされる必要があり、スクロール位置などの細かい状態が失われる場合があります。
もう 1 つの問題は、ユーザーが [戻る] ボタンや履歴の動作について非常に具体的な期待を抱いている点です。ポストバック モデルでは、残念ながら、何が履歴に記録されるか、またはユーザーが [戻る]、[進む]、[更新] を押したときに何が起こるかについては、ほとんどまたはまったく制御できません。理想的には、状態の変化に何が含まれ、何が履歴に残るのかは開発者が管理したいところですが、ポストバック アプリケーションでは、ユーザーとのほとんどすべてのやり取りでブラウザの履歴にエントリが作成されます。

UpdatePanel バージョン
このページを改良する簡単な方法の 1 つは、UpdatePanel を使用することです。UpdatePanel を使用すると、ユーザーの動作によりポストバックが生じるような場面で変更される部分を、ページ内で区切ることができます。今見ている非常に単純な例では、ページ内で更新する領域は詳細ビューです。UpdatePanel の部分更新を有効にするには、ページのフォーム タグのすぐ後に次に示すような ScriptManager コントロールを追加する必要があります。
<asp:ScriptManager ID="ScriptManager1" runat="server"/>
また、詳細ビューの周囲に、UpdatePanel 自体を追加する必要があります。
<asp:UpdatePanel ID="UpdatePanel1" runat="server" RenderMode="Inline">
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="ProductList"
            EventName="SelectedIndexChanged" />
    </Triggers>
    <ContentTemplate>
        <div class="float" id="productDetails">
            <fieldset>
            ...
            </fieldset>
        </div>
    </ContentTemplate>
</asp:UpdatePanel>
この UpdatePanel には、製品リストの SelectedIndexChanged イベントを監視するトリガがあることに注意してください。これは、部分更新をトリガする可能性のあるコントロールがすべて UpdatePanel の内部にある場合には必要ありませんが、ここでは、製品リストは常に UpdatePanel の外部に保持する必要があります。SelectedIndexChanged イベントの発生時にリストのレンダリングを更新する必要はないからです。このトリガがないと、部分更新の代わりに通常のポストバックが発生します。また、UpdatePanel を使用するときには、部分更新をトリガする可能性のあるすべてのポストバック コントロールに常に ID を付けることを忘れないでください。そうしないと、ページが特にはっきりした理由もなく通常のポストバックを使用する場合があります。
AJAX の外観を持つ古典的なポストバック Web フォームの変換についての説明は、これで終わりです。しかし、実際にはまだ必要な機能がすべて揃ったようには見えません。1 つの問題は、[戻る] ボタンに対する小さなサポートを失ったことです。ユーザーが何台かの自転車をブラウズした後で [戻る] ボタンを押すと、このサイトに来る前に見ていたサイトに戻ってしまいます。それに気付いたユーザーが [進む] ボタンを押すと、今度はアプリケーションの既定の状態 (ここでは、何も選択されていない状態の製品リスト) に戻ります。
さいわい、ASP.NET 3.5 SP1 では、ページに [戻る] ボタンのサポートを再度付け加えるための簡単な方法が用意されています。ScriptManager には非常に便利な EnableHistory プロパティ、AddHistoryPoint メソッド、および Navigate イベントがあり、それらを組み合わせることでアプリケーション開発者は、通常のポストバックで可能な範囲よりもずっと細かくブラウザの履歴を制御できます。この機能は、UpdatePanel の使用によって失われたものを取り戻すだけでなく、それをはるかに強力な形で再現します。
通常のポストバックとの大きな違いは、アプリケーションの状態の変化を構成する内容を正確に把握し、重要性が低いと判断したユーザーとのやり取りをフィルタリングできることです。また、"ブックマーク機能" も得られ、ブラウザの履歴ドロップダウンにわかりやすく意味のあるエントリを表示できるようになります。
ページに履歴管理を追加するには、ブックマークを使用する際にユーザーがどのような情報の保持を期待しているかを判断する必要があります。ここで、状態に含める必要のある関連情報は、1 つしかありません。現在選択されている製品の ID です。
その状態を変更しようとするイベントがある場合は、それを遮断する必要があります。実際、処理する必要のある唯一のイベントは、前にトリガとして使用したものと同じです。それは、製品リストの SelectedItemChanged イベントです。このイベントは既に詳細ビューの再バインドのために処理しているため、このイベントが発生するたびに新しい履歴ポイントを作成するようなコードを追加します。
protected void ProductList_SelectedIndexChanged(object sender,
                                                       EventArgs e) {
    var productId = (int)ProductList.SelectedDataKey.Value;
    var product = BindProductDetails(productId);
    if (ScriptManager1.IsInAsyncPostBack
                                   && !ScriptManager1.IsNavigating) {
         ScriptManager1.AddHistoryPoint("product",
           productId.ToString(), "AdventureWorks - " + product.Name);
    }
}
このイベントが、ユーザーが [戻る] ボタンで前のアプリケーション状態に戻った結果として発生したのではないことを確認するために、コードでは、要求が非同期ポストバックの一部であり、ナビゲーション操作の一部ではないことを確認します。このチェックを行わない場合、新しい履歴ポイントが作成され、ブラウザに存在する先の履歴を上書きしてしまいます。
このチェックが完了すると、AddHistoryPoint メソッドを安全に呼び出せるようになります。このメソッドには、状態の中で注目する唯一の情報である製品 ID を、"product" というパラメータ名で渡します。この名前は、後で示すとおり、変更された URL 内で使用されます。この値自体は、文字列に変換される必要があります。履歴状態は、クエリ文字列の 1 つの形式であると考えてください。メソッドに与える最後の情報は、ドキュメント タイトルです。履歴のナビゲーション ドロップダウンには、アプリケーションのナビゲーションに役立つ意味のある情報が表示されるため、これはユーザー エクスペリエンスを向上させる良い機会となります (図 5 を参照)。
図 5 履歴ドロップダウン (クリックすると拡大画像が表示されます)
この状態は、ブラウザ URL のハッシュによって保持されます (もともとはドキュメント内ナビゲーションのためにデザインされた、# 記号に続く部分)。これを記憶媒体として使用する理由は、ページおよびその JavaScript 状態と DOM 状態から実際に離れることなく履歴エントリを追加できる唯一の手段だからです (ブラウザでは URL が変更された場合以外は別の履歴項目を追加できません)。これには URL に状態を格納するという制約があり、使用できるスペースが限られます。一部のブラウザでは、1 KB を超える URL は拒否される場合があります。それより大きな URL が必要になる場合は、状態として関連する情報を適切に選択していない可能性があるので、情報をリファクタリングする必要があります。
ここで、製品の完全な名前ではなく、比較的短いデータである製品の ID を使用している点に注意してください。製品名はわかりやすいですが、通常は ID よりも長くなります。また、実際に URL のスペースが不十分である場合、そのデザインに対しては AJAX と履歴よりも通常の Web フォームと ViewState の方が適切である可能性があります。
問題の後半は、履歴をナビゲートしたときに、保存した状態を復元する必要があることです。これは、ScriptManager で Navigate イベントを処理することにより行います (図 6 を参照)。コードでは最初に、状態のない場合を処理します。これは、GET 要求にまでさかのぼったときに、ページの既定の状態に戻るようにするためです。状態が実際には新しいポストバックの実行によって復元されることを知らないと、これは多少普通ではないように見えるかもしれません。この場合、ポストバックの "前の" 状態、つまりフレームワークによって自動的に復元される状態は、ブラウザの履歴では時系列的に "後の" 状態であるため、その復元された状態を消去して、既定の状態で置き換える必要があります。
図 6 Navigate イベントの処理
protected void ScriptManager_Navigate(object sender, HistoryEventArgs e) {
    var productIdString = e.State["product"];
    if (productIdString == null) {
        ProductList.SelectedIndex = -1;
        ProductDetails.DataSource = null;
        ProductDetails.DataBind();
        ProductModelDetails.DataSource = null;
        ProductModelDetails.DataBind();
        ProductPhotoList.DataSource = null;
        ProductPhotoList.DataBind();
        Page.Title = "AdventureWorks";
    }
    else {
        var productId = int.Parse(productIdString);
        var product = BindProductDetails(productId);
        ProductList.SelectedIndex = (
            from p in AdventureWorksContext.Products
            where p.ProductSubcategoryID == 1 // Mountain bikes
            orderby p.Name
            select p).ToList().IndexOf(product);
        BindProductList();
        Page.Title = "AdventureWorks - " + product.Name;
    }
}
次に、状態それ自体を、ユーザーから提供されたデータと考えて検証する必要があり、これは状態を整数として解析することで行っています。そして最後に、リストおよび詳細の状態をリセットするのに加えて、状態の復元時にページのタイトルを復元します。
これらの変更の後でページを使用すると、ブラウザ内の URL は製品が選択されるたびに変更され、次のようなものになります。
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&5YLQHC81D2
OEdJU/9ZBdHUip1qx3ooPKDhCLgKogupQ=
これは醜く、とても読みにくいと思われませんか。それは、フレームワークが既定で、ユーザーから提供されたデータは危険であると判断し、改ざんを防ぐために状態をハッシュするからです。ただし、開発者は多くの場合、コードが状態を有効と見なしてユーザーがそれを改ざんできる可能性があるとしても、比較的読みやすく、それほど醜くない URL を好みます。状況によっては、URL の改ざんがむしろプラスであるとさえ考えられています (MSDN ライブラリがその良い例です。ナビゲーションをずっと簡単にするために、予測可能な方法でユーザーが msdn.microsoft.com/library/system.web.ui.scriptmanager.aspx のような独自の URL を構築できるようになっています)。
このようなシナリオを可能にするために、ScriptManager は EnableSecureHistoryState というブール型プロパティを公開しています。これを false に設定するだけで、URL は次のようにずっとわかりやすくなります。
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&product=776
その結果、単に Web フォームの AJAX バージョンに見えるように作られただけではなく、ブックマーク機能や [戻る] ボタンの最適な処理といった多くの便利な追加機能を備えた、より滑らかなページが得られます。そしてそのすべてが、1 行の JavaScript も書かずに達成されたのです。

純粋な AJAX バージョン
このページの UpdatePanel バージョンには良いところがたくさんありますが、まだ ViewState の重みを引きずっています。それを削ぎ落とすために、クライアント側にさらに多くのロジックを移動する必要があります。そのために、おもしろい JavaScript をいくらか記述することが必要になります。
ASP.NET AJAX 3.5 SP1 を使用して純粋な AJAX バージョンを書くこともできますが、データを取り出して HTML 形式に変換するのに手間がかかります。データを HTML に変換するには 2 つの基本的な方法があります。
1 つは、ほとんどのクライアント テンプレート エンジンで行われるように、静的テンプレート コンテンツと動的データ コンテンツの文字列を交互に連結する方法です。この方法は、innerHTML だけを使用して DOM とやり取りするため、比較的単純で高速に見えます。しかし、いくつかの問題点があります。
問題の 1 つは、インジェクション攻撃からの保護です。文字列を連結して HTML を生成する場合には、すべてのデータを使用前にエンコードする必要があります。そうしないと、属性に引用符を含めたり、テキスト ノードにスクリプト タグを含めたりした場合に、それが悪意によるかミスによるかにかかわらず、(セキュリティ上問題な) 任意のコード実行につながる可能性があります。テキスト属性、URL 属性、テキスト ノードのどれを挿入するのかに応じて異なるアルゴリズムが必要になる可能性があるため、エンコードは見かけよりも難しくなります。
また、テンプレート エンジンには式言語が必要です。プレーンなデータ フィールドを変更なしに挿入するのは簡単ですが、それは最も単純なシナリオに過ぎません。多くの場合は、書式文字列を適用したり、複数のフィールドを結合したり、より一般的に言えば、データを表示する前にデータに何らかの操作を加える必要があります。これはデータをテンプレートに送る前に変換を行うことでも実行できますが、その機能をテンプレート エンジンに組み込んでおけばより簡単で効率的です。実際、書式設定などの機能を追加し始めると、完全な式言語の柔軟性が必要であることがすぐにわかります。
ASP の <% %> ブロックで行うように、コードとマークアップを混在させることができれば、HTML フラグメントの周りにループを使用してマークアップを繰り返したり、単純な if ステートメントを使用した条件レンダリングを行うなど、興味深いシナリオを実現できます。そのような場合でも、真に有用なものとするためには完全な言語が必要となります。
最後に、HTML はまだ物語の半分に過ぎません。実際、AJAX アプリケーションとは、単なる DOM へのクライアント側の更新ではなく、アクティブ コンテンツに関するものです。HTML の生成が完了した後も、イベントを要素にフックしてコントロールと動作をアタッチする必要があります。それは HTML 生成後にコードで実行することもできますが、その場合 HTML とロジックの間にいびつな非対称が生じます。HTML が非常に簡単になる一方で、ロジックはより難しくなり、テンプレートの構造に関する知識が要求されます。
言い換えれば、動作をアタッチするには、それをどこにアタッチするかを知る必要があり、それはつまり、HTML テンプレート マークアップの構造に変更を加えた場合、それをアクティブ化するコードにも変更が必要になることを意味します。そのような関連付けを緩和する方法もありますが、それよりも良い解決策は、コンテンツのアクティブ化をテンプレート エンジンの一部に組み込むことです。
データから HTML を生成するもう 1 つの方法は、DOM API を直接操作して、コードから要素、属性、およびテキスト ノードを作成することです。これは一見、いくつかの理由で不適切なオプションに見えます。もちろん、標準を信奉する人々に対して同調できるという利点はありますが、ちょっとした不可思議な理由によって innerHTML よりもずっと低速です。しかし、この方法が一般に使用されない第 1 の理由は、DOM API は表現力がそれほど高くなく、結果のコードが読みにくいうえに、(少なくとも何らかの支援や追加の抽象なしには) 保守が困難であることです。優れた抽象化によってプロセス全体をずっとおもしろいものにする jQuery などのツールキットもありますが、そのようなツールを使用しても、やはり必要以上に困難な作業となります (jQuery にもいくつかのテンプレート プラグインがあるのはそのためです)。
読者の中には、マイクロソフトが既に ASP.NET AJAX Futures にテンプレート エンジンを組み込んでいたことをご存知の方がいるかもしれませんが、これはあまりに遅く、デザインが複雑であったため、大きく改善する必要性を感じていました。この失敗した最初の試みに関して良かったことは、新しいバージョンで何を改善すればよいか (つまり、遅さと複雑さ) について多くを学んだことです。
開発チームでは ASP.NET AJAX の新しいテンプレート エンジンに対して、文字列の連結から完全な DOM 操作まで多くの異なるデザインをテストし、それらのパフォーマンス、簡潔さ、および柔軟性を評価しました。また、どのようなシナリオが不可能になるかという観点からもそれらを比較しました。理想的なソリューションはありませんが、より良い妥協点となるものを選択しました。
新しいエンジンの原理はシンプルです。HTML、データ フィールド、式、宣言型コンポーネントのインスタンス化、および命令型コードを含むテンプレート コードを、等価な HTML を作成する JavaScript へと "魔法のように" 自動的に変換します。これは十分シンプルに見えますし、(ブラウザに奇妙な動作が現れない限りは) 実際にそうです。DOM API を使用することでパフォーマンス上のペナルティはありますが、十分注意して要素を DOM の外部に構築し、できるだけ少ない要素をできるだけ後で追加するようにすれば、パフォーマンスへの影響はそれほど大きくなく、結果として得られる驚くべき柔軟性はそのトレードオフに十分に見合うものです。文字列連結アプローチの問題はすべて自然に解消されるように感じられます。
インジェクション攻撃については、自動的に保護されます。テキスト ノードの作成と属性値の設定にはコードを使用しているため、エンコードを行う必要はありません。使用している API が既に安全であるからです。これは、文字列の連結による SQL の構築と、SQL パラメータの使用とを比べた場合に似ています。健全な知性の持ち主であれば、もはやそのような連結は行いません。したがって、ここで同じような危険を冒す必要もありません。
では、新しい式言語は必要でしょうか。それは既にあります。JavaScript です。テンプレート マークアップを JavaScript コードに変換する場合、生成するコードに JavaScript の式を挿入するのが最も簡単です。
最も一般的なテンプレート開発タスクである、1 回、1 方向のデータ フィールド挿入 (サーバーでは "<%= expression %>"、このシステムでは "{{ expression }}" と表記) を実現するために、しばしば敬遠されている JavaScript の機能、"with" キーワードを使用します。これにより、テンプレート インスタンスに関連付けられたデータ項目のフィールドを挿入する際に "{{ dataItem.myField }}" のような式に頼る必要がなくなります。"with" キーワードのおかげで、テンプレートに対して生成されたコードを "with(dataItem) {…}" のようなもので囲むことができるため、データ項目の任意のメンバがテンプレート関数の最上位スコープに昇格され、式の挿入が "{{ myField }}" のように単純になります。
テンプレートに動作を挿入するには、2 つの方法があります。1 つは、$attachEvent および $create コードを itemCreated イベントから記述するか、または、特別な $element 変数を使用してテンプレートにインラインで記述する方法です。この変数は、テンプレート内から利用でき、最後に作成された要素を参照します。または、ここで示す宣言型構文を使用します。たとえば、オートコンプリートおよび透かしの動作を入力タグに追加する場合は、次のように記述します。
<body xmlns:sys="javascript:Sys"
 xmlns:autocomplete="javascript:AjaxControlToolkit.AutoCompleteBehavior"
 xmlns:watermark="javascript:AjaxControlToolkit.extBoxWatermarkBehavior">
...
<input id="search" sys:attach="autocomplete,watermark"
 autocomplete:servicepath="SearchAutoComplete.asmx"
 watermark:watermarktext="Type your search terms here" />
ここでは、xmlns XHTML 名前空間宣言を使用して、HTML または body タグ上の (またはテンプレートの親タグ上の) 各宣言型動作に対してプレフィックスを登録します。これにより、XHTML マークアップを標準的な方法で拡張でき、これはサーバー コードに対する @Register ディレクティブに似ています。"xmlns:" に続く部分が、各動作またはコントロールに関連付けられるプレフィックスです。名前空間の URL は、"javascript:" プロトコルを使用して、プレフィックスを特定の JavaScript 型にマッピングします。"sys" 名前空間は特別なシステム名前空間であり、AJAX のルート名前空間である Sys 名前空間にマッピングする必要があります。
インスタンス化それ自体は特別な属性 sys:attach によって行われます。この属性の値は、インスタンス化して要素にアタッチする各動作またはコントロールのプレフィックスを並べたコンマ区切りリストです。これにより、すべてが名前空間によって適切に区別されているため、通常の HTML 属性や同じ要素の他の動作と競合するリスクなしに、これらすべての動作に対してプロパティを設定できます。
このエンジンの最も洗練された特徴の 1 つは、JavaScript コードへのテンプレートのコンパイルが実際のコンパイル手順と実によく似ていることです。つまり、コンパイルはテンプレートごとに 1 回しか行う必要がなく、いくつかのタスクをテンプレートがインスタンス化されるたびに実行する代わりに、前もって実行しておく機会が提供されます。しかし、理論の話はそれで十分です。これは、マスタ/詳細ページにどのように適用されるでしょうか。

AJAX バージョンのテンプレート
製品のリストに対するテンプレートは非常にシンプルです。
<ul id="productListTemplate" class="sys-template">
    <li>
        <a href="{{ String.format('3_Client.aspx?product={0}',
        ProductID) }}">{{ Name }}</a>
    </li>
</ul>
項目テンプレートは、単純なリンクを含むリスト項目です。リンクのテキストは単に製品の名前 ("{{ Name }}") であり、href 属性は、製品 ID からプレーンな JavaScript を使用して構築された、書式設定された文字列です。
"{{ String.format('5_Client.aspx?product={0}', ProductID) }}"
クラス "sys-template" は、ページの最初のレンダリングからテンプレートを隠すように CSS で定義されています。この単純なテンプレートのコンパイル済みコードを図 7 に示します。詳細ビューはもう少し複雑で、実際にはいくつかのインライン コードを含んでいます (図 8 を参照)。写真のリストをレンダリングするために入れ子になったテンプレートを使用することもできましたが、1 つの写真のマークアップに対して通常のループを使用する方がよりシンプルです。動的に変更されるデータを扱っていて、変更をマークアップに自動反映する場合 (これはサポートされているシナリオですが、この記事の範囲外です) であれば、入れ子になったテンプレートの使用も妥当かもしれませんが、ここでは 1 方向の 1 回のバインドを扱っているため、インライン コードが適切です。
図 7 テンプレートでコンパイルされたコード
function(__containerElement, $dataItem, $parentContext, __instanceId) {
   var __context = {}, $component, __app = Sys.Application,
      __creatingComponents = __app.get_isCreatingComponents(),
      __components = [], __componentIndex, __e, __f, __topElements = [],
      __p = [__containerElement], $index = __instanceId,
      $id = Sys.Preview.UI.Template._getIdFunction(__instanceId),
      $element = __containerElement;
   Sys.Preview.UI.Template._contexts.push(__topElements);
   with(__context) { with($dataItem || {}) {
      $element=__p[1]=document.createElement('LI');
      __topElements.push($element);
      $element=__p[2]=document.createElement('A');
      $component = $element;
      __e = document.createAttribute('href');
      __e.nodeValue = String.format('5_Client.aspx?product={0}',
                                                       ProductID);
      $element.setAttributeNode(__e);
      __p[1].appendChild($element);
      __p[2].appendChild(document.createTextNode(Name));
      $element=__p[2];
      __p[1].appendChild(document.createTextNode(" "));
      $element=__p[1];
   }
}
   for (var __i = 0, __l = __topElements.length; __i < __l; __i++) {
      __containerElement.appendChild(__topElements[__i]);
   }
Sys.Preview.UI.Template._contexts.pop();
 return new Sys.Preview.UI.TemplateResult(this, __containerElement, __topElements, __components);
}
図 8 詳細の表示
<div class="sys-template" id="productDetailsTemplate">
    <fieldset>
        <legend>{{ Name }} ({{ ProductNumber }})
            {{ String.format("{0:C}", ListPrice) }}</legend>
        <ul class="photoList">
            <!--* for (var i = 0; i < Photos.length; i++) { *-->
            <li><img src="{{ String.format('productphoto.ashx?id={0}',
                Photos[i]) }}" /></li>
            <!--* } *-->
        </ul>
        <table>
            <tr><td class="label">Summary:</td><td>{{ Summary }}</td></tr>
            <tr><td class="label">Experience:</td>
                <td>{{ RiderExperience }}</td></tr>
              ...
            <tr><td class="label">Style:</td><td>{{ Style }}</td></tr>
            <tr><td class="label">Wheel:</td><td>{{ Wheel }}</td></tr>
            <tr><td class="label">Maintenance:</td>
                <td>{{ MaintenanceDescription }}</td></tr>
        </table>
    </fieldset>
</div>
テンプレートは、最初のインスタンス化時には遅延コンパイルされますが、テンプレート マークアップの親要素をコンストラクタのパラメータとして使用して "new Sys.Preview.UI.Template" を作成することにより、準備が整います。テンプレートそれ自体は、サーバー上の Web サービスからデータを返すネットワーク呼び出しからのコールバックによって、インスタンス化されます。
AdventureWorks.GetProducts(1 /* Mountain bikes */,
  function(productArray) {
    renderProductList(productArray, productListTemplate);
    selectProduct(initialProductID, true);
});

function renderProductList(productArray) {
    var target = $get("productList");
    target.innerHTML = "";
    for (var i = 0, l = productArray.length; i < l; i++) {
        productListTemplate.createInstance(target, productArray[i]);
    }
}
これは ASP.NET 4.0 の製品版では不要になります。ASP.NET 4.0 では、DataView コンポーネントによってテンプレートの解析、コンパイル、およびインスタンス化が処理されます。このアプリケーションのコードのほとんどは最終的にはなくなりますが、見えないところでどのような動作が行われているかを示すためには有用です。また、テンプレートのレンダリングを必要とするコンポーネント開発者がその機能をどのように利用できるかも示されています。

イベント バブル
ユーザーが製品の 1 つをクリックしたときに適切な詳細ビューを表示するコードは、どこに置けばよいでしょうか。このコードでは、サーバー側でそれに相当するコードと同様に、イベント バブルを使用しているため、リスト内のすべてのリンクに対して 1 つのシングルクリック イベント ハンドラを記述することができました (それにより、新しいハンドラの作成や古いハンドラのクリーンアップについて気にすることなく、必要に応じてリストのリンクを追加または削除できました)。次のコードは、そのハンドラを示しています。リスト内のリンクに対するすべてのクリック イベントは、リスト自体へとバブルアップ (伝播) され、そこで処理されます。"e.target" は、実際にクリックされた要素への参照です。言い換えれば、これは href 属性から製品 ID を取得して関連する製品を選択できるようにするリンクです。
$addHandler($get("productList"), "click", function(e) {
    var href = e.target.href;
    selectProduct(parseInt(href.substring(href.indexOf('=') + 1), 10));
    e.preventDefault();
    e.stopPropagation();
});
それが実行されると、イベントの既定の動作 (リンクのナビゲーション) がキャンセルされ、イベントはそれ以上バブルアップできなくなります。これは、イベント オブジェクトに対して W3C 標準の stopPropagation および preventDefault メソッドを呼び出すことで行われます。これらは、Internet Explorer を含めたすべてのブラウザで、フレームワークによって使用可能となっています。

[戻る] ボタンの管理
サーバー側バージョンからまだ再現されていない唯一の機能が、履歴です。ScriptManager コントロールで履歴を有効にすると、前に使用したサーバー側 API と厳密に等価なクライアント側 API も有効になり、これらを同時に使用することもできます (それによってクライアント/サーバー状態の混在管理が可能になります)。
履歴ポイントの作成は、状態の変化 (ここでは、リスト内の製品のクリック) に対応したイベントから Sys.Application.addHistoryPoint を呼び出すことで行われます。
Sys.Application.addHistoryPoint({product: productDetails.ProductID},
    "AdventureWorks - " + productDetails.Name);
それに対応して、状態は Sys.Application の "navigate" イベントから復元されます。イベント ハンドラが受け取る HistoryEventArgs 引数には、1 つのプロパティがあります。それが状態であり、それによって復元する製品を取得できます。
Sys.Application.add_navigate(function(sender, e) {
    var ProductID = parseInt(e.get_state()["product"], 10);
    selectProduct(ProductID, true);
});

完成した製品
結果のページは、UpdatePanel バージョンと非常によく似た動作をしますが、ネットワーク トラフィックという点では比較になりません。製品が選択されると、UpdatePanel バージョンはサーバーに対して 4 KB 以上のデータを送信して約 8 KB を受信します。一方、純粋な AJAX バージョンは、"{"productId":771}" と標準の HTTP ヘッダーだけを送信して、2 KB の純粋な JavaScript Object Notation (JSON) データを受信します。つまり、ユーザーが製品をクリックするごとに、約 10 KB 分の帯域幅が節約されています。
これは、ASP.NET 4.0 で計画されている魅力的な機能の 1 つに過ぎません。go.microsoft.com/fwlink/?LinkId=126987 で詳細をご確認ください。

Bertrand Le Roy 博士は、マイクロソフトで AJAX を担当するプログラム マネージャです。この領域での開発作業に 5 年間従事してきました。また、OpenAjax アライアンスでのマイクロソフトの代表でもあります。


簡単な非同期
単純な F# の式から同時実行アプリケーションを構築する
Chance Coble

この記事は、F# 用 Visual Studio アドインのプレリリース版、および関連するテクノロジに基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。

この記事では、次の内容について説明します。
  • 従来型表記と関数型表記
  • 非同期式
  • プリミティブの作成と使用
  • 他の .NET 言語との使用
 この記事では、次のテクノロジを使用しています。
F#
目次
従来型表記の問題点
関数型プログラミング
非同期式
let! と return! を使用する
プリミティブ
独自のプリミティブを作成する
プリミティブとサポート関数
非同期呼び出しを Web に拡張する
他の .NET 言語と共に使用する
非同期プログラミングは、多くのプログラマから非常に難しいものと見なされています。そのため、非同期アプリケーションとして実行した方がより効率的であるにもかかわらず、単に非同期コードで予想される複雑さを回避するために、同期バージョンのプログラムを作成するケースが多くあります。
最近までは、同期か非同期かの選択にはほとんど意味がありませんでした。しかし、マルチコア プロセッサやサービス指向アーキテクチャなど、分散コンピューティングの機会が新しく増える中で、開発者は負担を感じ始めています。その古典的な例の 1 つが I/O 動作であり、I/O に制約される動作の実行中はプロセッサ時間の本来の利用効率が活かせません。多数のファイルを読み取って処理したり、複雑に協調する Web サービスを呼び出してデータを取得、処理した後、他のサービスに送信したりするアプリケーションが、大量に存在しています。
実際には、非同期プログラムの方が同期プログラムよりも構造が単純です。非同期プログラムは、実行するコマンドの順序について、意図的に何も規定しません。これは緩い仕様であり、定義するのも簡単です。
非同期プログラミングで開発者がつまづく要因の 1 つは、同時実行を正しく行うことに気を取られるあまり、プログラムの核心部分の単純さを忘れてしまうことです。ロックやセマフォなどの同時実行用コンピューティング ツールは、コア ロジック周辺のプログラムのレイアウトよりも、リソースの同時実行管理に関わるプログラムのレイアウトに重点を置いています。
F# の非同期式を使用する価値は、そこにあります。同時実行の制御フローから単純なプログラムを分離し、コア プログラムの単純さと読みやすさを活かすことができます。さらに、F# 言語の最も強力な側面の 1 つは、Microsoft .NET Framework と互換性を持つ他の任意の言語からシームレスに呼び出すことのできるライブラリを作成する能力であり、それによって、F# の非同期式により提供される直観的な並列処理を、既存の命令型言語である C# または Visual Basic のコード内から利用できるようになります。
この記事では、F# の非同期式に関して基本的な事項を説明します。いくつかの実装例と共に、単純な非同期プリミティブに独自のフレームワークを組み込む方法も紹介します。ここで取り上げた手法を用いることにより、難易度の高い同時実行の問題に対して、モジュール化された表現力の高いプログラムを作成できるようになります。

従来型表記の問題点
メソッドや関数を呼び出すときの従来の表記例を次に示します。
commandOne();
commandTwo();
commandThree();
この従来型表記では、commandTwo の前に commandOne が実行され、commandTwo の完了後にのみ commandThree が実行されることが明らかに示されています。
この表記法では、これらのコマンドを任意の順序で実行できるように指定するのは困難です。これは、コマンド間に単純な区切り文字を入れることで簡単に指定できます。
commandOne() | commandTwo() | commandThree()
このような表記は、3 つのコマンドの実行順序を問わない場合には便利かもしれません。おそらく、区切り文字の代わりに、実行方法を示す次のようなコンストラクトを使用すると、さらに便利でしょう。
async {commandOne()}; async {commandTwo()}; async {commandThree()}
次に、最初に実行が完了したコマンドの結果に値を設定すると考えます。
x = first(
     async { commandOne() };
     async { commandTwo() };
     async { commandThree()}; )
この命令は一見単純ですが、ほとんどの一般的なパラダイムではたいへんな手間がかかります。
この観点から見ると、セミコロンはステートメントの終わりを示すというよりも、現在のステートメントが完了するまで次のステートメントの実行をブロックする演算子のように見えてきます。そう考えれば、同期プログラムの方がより細かいプログラム定義となることが直感できます。
プログラミング言語のデザインは多くの点で進歩を遂げ、非同期プログラム デザインの問題にも新たな光が投げ掛けられています。それらをプログラミング言語の問題として考えれば、ずっと表現力に富んだ、明快で簡潔な表記を作成する方法がすぐに浮かび上がってきます。F# チームは非同期式を使用することにより、その目標に近づいただけでなく、.NET コミュニティがまだアクセスしたことのないプログラミング言語と、プログラマとの間で、やり取りを容易にしました。

関数型プログラミング
F# については、Ted Neward による入門書 (msdn.microsoft.com/magazine/cc164244) で既に多くをご存知かもしれません。ここでは、この言語に初めて触れる方のために、いくつかの重要な点を改めて説明しておきます。ただし、F# の非同期式の説明に必要な事項に限ります。
この記事では、すべてのコード例で #light コンパイラ ディレクティブを使用していますが、明示的でない場合もあります。このディレクティブにより、コンパイラはコードの各行を、他のコードとの相対的な配置に基づいて解析します。その結果、レイアウトが明確になるほか、スペースもコード本体の重要な一部分となります。この記事のコードを利用する際には、必ずタブでなくスペースを使用して、この例での配置にあわせてください。#light ディレクティブは、このディレクティブが影響を与えるコードの上に配置してください。
#light
System.Console.WriteLine "Hello World"
この記事に含まれるすべてのコードはコンパイル可能ですが、実行を簡単にするため、F# Interactive コンソールの使用を考慮してください。このコンソールを起動するには、まず Visual Studio を起動します。[表示] メニューの [その他のウィンドウ] をクリックし、[F# Interactive] をクリックします。
F# の重要な側面の 1 つは、関数も含めて、すべての要素が値であることです。既定では、すべてが不変値です。たとえば、次のコード行を考えてみましょう。
let x = 2
このコードは、x として参照できる値 2 を作成します。後で値 x が別のものに変化することはできません。別のスコープ内では x がまったく別のものを参照することも可能ですが、このスコープ内では変更できません。値は、式の結果としても作成できます。次の行は、結果 5 を x にバインドします。
let x = 2 + 3
この設定では、等号を代入としてよりもむしろ、右辺が左辺に等しいという仮定として解釈できます。このような値の設定は、関数に対しても機能します。コンソールに頻繁に書き込みを行う場合は、文字列に対する Console クラスの WriteLine メソッドを表す、省略値 emit を作成できます。
let emit:string->unit = System.Console.WriteLine
これにより、値 emit はこのコンソール メソッドを参照します。コロンの後には、関数の型シグネチャが続きます。この型シグネチャは、関数が文字列を受け取って unit を返すことを示しています (unit は C# の void に相当します)。ここでも、値 emit は可変ではなく、このスコープ内で他のものを表すことはできません。したがって、F# では、メソッドも関数も値であることがわかります。
関数に値を設定したところで、その呼び出し方法を見ておきましょう。それはつまり、プログラムが実際にどのように実行されるかということです。
関数型プログラミングでは、"呼び出し" の代わりに "適用" という用語を使用します。これは単に、関数を引数に対して適用することです。F# で関数を呼び出すには、関数をそのすべての引数に対して適用する必要があります。
関数型の設定では、関数を引数に適用する方法が多少異なります。関数型プログラミングでは、関数を値に頻繁に適用する必要があるため、スペースという最も直観的でない演算子を使用して適用を示します。たとえば、関数 Console.WriteLine を表す emit を適用する場合は、次のように記述します。
emit "Hello World"
このコードの出力結果は、次のようになります。
Hello World
または、次のようにも記述できます。
emit("Hello World")
C# の開発者は、この形がおなじみのメソッド呼び出しと似ているため、このような柔軟性は混乱の元であると感じるかもしれません。このかっこは、引数をグループ化するためにだけ存在し、それによってコンパイラはスペースの省略を許可できます。この記事では、引数や項をグループ化する必要があるときにだけ、かっこを使用しています。
最後に、いくつかの関数は 1 つの void 引数を取ります。F# では、これは unit と呼ばれ、空のかっこで表されます。
let emitter () = emit "Hello World"
emitter ()
このコードの出力結果は、次のようになります。
"Hello World"

非同期式
これで非同期式に触れる準備ができました。図 1 に、ディレクトリ内のテキスト ファイルを読み取って各ファイル内のスペース文字数を数える同期プログラムを示します。関数 countSpace は、ファイル名とサイズを受け取り、適切なバイト数で読み取るストリームを開きます。次に、リスト折りたたみ操作で、スペースであるバイトをすべてカウントします。その下にある値 "files" は、該当ディレクトリ内のテキスト ファイルの配列であり、値 "counted" は Array.map を使用して、関数 countSpace を各ファイルについて計算します。
図 1 同期によりスペースをカウントする
#light
open System.IO
open System
let countSpace filename size =
    let space = Convert.ToByte ' '
    use stream = File.OpenRead filename
    let bytes = Array.create size space
    let nbytes = stream.Read (bytes,0,size)
    let count =
     bytes
     |> Array.fold_left (fun acc x -> if (x=space) then acc + 1 else acc) 0
    count
let files = (DirectoryInfo @"C:\Users\Chance\Documents").GetFiles "*.txt"
let counted =
   files |>
   Array.map (fun f -> countSpace (f.FullName) (int f.Length))
このプログラムが終了すると、各ファイル内のスペース数を示す整数の配列が作成されています。それでは、このプログラムの同時実行バージョンの作成にはどの程度の労力が必要でしょうか。
非同期式を使用して同時実行バージョンを作成することは、識別子 async に続く中かっこ内へのラップによって関数から式を作成することを意味します (図 2 を参照)。この式は Async<int> 型であり、これは aCountSpace がその 2 つの引数に適用されたときに返す型です。それ以外で同期バージョンとの唯一の大きな違いは、ストリームが単なる読み出しの代わりに ReadAsync を呼び出すことです。このメソッドは、Microsoft.FSharp.Control.CommonExtensions 名前空間を通して利用できます。
図 2 非同期でスペースをカウントする
open Microsoft.FSharp.Control.CommonExtensions
#nowarn "057"
let aCountSpace filename size =
  async {
     let space = Convert.ToByte ' '
     use stream = File.OpenRead (filename)
     let bytes = Array.create size space
     let! nbytes = stream.ReadAsync (bytes,0,size)
     let count =
       bytes
       |> Array.fold_left (fun acc x -> if (x=space) then acc + 1 else acc) 0
     return count
     }
ここで関数を引数に適用して式を取得し、式を実行する必要があります。この点が、最終的なカウントの配列を取得する際の本質的な違いです。counted 配列の計算と同様に、Array.map 関数は aCountSpace を各ファイルに対して適用します。ただし、関数を適用すると、(aCountSpace は Async<int> を返すため) 非同期計算のみが返され、これはいつでも必要なときに実行できます。非同期計算の結果の配列は Async.Parallel プリミティブに送られ、このプリミティブによって非同期計算の配列が、結果の配列を返す 1 つの非同期計算に変換されます。その後、式が Async.Run 関数に渡されます。
let aCounted =
    files
    |> Array.map (fun f -> aCountSpace (f.FullName) (int f.Length))
    |> Async.Parallel
    |> Async.Run
最後に Async.Run が非同期計算を実行して、結果を返します。

let! と return! を使用する
stream.ReadAsync の呼び出しの反対側に感嘆符が付いていることに気付かれたかもしれません。これは誤植ではありません。標準的な let によるバインドでは、値に対して名前が作成されます。非同期式では、他の非同期式の結果に対して新しいバインド手法を利用できます。この手法は、他の式から合成できる非同期計算の作成に使用されます。後で説明するように、それが非同期式の主要な価値となっています。
非同期式は、それによって得られる値に基づいて考えることができます。次のコードは、整数 1 と見なして扱うことができます。
async { return 1}
ただし、次の例のように、非同期式の結果の値を取り扱う際には、ちょっとした問題があります。
let x = async {return 1}
let y = async { let res = x
  return 5 + res } // TYPE ERROR
式の合成を行うときには、特別な配慮が必要となります。そのため、この非同期式を現在の式の実行時に実行するよう指定するために、演算子に少し変更を加えます。式 x を作成する代わりに、式を非同期に実行し、x をその結果値にバインドする必要があります。res の型は、前の例のような Async<int> ではなく、int とします。
式を通して基となる型にアクセスするための演算子が、let! です。もちろん、Async.Run が合成全体に適用されるまで、式は実際には実行されません。
let x = async { return 1}
let y = async {  let! res = x
  return 5 + res } // WORKS!
Async.Run y
この結果は当然、整数 6 となります。
let! と同様なものに return! があり、return ステートメント内に直接他の式を合成する場合に使用されます。
let x = async { return 1}
let y = async { return! x}
この例はまだ非常に有用とは言えませんが、要点は示されています。入れ子になった式を取り扱うときには、let! と return! を使用すれば、式をバインドする方法を考え出すことに知的な労力を浪費せずに済みます。そのようなことをしなくとも、式を単にそれが返す値として扱えばよいのです。実際、上記のコードは次のように let! を使用して記述することもできます。
let x = async {return 1}
let y = async { let! temp = x
        return temp }
このように合成に焦点を合わせることが、この表記法が提供する価値の鍵となります。これにより、プログラマは基本的なプログラム ロジックに専念し、ロジックを計算の並列的性質の記述とは別個に保持することができます。

Async.Run は、非同期計算が完了するまで待つためのブロック手段です。しかし、前に示したコードの Async.Parallel の例については、まだ完全に説明していません。これは非同期式の配列に適用できる関数であり、配列上に 1 つの非同期式を返します。Async.Parallel は、非同期プリミティブであり、組み込みの例外処理および合成を利用して非同期式に新しい実行ロジックを取り込むようデザインされた、関数のファミリです。
プリミティブの考え方は、式が完全に適用されたときにどのように実行されるかを制御するものです。たとえば、ここに示したような非同期式は、他のプロセスと同時に実行されるよう実行フロー ロジックが組み込まれるまでは、実際に非同期には実行されません。
let one = async { return 1 }
優れたプリミティブの鍵は、計算を再利用可能なパターンで非同期に管理することです。Async.Parallel は非同期式の配列 (Async<'a> 配列) に適用でき、結果の型の配列を返す非同期式を返します (Async<array<'a>>)。実行時には、配列内のそれぞれの式が並列に実行されます。Async.Parallel は、各メンバをそれぞれ別のスレッドで実行し、返される配列の対応する要素内の各スレッドの結果について、管理の詳細をすべて処理します。
Async.Parallel は、非同期式に対する再利用可能な実行アプローチの優れた例です。これは特に、配列内の要素数がコンピュータ上のプロセッサ コア数に対応している場合に有用です (ただし、これらの対応関係については気にする必要がありません。スレッド プールが正しいスレッド数を作成して管理し、非同期作業を適切にスケジュールします)。配列をすばやく生成するには、F# で [| … |] 生成表記を使用できます。
let nums = [|async {return 1 + 1};
            async {return 1+ 2};
            async {return 1 + 3}|]
  Async.Run (Async.Parallel nums)
前に見た別のプリミティブとして、ファイル データを読み込むストリーム内の ReadAsync (Stream.Read の非同期バージョン) があります。このプリミティブは (Microsoft.FSharp.Control.CommonExtensions 名前空間が開いている場合)、データを読み取るストリーム上で使用できます。関数は、バッファ配列、開始点、および長さに適用されます。この関数は読み取ったバイト数を示す Async<int> を返します。もちろん、これはすべて、Async.Run または Async.Spawn などの実行関数が式全体に適用されたときにのみ実行されます。
これらのプリミティブが、合成の構成要素となります。これらが適切に構造化されていれば、並列計算の制御フローの詳細をモジュール化して、実際のプログラム ロジックから切り離すことができます。プログラムの表現は十分に抽象化されているため、これらのプログラムをさまざまな方法で実行するための計算手段として、新しい関数を試してみることができます。
これらのプリミティブは両方とも、前に作成したものと同様に非同期式です。ただし、それらのプリミティブは、Async.Run (または Async.Spawn など他の実行関数) がそれらに適用されたときに実行され、非同期アプローチの詳細を処理します。このような種類の計算を非同期式として配置することで、表現力の高い合成アーキテクチャが得られ、プログラムの実行方法を選択できるようになります。そしてこれが、大規模で複雑なシステムを構築するための手段となります。単純なプリミティブを使用して複数のプログラムを互いに結合すればよいのです。

独自のプリミティブを作成する
異なる式をつなぎ合わせるために使用するプリミティブは、既に十分な検討がなされており、作成が必要なロジックのほとんどを処理できます。多くのタスクを単純に処理できるようにするには強力なライブラリを使用するのが確実ですが、特別なケースで特定のニーズを満足するような高度にカスタマイズされたソリューションを開発すれば、大きな価値を付加できます。非同期式は、必要なときにカスタム プリミティブを追加する機会を提供します。このセクションでは、新しい非同期プリミティブの実装例について説明します。
ほとんどの命令型言語で特別に難しいことの 1 つは、他の呼び出しの完了結果に基づいて I/O 要求を省略することです。C# の if ステートメントについて考えます。
if ( a || b) {Console.WriteLine("A and B were true");}
  else { Console.WriteLine("Either A or B was false");}
a が true の場合、b はチェックされず、最初のステートメントが実行されます。最初に成功した結果を取得することが目的であれば、I/O 操作でのこのような省略は、冗長なデータ ソースに対して呼び出しを行う場合には実に有用です。そのような場合は、最初に値を返すデータ ソースだけをチェックする必要があります。そのデータ ソースがデータベース、フラット ファイル、Web サービス、外部センサのいずれであってもかまいません。
命令型言語では、これは簡単な作業ではありません。そのような方法を試みたとしても、最初に得られたデータを取得する単純な式から同時実行制御を分離するのに失敗し、同時実行の問題で行き詰まる可能性があります。例外処理のような問題がさらに複雑さを増大させます。
例として、Microsoft TerraService データベースにデータの要求を送信してみましょう。最初に、WebReferences 名前空間内の TerraService.dll ファイルを参照します。
#light
#r "TerraService.dll"
#nowarn "57"
open System
open System.Xml
open System.Xml.Serialization
open System.IO
open Microsoft.FSharp.Control
open Microsoft.FSharp.Control.CommonExtensions
open WebReferences
前に述べたとおり、すべての例で #light コンパイラ ディレクティブが使用されています。また、非同期式は現在のところ実験的な機能であるため、#nowarn "57" を含めることで警告をスキップします。さらに、TerraService Web サービス プロキシへのアクセスを提供する WebReferences 名前空間を含め、いくつかの名前空間を開く必要があります。
ここで次の手順として、弁別子付き共用体を使用していくつかの型を設定します。
type 'a Result =
   | Complete of 'a
   | Failure of 'a

type 'a RetrievedValue =
   | File of 'a
   | Web of 'a
   | Timeout
   | Error

let ts = new TerraService()
Result 型は、計算の終了について記述しています。RetrievedValue は、TerraService 呼び出しのソースおよび発生する可能性のあるエラーを記述しています。結果はファイル システムにキャッシュするため、共用体のパターンとして File と Web の両方を含めています。最後に、新しい TerraService 型をインスタンス化します。

プリミティブとサポート関数
ほとんどの F# コードは、内側から外側、下から上へ読み取るのが最良です。図 3 の場合、最初に理解する必要があるのは first 関数であり、これは実際には非同期プリミティブです。first の唯一の役割は、非同期式のリストに対して実行される非同期プリミティブを作成することです。これらの非同期式は、前に示した弁別子付き共用体の Result 型の 1 つを返します。
図 3 updateAndContinue
let updateAndContinue n i cont res =
     lock n (fun () -> if !n = -1 then n:=i)
     if (!n=i) then cont res

let aContinueFirstSuccess cont n i x =
    async {
      let! y=x in
      do match y with
         | Complete(res) -> updateAndContinue n i cont res
         | _ -> ()
     }

let continueIfFirst cont n i x =
   Async.Spawn (aContinueFirstSuccess cont n i x)

let first (ls:Async<'a Result> List) =
  Async.Primitive
    (fun (cont,exn) -> let n = ref (-1) in
List.iteri (continueIfFirst cont n ) ls)
すべての非同期プリミティブと同様に、コアとなるセクションは、続行関数 (cont) と例外関数 (exn) を受け取る関数です。式の計算で何が起こるかに応じて、いずれかの関数が呼び出されますが、各分岐がどのような状況で起こるかを定義するのは開発者の仕事です。
関数 first は、初期値が -1 である整数への参照を作成します。F# の参照セルは、可変値を作成する手段の 1 つです。これらは、C# の変数と同様に := 演算子を使用して代入できます。これらの値には、名前の前に感嘆符を付けることでアクセスします (図 3 の updateAndContinue 関数内を参照)。
-1 は、参照が設定されていないことを示すフラグです。次に、List.iteri コンストラクトを使用して非同期式のリストが反復的に参照され、cont および整数参照 n を持つすべての式に対して continueIfFirst が呼び出されます。List.iteri は、List.iter とは異なり、対象リスト内の要素の整数インデックスを適用します。これは実行された各式の識別に使用されます。
関数 first のすぐ上にある関数 continueIfFirst は、新しいスレッドを生成し、続行関数 cont、整数参照、非同期式のリスト内のインデックス (first 内の ls から)、および実行する式に基づいて、非同期式 aContinueFirstSuccess を起動します。
元の非同期式リスト内の各要素に対して別々のスレッドで実行されている非同期式 aContinueFirstSuccess が、現在のスレッドの非同期式を実行し、その結果をチェックします。プリミティブの実装では、各非同期式が、('a で表される) ジェネリック型にバインドされた弁別子付き共用体から Result 型の 1 つを返すことを思い出してください。結果が Complete の場合は、元の非同期式を除いた同じ引数を渡して updateAndContinue を呼び出すことにより、コードが更新され続行されます。関数 updateAndContinue により、n を最初に更新した式だけが続行できるようになります。参照セル n はロックを使用したスレッド セーフな方法で更新され、後で値が i に等しいことがチェックされます。参照セル n と i が等しい場合は、このスレッドが最初に n を更新したスレッドと判断されます。この式が計算されたので、結果を res として渡します。
最終的に、プリミティブ first とそのいくつかのサポート関数によって、非同期計算のリストを生成して最初の成功した結果を返す実行メカニズムが提供されます。
このコードから作成できる最もわかりやすい機能の 1 つは、タイムアウト メカニズムです。結局、計算のセットのうち最初のものが返されるときには、ほとんどの場合、ユーザーが待つ必要のある最大時間制限が存在しています。
タイムアウトの作成に必要な 2 つの関数を次に示します。最初の関数は timeout と呼ばれ、値を受け取り、その時間 (ミリ秒) が経過したときに値を返します。
let timeout i x =
  async {
    do System.Threading.Thread.Sleep(i:int)
    return Complete(x)
    }
2 番目の関数は、terminateInSeconds という関数です。これは、既存の非同期式上に合成されることが意図されています。
let terminateInSeconds time zero expr =
  let success =
    async {
      let! res = expr
      return Complete(res)
    }
  first [ success; timeout (1000*time) zero]
first を使用して、この関数は与えられた式を実行し、所定の時間内に式の実行が成功しなかった場合は引数 zero を返します。
非同期計算リストの実行の非常に単純な利用法として、カウントダウンの作成があります。カウントダウンは、次に定義する非同期式のように記述できます。
let rec countdown x =
   async {
     do Console.WriteLine(x:int)
     do System.Threading.Thread.Sleep(1000)
     if x = 0 then return Complete("Done")
     else
      let! res =
       first [ countdown (x-1);
         timeout ((x-1) * 1000) "Launch"]
      return Complete(res)
    }
これを Async.Run プリミティブで実行し、引数 5 を渡すと、1 秒間隔のカウントダウンが得られます。
> Async.Run (countdown 5);;
5
4
3
2
1
0
val it : string Result = Complete "Launch"
結果が "Done" ではなく "Launch" であることに注意してください。それぞれの場合にタイムアウトが発生しますが、再帰チェーン内で次のカウントダウンを開始したときにのみ、非同期計算を終了しています。Async.Spawn を使用して実行した場合も同じことが起こりますが、返された式はすぐに出力され、カウントダウンの実行中にも他の操作を実行できます。

非同期呼び出しを Web に拡張する
このような種類のショートカットを TerraService Web サービス呼び出しに含めることができれば理想的です。このようなオーケストレーションには、ファイル システム、データベース、Web、複雑な遠隔計測など、種類を問わずすべての I/O を含めることができます。接続の速度や信頼性が低い状況では、データの冗長なソースを作成することが有用かもしれません。このような状況は、低性能な接続インフラストラクチャを持つ環境にグローバル ソフトウェアを配置する場合に多く見られます。
2 つの非同期式を記述しましたが、そのいずれも単体では何も非同期操作を行いません。1 つ目は aGetPlaceWeb であり、これはある場所の市、国、および州という 3 つ 1 組の文字列を受け取ります。この式は TerraService を使用して場所を取得し、savePlace を使用してそれをローカル ファイルに保存します。
type TerraService with
  member ts.AsyncGetPlaceFacts(p) =
   Async.BuildPrimitive
    ((fun (callback,asyncState) ->
      ts.BeginGetPlaceFacts (p,callback,asyncState)),
    ts.EndGetPlaceFacts)

let aGetPlaceWeb (city,state,country) =
  async {
    let p = new Place(City=city, State=state, Country=country)
    let! facts = ts.AsyncGetPlaceFacts(p)
    do savePlace facts
    return Complete(Web(facts.Center ))
  }
2 番目は aGetPlaceFile であり、これもやはり場所を示す 3 つの文字列を受け取ります。
let aGetPlaceFile placeVals =
  async {
    let name = nameFromTriple placeVals
    if File.Exists(name) then
      let coord = coordFromFile name
      return Complete(File(coord))
    else return Failure(Error)
  }
この式は nameFromTriple を呼び出してファイル名を取得してから、そのファイルが存在するかどうかを確認します (Web 呼び出しが既にその場所を取得していれば存在します)。したがって、任意の場所に対する最初の呼び出しでは、aGetPlaceWeb が情報を取得する唯一の可能なオプションです。その最初の呼び出し後は、aGetPlaceFile から情報を取得できます。aGetPlaceFile の方が Web への呼び出しよりもずっと速いと思われているかもしれません。もちろん、可能であればそれを利用した方がよいでしょう。さらに、タイムアウト メカニズムを含めることで、所定の時間内にどの呼び出しも成功の結果を返さなかった場合に、適切なメッセージを出力できます。
場所のリストは greatPlaces として作成されます。次に、getWebPlaces が非同期式の aGetPlaceWeb を greatPlaces にマッピングします。つまり、非同期式の aGetPlaceWeb は、greatPlaces リストのすべての要素に対して実行され、非同期式のリスト (場所ごとに 1 つ) を返します。getFilePlaces では、aGetPlaceFile を使用して同じことが行われます。
let greatPlaces = [
  ("Austin","Texas","United States");
  ("Las Vegas","Nevada","United States");
  ("Asheville","North Carolina","United States");
  ("Arlington","Virginia","United States");
  ("Laguna Beach","California","United States")]
let getWebPlaces = List.map aGetPlaceWeb greatPlaces
let getFilePlaces = List.map aGetPlaceFile greatPlaces
これらの場所と次に取得する式が定義されたら、それらをまとめて 1 つの完全なリストを作成する必要があります。そのリストの各要素では、ファイル取得式と Web 取得式の両方が非同期で実行される必要があります。そこで、first プリミティブが使用されます。
List.combine 関数を使用してそれらを結合したら、各ペアが first プリミティブに渡され、それぞれを並列に計算して最初の結果だけを完了行に返す 1 つの非同期式が作成されます。
let combineAsyncFirst l1 l2 =
  List.map (fun (one,two) -> first [one;two]) (List.combine l1 l2)

let combined = (combineAsyncFirst getWebPlaces getFilePlaces)

let firstPlaces = List.map (terminateInSeconds 3 Timeout) combined

let e = Async.Parallel firstPlaces

let result = Async.Run e
firstPlaces リストはこの結合されたリストであり、割り当てられた秒数後に終了する terminateInSeconds 非同期式を使用しています。ここでは、弁別子付き共用体の RetrievedValue から Timeout 値を返します。
このコードでは、合成によって提供される能力がその真価を発揮し始めています。計算のペアのリストを作成し、最初に成功して終了したもの (その理由が速度であってもエラーであっても) を返すことは、命令型言語では簡単ではありません。そのうえ、全体の処理を省略するようなタイムアウトを付加することは、相当に手間のかかる作業です。しかし、合成と共に非同期式を使用すれば、それよりずっと複雑な非同期計算を実現するための、必要な小さな部品すべてを簡単に作成できるようになります。
次のコードに示されるように、これらの非同期計算はすべて並列で実行され、その結果、場所情報のリストを Web から、または既に利用できる場合にはファイル システムから得ることができます。
let e = Async.Parallel firstPlaces
図 4 は、この非同期式で作成された計算モデルを表しています。同時に実行される 2 つの I/O プロセスのうち最初のものを返すためのフレームワークが用意され、タイムアウト メカニズムが実現されています。それにより、まったく同じメカニズムを再度使用して、2 つの冗長ソースから同時にデータを取得できます。これらがすべて、各 I/O タスクを並列に実行する大規模なフレームワークに組み込まれています。
図 4 非同期計算モデル (クリックすると拡大画像が表示されます)
並列メカニズムと first メカニズムの間でフレームワークを再利用して、この合成を拡張し、ロジックの大規模なネットワークを作成できることは、容易に想像されます。このカウントダウン メカニズムは、このアーキテクチャがどれだけの柔軟性を持ち、時計のようなフレームワークをどのように作成できるかを示しています。並列メカニズムと first メカニズムの連携によって部品が作成され、合成によってそれらの統合が可能になります。
後で結果を破棄しても問題のない Web サービスを呼び出して、必ずしも必要でないかもしれない動作をその Web サービスに実行させることは、欲張りすぎと感じられるかもしれません。しかし、それが完全に意味を持つような状況はさまざまに存在します (詳細については、msdn.microsoft.com/magazine/cc872850.aspx で今月号の End Bracket のコラムを参照してください)。著者はこれまで、信頼性の低いネットワーク インフラストラクチャが実際の複雑さを生んでいるような世界の地域で、ソフトウェアを展開してきました。それが実際、このような非同期合成のアイデアが浮かんだ要因です。そのような状況ではしばしば、信頼できるネットワーク ソフトウェアを持つことが、複数のソースの情報を冗長的に呼び出す能力を意味します。

他の .NET 言語と共に使用する
.NET Framework には、それぞれ異なる長所を持つ多くの言語が含まれています。F# の最も強力な側面の 1 つは、他の任意の .NET 準拠言語からシームレスに呼び出すことができるライブラリを作成する能力です。これは、F# が関数型とオブジェクト指向のハイブリッド言語であるためです。もちろん、これはただでは手に入りません。F# ライブラリは、C# など他の言語に慣れた開発者にも直観的に理解できるように、慎重にデザインされる必要があります。
たとえば、C# のプログラマに対して理解しやすい構造を作成するために、C# の開発者が直観的と感じるクラス型のアーキテクチャに関数を埋め込むことが多くあります。ただし、基となるコードはまったく異なる言語で書かれています。
type GreatPlaces() =
  let places = List<(string * string * string)>()
  member self.AddPlace(city,state,country) =
        places.Add( (city,state,country) )
  member self.RetrievePlaces () =
        let lplaces = List.of_seq places
        let result = runPlaces lplaces
        Array.combine (Array.of_seq places) result
        |> Array.map compileResultToken
F# で Web サービス呼び出しを設定するには、コマンド ラインおよび自動コード生成機能を使用できます。C# のような成熟した言語では、これらのツールは Visual Studio に統合されており、参照をシームレスに生成します。F# では、同じ結果を実現するためにいくつかのちょっとした手順が必要となります。最初に、.NET コマンド ライン ツールを開き、wsdl.exe を使用して Web サービス wsdl を解析し、C# でプロキシを生成します。次に、その C# を、プロジェクトが参照する DLL へとコンパイルします。
たとえば、前に述べた TerraServer Web サービスとやり取りするのに使用するスクリプトを次に示します。
> wsdl /namespace:WebService http://terraservice.net/terraservice.asmx
Microsoft ® Web Services Description Language Utility
Writing file 'c:\dev\TerraService.cs'
>csc /target:library TerraService.cs
これにより、Web サービスを他のクラスと同様に呼び出すために必要なライブラリが生成されました。この 2 つの手順の後、F# ソースの参照インジケータが続きます。F# では、外部ライブラリへの参照をテキスト内に直接追加できるため、TerraService ライブラリへの呼び出しの前に、これをソース ファイルに含めることができます。
#r "TerraService.dll"
これは .NET のよく知られた能力であり、それぞれ小さな問題を解決するためのプログラムを、その問題に最も適した言語を使用して連携させることができます。

Chance Coble (理学修士) は、Ideal Innovations Inc. に勤務する科学者です。数年前に Haskell から関数型プログラミングに興味を持ち、機械学習、データ マイニング、およびデータ統合に関連したグローバル生産ソリューションに対して、この 2 年間 F# ソリューションの開発を続けています。


編集者のメモ
Howard Dierking


ツールボックス
現実的なテスト データ、Dare Obasanjo のブログ、色分けコーディング ツール、その他
Scott Mitchell

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

目次
データベースのテスト データの作成
注目のブログ
Web ページでの色分けコーディングされた構文の表示
推薦図書

データベースのテスト データの作成
データ駆動型アプリケーションを構築およびテストする際は、開発データベースのデータが実稼動時に使用されるデータを正確に反映していることが重要です。開発時と実稼動時に同じ量および同じ種類のデータを使用することで、設計者はユーザー インターフェイスのより優れた評価を実施し、開発者はシステムのスケーラビリティを判断し、テスト担当者はより現実的なシナリオに基づいてテスト ケースを作成できます。また、デモや販売プレゼンテーションのための現実的なデータを用意するのにも役立ちます。
独自の SQL スクリプトを作成してテスト データを生成することもできますが、Red Gate Software の SQL Data Generator (Version 1.1) を使用すると、テスト データの生成がはるかに簡単で迅速になります。SQL Data Generator では、マウスの数回のポイント操作とクリック操作によって、現実的な値を持つ何百万件ものレコードを生成し、生成したデータを簡単にカスタマイズできます。
開始するには、SQL Data Generator を起動し、新規プロジェクトを作成します。これにより、プロジェクト構成 (Project Configuration) ウィザードが表示されます。ウィザードでは、サーバーとデータベースを選択し、データ生成の前後に実行するカスタム SQL スクリプトがあるかどうかを指定し、INSERT および DELETE トリガを起動するかどうか、または CHECK 制約を適用するかどうかなどのオプションを選択します。
データベースとオプションを選択すると、SQL Data Generator に 3 つのペインのインターフェイスが表示されます。左側のペインにはデータベース内のテーブルの一覧が表示され、生成されたデータをどのテーブルに割り当てるかを選択できます。左側のペインからテーブルを選択すると、右側の 2 つのペインにそのテーブルの情報が読み込まれます。右上のペインには、テーブルのデータ生成設定の概要が示されます。ここでは、追加する行数、生成されたデータを追加する前にテーブルのデータを削除するかどうか、および無効なデータの挿入を試みたときに実行される操作を選択できます。右下のペインには、生成されるデータのプレビューが表示されます。
特定のテーブル列のデータをカスタマイズするには、左側または右下のペインからその列を選択します。すると、右上のペインに列の設定が読み込まれます。ここでは、この列に生成された値が一意である必要があるかどうかを指定したり、NULL にする必要がある値の割合を指定したりできます。また、単なるランダムな文字列、数値、および日付の代わりに現実的な値が含まれるようにするために、データの生成方法を指定することもできます。データは .csv ファイル、SQL クエリ、使用可能な値の重み付けリスト、または正規表現から取得できます。
また、色、電子メール アドレス、名、姓、住所、市区町村、郵便番号、クレジット カード番号、会社名、URL などのカテゴリを選択し、SQL Data Generator でそのカテゴリに属する事前定義された値を使用することもできます。もちろん、各テーブルのそれぞれの列の生成方法を選択することもできますが、SQL Data Generator ではテーブルと列の名前やデータ型から適切な値を論理的に推測します。たとえば、FirstName という文字列ベースの列のデータを生成する場合、SQL Data Generator は First Name (名) カテゴリの値を使用します。
設定を確認したら、[Generate Data] (データの生成) ボタンをクリックして、選択したテーブルにデータを追加します。SQL Data Generator を使用すると、大量のデータベース データが簡単で迅速に生成されます。
価格 : 295 ドル。
go.microsoft.com/fwlink/?LinkId=124071
SQL Data Generator によるデータ レコードの生成とカスタマイズ (クリックすると拡大画像が表示されます)

注目のブログ
ここ数年間でソーシャル ソフトウェアがこれほど普及したのは驚くべきことです。ほとんどの人がブログを書き、Facebook、LinkedIn、Blogger.com などのサイトはインターネットで多数の閲覧者数を誇ります。多くのソーシャル ソフトウェアは Web サービス、XML、および他のオープン標準を利用して、コンテンツをシンジケートし、サードパーティ製のアプリケーションとの統合を可能にしています。これらのテクノロジへの洞察や、関連する開発トピックにかけては、Dare Obasanjo のブログ以上のものはありません。
Dare は、何年もの間さまざまなソーシャル ソフトウェア標準の評価と運用に携わってきました。現在は Microsoft Contacts チームのプログラム マネージャとして Windows Live Messenger および Windows Live Spaces のバックエンド サービスを担当し、Microsoft .NET Framework の XML および XML スキーマ API に従事しています。また、人気の高い RSS アグリゲータである RSS Bandit の作成者でもあります。こうしたバックグラウンドを通じて、彼はソーシャル ソフトウェアで使用される標準への多数の興味深い洞察を行い、これらの標準に対するプログラミングのヒントや提案を提供しています。
Dare のブログを読むことはときに、消防ホースからの水をひとすすりするようなものです。平日はほぼ毎日新しいブログ エントリが追加され、そのほとんどは長く、情報が満載です。しかし、ソーシャル ソフトウェアのしくみに対する理解を深め、関連する標準への認識を深めたい方にとって、Dare のブログは欠かせないサポートです。
25hoursaday.com/weblog

Web ページでの色分けコーディングされた構文の表示
すべての最新のプログラミング環境では、ソース コードの判読性を高めるために色分けコーディングを使用しています。Visual Studio ではキーワード、型、文字列、およびコメントが異なる色で表示され、ユーザーはこれらを含む他のトークンのフォント、サイズ、およびスタイルをカスタマイズできます。しかし、多くの印刷物やオンラインのコード サンプルでは、コーディングが色分けされていません。
書籍やコンピュータ雑誌で構文が強調表示されていない理由にはカラー印刷に関するコストが考えられますが、もっと多くのブログや Web サイトでソース コードのサンプルを色分けコーディングしても良さそうなものです。Web ページで構文を強調表示するための課題には適切な HTML および CSS の作成がありますが、この処理を簡単に行うためのフリー ツールがいくつかあります。
1 つの選択肢は、Quick Highlighter (go.microsoft.com/fwlink/?LinkId=124072) などの、オンラインの構文強調表示サービスを使用する方法です。Quick Highlighter のインターフェイスは、書式設定する対象のコードを入力する大きなテキスト ボックス、96 個の異なるプログラミング言語を選択できるドロップダウン リスト、およびいくつかの書式設定オプションで構成されています。構文を色分けコーディングする HTML および CSS を生成し、ブログ エントリや Web ページに貼り付けることができます。
筆者のお気に入りの色分けコーディング ツールの 1 つは、Colin Coller による Visual Studio アドインの CopySourceAsHtml です (詳細については、go.microsoft.com/fwlink/?LinkId=126646 を参照)。CopySourceAsHtml を使用すると、構文を色分けする Web ページに移動する必要はなく、Visual Studio から HTML および CSS を生成できます。
書式設定するコードを選択し、右クリックし、[Copy As HTML] (HTML としてコピー) メニュー オプションを選択するだけです。これにより、さまざまな書式設定オプションを含むダイアログ ボックスが開きます。[OK] をクリックすると、対応する HTML および CSS が生成され、Windows クリップボードにコピーされます (Colin による CopySourceAsHtml の最新の更新は Visual Studio 2008 のリリース前でした。Guy Burstein が作成した Visual Studio 2008 互換バージョンは、go.microsoft.com/fwlink/?LinkId=124074 からダウンロードできます)。
ASP.NET ページのサーバー側コードから構文を強調表示することもできます。squishyWARE (go.microsoft.com/fwlink/?LinkId=124076) の squishySyntaxHighlighter コンポーネントと 2 行のコードによって、通常の C#、Visual Basic、または XML 構文の文字列を、強調表示された HTML および CSS マークアップに変換できます。
価格 : すべて無料。
オンラインの構文強調表示サービスの Quick Highlighter (クリックすると拡大画像が表示されます)

推薦図書
ADO.NET は System.Data および System.Xml 名前空間内のクラスの集まりであり、開発者はこれらを使用してさまざまなデータ ストアのデータをプログラムで照会、挿入、更新、および削除できます。データ駆動型アプリケーションに取り組むすべての開発者が ADO.NET の強固な基礎を必要としていた時期がありました。しかし、.NET が成熟するにつれ、マイクロソフトやコミュニティからのさまざまなツールやアプリケーション アーキテクチャによって、ADO.NET の低水準の詳細と複雑さの多くがカプセル化されました。しかし、今はまだ ADO.NET のすべてを忘れるわけにはいきません。依然として ADO.NET を使用するかなりのアプリケーション コードが存在し、既存アーキテクチャのデータ アクセス層の機能を拡張する必要がある場合は、テクノロジに対するしっかりとした理解が重要になります。
筆者が ADO.NET コードを作成または保守する場合は、Bill Hamilton 著『ADO.NET 3.5 Cookbook』(O'Reilly、2008 年) を手元に置いておきます。『ADO.NET 3.5 Cookbook』には、データベース接続文字列の格納場所から SQL 文の非同期実行の方法まで、ADO.NET の一般的な問題に対する 200 以上のレシピが記載されています。ある操作を実行する方法を思い出すのに行き詰ったら、この本を開いて目次をめくり、レシピを見つけてから仕事を始めます。
各レシピは、"ストアド プロシージャから返された出力パラメータにアクセスする必要があります。" などの問題提起から始まります。その後、解決策と議論が続きます。この問題/解決策/議論の形式は自己完結型のレシピであり、簡単に理解できます。さらに、ほぼすべての解決策が、Windows フォームまたは Web アプリケーションに対し、コンソール アプリケーションとして紹介されています。これにより、ユーザー インターフェイスの作成をステップ実行する必要がなくなり、解決策セクションが簡素化されています。
ADO.NET でのアーキテクチャの決定に関する詳細な洞察や、LINQ などの新しい ADO.NET 3.5 の機能を重点的に取り扱う本を必要としている方には、この本は適していません。一般的な問題に対する解決策をすばやく見つけたいときに役立つリファレンスをお探しの場合は、『ADO.NET 3.5 Cookbook』を読んでみてください。
価格 : 54.99 ドル。
go.microsoft.com/fwlink/?LinkId=124077

ご意見やご質問は、Scott (toolsmm@microsoft.com) までお送りください。

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


CLR 徹底解剖
Silverlight 2 のセキュリティ
Andrew Dai

目次
アクセス許可と信頼されていないコードのサンドボックス化
透過性モデル
CoreCLR での透過性
継承ルール
Silverlight 透過性への公開
2008 年 8 月号の MSDN Magazine の「CLR 徹底解剖」のコラムでは、Andrew Pardoe が CoreCLR のセキュリティ モデルについて簡単に説明しました。彼は、Silverlight でコード アクセス セキュリティ (CAS) ポリシーが使用されなかった理由、そして Transparent コード、SafeCritical コード、および Critical コードの違いについて説明しました。今回は、透過性モデルがどのようにして誕生したのか、Silverlight でのその動作、および今後の展望について、さらに詳しく解説します。

アクセス許可と信頼されていないコードのサンドボックス化
透過性モデルについて詳しく説明する前に、完全な Microsoft .NET Framework 内での CLR セキュリティについて背景知識を少し持っておくと役に立ちます。CLR は、アクセス許可によって表され、コードの実行元となる場所に基づいてコードが保持する機能または権限、コードの署名 (存在する場合)、およびその他を判別するためのメカニズムを提供します。これにより、インターネットなどの信頼されていない場所からのコードが、ローカル コンピュータの Program Files フォルダなどの信頼できる場所からのコードと同じ特権を持たないことが保証されます。
CLR は、各マネージ アプリケーションを専用のアプリケーション ドメインで実行します。信頼されていない場所からのコードを安全に実行するために推奨されるメカニズムは、サンドボックス化されたアプリケーション ドメインで実行することです。このようなドメインで実行するコードは、部分的に信頼されたコードに適したアクセス許可に限定された環境であるサンドボックスの権限に制限されます。一般に、サンドボックス化されたアプリケーション ドメインは、完全な信頼と部分的な信頼の 2 つのグループにコードを分類します。これら 2 つのグループの目的とこれらを区別する方法については、このセクションでさらに詳しく説明します。
この分離を実装および使用しやすくするため、簡易サンドボックス化 API と呼ばれるものが .NET Framework 2.0 で導入されました。この API は、そのサンドボックスに対する指定されたアクセス許可セットと、グローバル アセンブリ キャッシュ (GAC) に含まれない完全に信頼されたアセンブリのリストを使用して、各アプリケーション ドメインを作成します。GAC 内のすべてのアセンブリは既に完全に信頼されています。このアプリケーション ドメインに読み込まれ、完全に信頼されたアセンブリのリストに含まれていないすべての項目は、サンドボックスに付与されたアクセス許可セットを取得します。インターネットから実行されるアプリケーションの場合、これは組み込みのインターネット アクセス許可セットです。CLR セキュリティ チームのソフトウェア設計エンジニアである Shawn Farkas が、この API の使用方法について、go.microsoft.com/fwlink/?LinkId=124952 にすばらしいブログを投稿しています。
このモデルでは、ホスト (ブラウザ プラグインなど) によって、その下で実行されるアプリケーションごとにアプリケーション ドメインが作成されます。これらは部分的な信頼として読み込まれて、サンドボックスのアクセス許可セットを取得します。ホストは、完全に信頼されたマネージ関数またはネイティブ関数の一部を (安全に) ラップし、それらをホストされるアプリケーションに対してライブラリの形式で提供することが必要な場合もあります。このようなライブラリ アセンブリは、完全に信頼されたアセンブリ リストに含まれます。たとえば、ファイル システム アクセスは特権が必要な操作ですが、情報のローカルな保存は便利であり、正しく行えば安全です (固定した場所、実行しない、など)。ただし、この機能を安全に公開するのは難しい場合があります。

透過性モデル
透過性モデルは実際には CoreCLR (CLR の Silverlight バージョン) の新機能ではなく、.NET Framework 2.0 から存在していたものです。最初は、部分的に信頼されたコードにサービスを公開する Framework ライブラリの数の増加と、.NET Framework チーム内で行われる内部セキュリティ監査の問題に対処するために導入されました。APTCA (AllowPartiallyTrustedCallers) 属性を持つライブラリは、完全に信頼されたサービスを部分的に信頼されたコードに公開します。このようなコードは危険である可能性があり、APTCA ライブラリの方で適切なセキュリティ検査が必要です。
透過性が導入される前は、APTCA アセンブリ全体がコード監査の対象でした。これには非常に大きなコストがかかり、特に、ライブラリの多くは共通の特権付き関数を呼び出す可能性があることを除けば完全に安全であることを考えると高コストでした。
特権を持つコードと特権のないコードを厳密に分離する境界を作成することを目的として、透過性が開発されました。このモデルでは、Transparent と Critical という 2 種類のコードがあります。Transparent コードは、以下の制限の対象になります。
  • アクセス許可をアサートしたり昇格したりすることはできません。
  • 安全でないコードまたは検証できないコードを含むことはできません。これは Critical 操作と見なされ、Transparent コードから発生した場合は要求が注入されます。
  • Critical コードを直接呼び出すことはできません。これに対する例外については、このセクションで後述します。
  • ネイティブ コードまたは SuppressUnmanagedCodeSecurity 属性を持つ API を呼び出すことはできません。
  • LinkDemands を満たすことはできません。すべての LinkDemands は、完全なスタック ウォーク要求になります。
すべてのアクセス許可要求は、Transparent コードを通過してアプリケーション ドメインに到達します。Transparent コードはこのような要求をアサートできません。したがって、Transparent コードは常にサンドボックスの権限に制限されます。Critical コードにはこのような制限はなく、前に挙げた操作を実行することができるため、APTCA アセンブリに対するセキュリティ監査の負担は、アセンブリの Critical 対象領域に削減されます。
それでは、この 2 種類のコードはどのように相互作用するのでしょうか。開発者は、Critical API が Transparent コードから呼び出しても安全であると判断した場合 (たとえば、オペレーティング システムを呼び出して日時を取得する API)、API を TreatAsSafe (コードでの実際の属性は SecurityTreatAsSafe) とマークして、この呼び出しを有効にできます。また、2.0 の透過性モデルでは、透過性ルールはアセンブリ内でのみ適用されることの結果として、パブリックの Critical は暗黙で TreatAsSafe になるという点も重要です (このコラムでは、この透過性モデルを 2.0 透過性モデルと呼びます。この透過性モデルは、実際には、.NET Framework Versions 3.0、3.5、および 3.5 SP1 の一部でもあります)。したがって、パブリックの Critical メソッドは Transparent コードから呼び出すことができます。
2.0 透過性モデルはデスクトップ バージョンの .NET Framework の以降のバージョンにおいて非常に役に立ち、これにより、セキュリティが推測しやすくなり、部分的に信頼されたアプリケーションへのサービスの公開がはるかに簡単になりました。CoreCLR では、透過性システムの改善を行うだけでなく、ランタイムおよびそれに対して作成されるコードにセキュリティを適用するための主要なメカニズムとしてこれを作成することもできました。

CoreCLR での透過性
透過性はアセンブリのセキュリティの評価で非常に役に立ちますが、.NET Framework でのセキュリティの決定要因は透過性だけではありません。異なるアプリケーション ドメインは、アクセス許可セットが異なるさまざまなサンドボックスを持つ場合があります。インターネットから実行されるアプリケーションと、企業のイントラネットから実行されるアプリケーションでは、特権が異なります。したがって、コードでこれらのアクセス許可を検査し、アサートする必要があります。
CoreCLR では、それに対して作成されるすべての Rich Internet Application が Web ブラウザ内で実行することがわかっているため、簡素化による想定が可能になりました。最初の想定は、各アプリケーション ドメインは固定のサンドボックスを持ち、各 Silverlight アプリケーションの権限は正確にわかっているため個別のアクセス許可を扱う必要はない、というものです。アクセス許可セットは、完全な信頼と Silverlight サンドボックスの 2 つだけです。第 2 の想定は、すべての Silverlight アプリケーションは部分的に信頼されるため、Silverlight サンドボックスのアクセス許可を超えて昇格することはできません。
これらの大幅な簡素化により、次のような結果がもたらされます。すべての Silverlight アプリケーションは Transparent であり、Silverlight サンドボックスのアクセス許可セットを受け取り、Silverlight プラットフォーム ライブラリ (そのほとんどは完全に信頼されています) は Silverlight アプリケーションによってのみ呼び出されるので、固有のアクセス許可は必要ありません。危険な API を Critical にするだけで十分です。最後に、要求がないため、アクセス許可のスタック ウォークを停止する必要がなく、したがってアサートの必要がありません。
こうして、透過性は CoreCLR での唯一のセキュリティ実施メカニズムになり、CAS の多くを無視できます。実施メカニズムをさらに強力なものとするため、Transparent コードに対するルールを厳密にし、透過性システムがいっそう直観的で効果的になるようにさらなる改善を行いました。
2.0 透過性モデルでは、Transparent コードで実行される安全でないコードまたは検証できないコードが原因で、完全な障害または例外ではなく、UnmanagedCodePermission の要求が注入されることに気付かれたかもしれません。一般に、このアクセス許可は完全な信頼でのみ使用可能である必要があるため、このコードは部分的な信頼では失敗しますが、それ以外では動作します。
ただし、CoreCLR ではアクセス許可の要求という概念がないため、これが発生すると、(新しい透過性モデルに要求が注入されるのではなく) 例外がスローされます。この種のコードはどちらのモデルでも失敗しますが、最終的に失敗する前に要求のスタック ウォークを処理する必要がないため、パフォーマンスは向上しています。
また、Silverlight では、すべてのコードは既定で Transparent です。これには、Silverlight プラットフォーム ライブラリも含まれます。すべての Critical コードは、明示的に注釈付けされる必要があります。これにより、既定で安全が保たれ、暗黙の Critical メソッドが誤って作成される可能性が低くなります。型の継承とメソッドのオーバーライドに関してさらにルールが追加されましたが、これについては継承ルールのセクションで説明します。
TreatAsSafe とマークされた Critical API を Transparent コードから呼び出せることについては、既に説明しました。また、デスクトップ フレームワークでの透過性はアセンブリ内でのみ適用されるため、パブリックの Critical API は暗黙的に TreatAsSafe であることについても、既に指摘しました。CoreCLR では、Critical であり、かつ Transparent コードから呼び出すことのできるコードを記述するため、新しい属性 SafeCritical (実際の注釈は SecuritySafeCritical) が作成されました。透過性はアセンブリ内およびアセンブリ間に適用されるようになったため、Critical コードは、パブリックかどうかに関係なく、Transparent コードから呼び出すことができません。
この変更によって、Critical コードを Transparent コードに公開する方法に関する混乱が明確になります。第 1 に、すべての Critical コードが制限され、パブリック コードに対する例外がなくなったため、Critical 注釈の追加は常に、Transparent コードからの呼び出しができないことを意味します。さらに、TreatAsSafe は、Critical 属性が既に存在する場合にのみ意味を持ちます。何も実行しない Transparent メソッドであっても TreatAsSafe を設定することが可能なため、2 つの属性を要求すると混乱が発生する可能性があります。最後に、安全を指定する方法は 1 つだけなので、Critical コードはモデルをさらに簡素化します。
この新しい属性に関して理解する必要のあることの 1 つは、Critical コードについて話すとき、通常は SafeCritical コードが含まれるということです。これは、SafeCritical コードは Critical コードであるためです。SafeCritical コードは Critical コードと同じ機能ですが、Transparent コードから呼び出すこともできます。したがって、本当に安全かつ Critical にするためには、ライブラリ開発者は、(安全でない) Critical コードが呼び出される前に適切な入力検証および他のセキュリティ検査を行い、後から必要な出力検証および副作用の検査を行う必要があります。SafeCritical は、メソッドが Critical コードにアクセスする前後にすべてのチェックが適切に行われることを示すコントラクトになります。新しい透過性モデルでは、セキュリティ監査の焦点は SafeCritical コードです。

継承ルール
透過性モデルが有効であるためには、型またはメソッドのクロージャも安全であることを保証する必要がありました。基本型または基本メソッドの権限とは異なるリソース アクセス権限を持つ派生型または派生メソッドでは、アクセス保護の問題が発生しました。特に、アクセス可能な範囲の広い親の型またはインターフェイスにキャストされる場合はそうでした。たとえば、だれかが ToString などの Transparent 仮想メソッドの Critical オーバーライドを作成したとして、Object.ToString にキャストすることで Transparent コードがオーバーライドを呼び出すのを防ぐ必要がある場合を考えます。
透過性レベルに対するアクセスと能力の階層があり、Transparent から順に SafeCritical、Critical に移動するとアクセスはより厳しく制限されますが、機能は向上します (ここでのアクセスとは、このレベルのコードを包含したり、呼び出したりできる機能を指します)。型の継承のルールでは、派生型は少なくとも基本型と同程度に制限される必要があります。これにより、開発者が誤って Transparent コードで呼び出し可能な Critical 型のオーバーロードを作成することはなくなります。図 1 は、このパターンに従って許可される内容を示しています。
図 1 許可される型継承パターン
基本型 | 許可される派生型
Transparent | Transparent、SafeCritical、Critical
SafeCritical | SafeCritical、Critical
Critical | Critical
仮想メソッドのオーバーライド ルールは異なります。派生メソッドは、Transparent コードからの基本メソッドと同じアクセス可能性を持つ必要があります。図 2 は、このパターンに従って許可される内容を示しています。
図 2 許可されるメソッド継承パターン
基本 (仮想/インターフェイス) メソッド | 許可されるオーバーライド メソッド
Transparent | Transparent、SafeCritical
SafeCritical | Transparent、SafeCritical
Critical | Critical
Transparent 仮想メソッドの Critical オーバーライドは許可されていないことに注意してください。この場合は、基本メソッドにキャストし、呼び出すときにオーバーライドで Critical の検査をバイパスするだけです。Transparent 関数の SafeCritical オーバーライドは、キャストに意味がないため許可されています。Transparent コードはいずれにしても SafeCritical コードにアクセスできます。SafeCritical 仮想メソッドの Transparent オーバーライドも許可されています。これは一見すると驚かれるかもしれませんが、キャストによる危険はやはり発生しないため、実際には安全です。
Silverlight 透過性モデルをまとめると、すべてのコードは既定で Transparent になります。すべての Silverlight アプリケーションは、完全に Transparent です。それ以外を指定するすべてのマーキングは無視されます。さらに、固定されたアクセス許可セットによって、透過性は CoreCLR に必要な唯一の適用されるメカニズムになります。図 3 は、3 種類のコードとその機能および特徴をまとめたものです。
図 3 Transparent、SafeCritical、Critical の各コードの比較
 | Transparent | SafeCritical | Critical
アクセス制限 | Critical コードを直接呼び出すことはできません。 | Critical コードで可能なすべての操作を行うことができます。 Critical コードを呼び出す前後に適切な検査を実行する必要があります。 | 特権を持ち、任意のコードを呼び出すことができます。完全な信頼のみと同等です。 Transparent コードから直接呼び出すことはできませんが、SafeCritical コードを通して公開できます。
安全でないコード | 安全でないコードまたは検証できないコードを含むことはできません。 | 安全でないコードまたは検証できないコードを含むことができます。 | 安全でないコードまたは検証できないコードを含むことができます。
アプリケーションまたはプラットフォームのコードに対する使用可能性 | アプリケーションおよびプラットフォームのどちらのコードにも存在します。 | Silverlight プラットフォーム アセンブリでのみ使用できます。 | Silverlight プラットフォーム アセンブリでのみ使用できます。
型継承の特徴 | 型は、他の Transparent 型から派生する必要があります。 | 型は、Transparent 型または SafeCritical 型から派生する必要があります。 | 型は、注釈に関係なくすべての型から継承できます。
メソッドのオーバーライドの特徴 | メソッドは、Transparent または SafeCritical の仮想/インターフェイス メソッドをオーバーライドする必要があります。 | メソッドは、Transparent または SafeCritical の仮想/インターフェイス メソッドから派生する必要があります。 | メソッドは、Critical の仮想/インターフェイス メソッドから派生する必要があります。

Silverlight 透過性への公開
Silverlight 透過性モデルの多くの利点は、Silverlight プラットフォームでセキュリティを簡単に適用できることによりもたらされます。Silverlight は閉じたプラットフォームであるため、現時点では信頼されたサードパーティ製ライブラリの概念はありません。したがって、開発者が Silverlight 透過性モデルと対話する際は、アプリケーションが完全に Transparent であるため、呼び出しが許可されている API (Transparent または SafeCritical) に限定されます。一方、.NET Framework では、プラットフォームは閉じていないため、簡単なセキュリティの適用の恩恵を受けられる追加のライブラリを開発することができます。
ただし、これらの利点はデスクトップ フレームワークの次のメジャー リリースで利用できるようになります。.NET Framework 2.0 で導入された透過性モデルは、APTCA ライブラリのセキュリティの分析および判定の作業を大幅に簡素化しました。Silverlight 透過性モデルはさらに発展し、以前は面倒なレビューによって確認する必要があった想定と保証が可能になりました。今後は、完全な .NET Framework に向けて、Transparent コードの厳密なルール、SafeCritical 属性、および透過性継承ルールの導入を予定しています。また、外部の開発者からの利用を可能にすることも計画しています。
デスクトップ フレームワークと Silverlight で 1 つ大きく異なる点は、デスクトップ フレームワーク上のアプリケーションのほとんどが完全に信頼された環境用に作成されているのに対し、Silverlight のすべてのアプリケーションは部分的に信頼された環境用に作成されている点です。ほとんどのデスクトップ アプリケーションは必要なあらゆる操作を実行し、それを続けていくことができるため、透過性について考慮する必要はまったくありません。実装に関して、これらのアプリケーションは Critical であると考えられます。
APTCA ライブラリの開発者は、Silverlight プラットフォーム アセンブリの多くの利点を利用できます。部分的に信頼された Transparent コードからは Critical 層にアクセスできないため、APTCA ライブラリの監査可能な領域は、現時点では SafeCritical 層が対象となります。さらに、継承ルールの導入により、ライブラリのオーバーライドと派生クラスが意図せずにセキュリティ ホールを公開することはありません。ただし、デスクトップ フレームワーク上ではさまざまなサンドボックスが可能であるため、APTCA ライブラリは個別のアクセス許可を処理する必要がある、ということに留意してください。したがって、SafeCritical API は、アクセス許可の要求と、他の入力検証チェックを実行する必要があります。
また、より厳密な Transparent コード ルールにより、部分的に信頼されたアプリケーションの安全性とパフォーマンスが以前に比べ向上します。.NET Framework 2.0 の透過性モデルでは、安全でないコードまたは検証できないコードを呼び出したり包含したりする Transparent コードで UnmanagedCodePermission の要求の注入が発生し、安全でないコードまたは検証できないコードを呼び出したときにコストの高いスタック ウォークが発生しました。ほとんどのサンドボックスに付与されたアクセス許可セットにはこのアクセス許可が含まれないため、通常これは失敗に終わります。
新しい透過性モデルでは、CoreCLR では要求の注入は発生せず、スタック ウォークも発生しないため、このような失敗は難しくなります。新しい透過性モデルを次期バージョンのデスクトップ フレームワークに導入することで、APTCA ライブラリの開発、および部分的に信頼されたアプリケーションの開発が容易になることが期待されます。

ご意見やご質問は clrinout@microsoft.com までお送りください。

Andrew Dai は、マイクロソフトで CLR のプログラム マネージャを務めています。彼は、Silverlight とデスクトップ フレームワークのセキュリティに携わっています。


基本的な本能
XML リテラルを使用した動的なデータ入力
Beth Massi
目次
XAML の動的な読み込み
メタデータの取得
メタデータに基づいた UI の生成
動的なデータ入力
データ駆動型アプリケーションのあらゆる管理画面を自動的に生成できればすばらしいと思いませんか。ここで言う "管理" の対象は、シンプルなルックアップ データ テーブルや連絡先テーブルです。それらの画面の作成をチームの新人開発者に任せる代わりに、ご自分のオブジェクト モデルやデータベース スキーマに基づいて、実行時に生成できるようにしてみてはいかがでしょうか。XAML を使用する Windows Presentation Foundation (WPF) では、これは比較的容易な作業です。また、Visual Basic の幅広い XML のサポートがあれば、ユーザー インターフェイスを動的に生成することなど実に簡単に行えます。

XAML の動的な読み込み
Visual Basic の XML リテラルおよび XML 名前空間インポートを使用すると、難なく WPF ユーザー インターフェイスを実行時に動的に生成できます。WPF ではそのユーザー インターフェイスの要素とレイアウトを XAML で表現できるうえに、Visual Basic が複雑な処理をすべて肩代わりし、適切な XAML を作成するために XML 名前空間を解決してくれるためです。
具体的に説明しましょう。System.Windows.Markup 名前空間の XamlReader クラスと XamlWriter クラスを使用することで、XAML の読み込みと保存を実行時に行うことができます。新しい WPF プロジェクトを作成し、Window1 分離コードで次のように記述してください。
Imports <xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
Imports <xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Imports System.Windows.Markup

Class Window1
  Private Sub Window1_Loaded() Handles MyBase.Loaded
    Dim UI = <Label Name="Label1">This is COOL!</Label>
    Me.Content = XamlReader.Load(UI.CreateReader())
  End Sub
End Class
図 1 に示すように、ウィンドウのコンテンツがラベルに表示されます。
図 1 実行時に XAML を読み込む XAMLReader
コード ファイルの上部に WPF 名前空間インポートを含めた場合は、XAML を実行時に読み込むために必要な XAML が Visual Basic によって自動的に作成されます。この例ではコントロールの既定の名前空間を使用していますが、名前空間プレフィックスを指定し、XAML を変更してそれを使用することもできます。その結果、コードで操作する他の XML 用に既定の名前空間を残しておくことができます。
Imports <xmlns:controls=  "http://schemas.microsoft.com/winfx/2006/xaml/presentation">
Imports <xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Imports System.Windows.Markup

Class Window1
  Private Sub Window1_Loaded() Handles MyBase.Loaded
    Dim UI = <controls:Label Name="Label1">      This is COOL!      </controls:Label>
    Me.Content = XamlReader.Load(UI.CreateReader())
  End Sub
End Class
ここで注意しなければならないのは、動的に読み込まれるコントロールのイベント ハンドラを XAML では直接指定できないという点です。実行時に読み込む XAML ではコンパイルを要求できないためです。ただし、実行時に分離コードに追加することはできます。たとえば、次の処理を動的に行うことはできません。"イベントを指定する XAML ファイルをコンパイルする必要があります。" という XAML 解析例外が発生するためです。
Dim UI = <Label
    MouseDown="Label1_MouseDown"
    Name="Label1">
    This is COOL!
  </Label>
Me.Content = XamlReader.Load(UI.CreateReader())
代わりに、実行時にラベルへの参照を取得し、コードにイベント ハンドラを明示的に追加できます。
Private Sub Window1_Loaded() Handles Window1.Loaded
  Dim UI = <Label Name="Label1">This is COOL!</Label>
  Dim myLabel As Label = XamlReader.Load(UI.CreateReader())
  Me.Content = myLabel
  AddHandler myLabel.MouseDown, AddressOf Label1_MouseDown
End Sub

Private Sub Label1_MouseDown()
  MsgBox("Mouse Down!")
End Sub
このように、さまざまな手段があります。もちろん、UI の動的な生成は WPF に限ったものではありません。Windows フォームでも同様に行うことができます。ただし、手動によるレイアウトのコーディングの練習のような作業になります。WPF と Visual Basic を使用すると、この処理が非常に簡単になります。ごくわずかな XAML を定義し、単一の LINQ クエリでそれを構築できるためです。

メタデータの取得
UI の XAML を構築するには、いくつかのメタデータを取得しなければなりません。テーブルのスキーマに基づいて UI の外観を構築する処理を記述するための簡単な方法が必要です。そこで、リフレクションを使用してオブジェクト モデルを参照することができます。ただし、そのモデルが非常にシンプルで、データベース テーブルに 1 対 1 でマッピングされるのであれば、データベースへの問い合わせを行って、そのテーブルに関するメタデータを取得することができます。
シンプルな UI を生成するために、少なくともテーブルの各列のプロパティ (ColumnName、DataType、MaxLength、および IsPrimaryKey) を取得しなければならない場合があります。これを行うための 1 つの方法は、情報スキーマ ビューを使用して特定のテーブルの基本的なスキーマを返す、GetTableSchema というストアド プロシージャ (この例では、Northwind データベースを使用しています) を作成することです (図 2 を参照)。
図 2 GetTableSchema
CREATE PROCEDURE dbo.GetTableSchema
  (
  @table varchar(50)
  )
AS
SELECT
c.table_name As TableName,
c.column_name As ColumnName,
c.data_type As DataType,
c.character_maximum_length As MaxLength,
  COALESCE (
  ( SELECT
    CASE cu.column_name
      WHEN null THEN 0
      ELSE 1
    END
  FROM information_schema.constraint_column_usage cu
  INNER join information_schema.table_constraints ct
  ON ct.constraint_name = cu.constraint_name
  WHERE
  ct.constraint_type = 'PRIMARY KEY'
  AND ct.table_name = c.table_name
  AND cu.column_name = c.column_name
  ),0) AS IsPrimaryKey
FROM information_schema.columns c
INNER JOIN information_schema.tables t
ON c.table_name = t.table_name
WHERE @table = t.table_name and
  (t.table_type = 'BASE TABLE' and not
  (t.table_name = 'dtproperties') and not
  (t.table_name = 'sysdiagrams'))
ORDER BY c.table_name, c.ordinal_position
このストアド プロシージャには、決められた簡単な方法でアクセスできます。具体的には、新しい LINQ to SQL クラスのアイテムをプロジェクトに追加し、サーバー エクスプローラでデータベースの [ストアド プロシージャ] ノードを展開し、上記のプロシージャをメソッド ペインにドラッグします。次に、ストアド プロシージャから返されるフィールドと同じプロパティを格納する TableSchema というオブジェクトをデザイン サーフェイスで作成する必要があります。この例で編集することにしている Northwind 顧客テーブルもモデルに追加できます。設定を終えたら、GetTableSchema メソッドのプロパティで、結果の型を TableSchema クラスに設定します。

メタデータに基づいた UI の生成
ここまでの手順で、メタデータを取得できるようになりました。実行時に XAML を生成するためのクエリを記述する準備はほぼ整っています。ContentControl を使用すると、ウィンドウのコンテンツ全体を生成することも、その一部だけを生成することもできます。そのため、ウィンドウの静的要素を簡単にレイアウトしたり、動的コンテンツの配置先を指定したりできます。
この例では、フォームをレイアウトするためのグリッド パネルを使用して、シンプルなテキスト ボックス、[Find] (検索) ボタン、および [Save] (保存) ボタンをウィンドウ上部の固定領域に作成します。ウィンドウのその下の領域には ContentControl を配置し、DynamicContent などの名前を付けます (図 3 を参照)。この場合の XAML は図 3 のようになります。
図 3 ウィンドウの定義
<Window x:Class="Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1"  Name="Window1" SizeToContent="WidthAndHeight" >
  <Grid Name="MainGrid"  >
    <Grid.RowDefinitions>
      <RowDefinition Height="10*" />
      <RowDefinition Height="60*" />
    </Grid.RowDefinitions>
    <StackPanel Name="StackPanel1" Orientation="Horizontal"
      Margin="3" VerticalAlignment="Top">
      <Label Height="28" Name="Label1" Width="84"
        HorizontalContentAlignment="Right" FontWeight="Bold">ID</Label>
      <TextBox Height="25" Name="txtSearch" Width="120"></TextBox>
      <Button Height="25" Name="btnFind" Width="75">Find</Button>
      <Button Height="25" Name="btnSave" Width="75">Save</Button>
    </StackPanel>
    <ContentControl Grid.Row="1" Name="DynamicContent" Margin="3" />
  </Grid>
</Window>
分離コード ファイルの先頭に、適切な XML 名前空間インポートを追加してください。その後、Window Loaded イベント ハンドラに LINQ クエリを記述することで、UI を生成できます。実際に顧客データの読み込みと保存を行えるように、ボタン クリック ハンドラを追加することもできます。
図 4 に、Window クラスのコード全体を示します。このコードにはいくつかの LINQ クエリ式が埋め込まれていることに注意してください (図 4 を参照)。このクエリ式により、コントロールの XAML を XElement として返す GetUIElement というヘルパ関数を使用して、フィールド名、コントロール、およびデータ バインドのラベルを作成できます。
図 4 UI の動的な生成
Imports <xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
Imports <xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Imports System.Windows.Markup

Class Window1
  Dim db As New NorthwindDataContext
  Dim CustomerData As Customer

  Private Sub Window1_Loaded() Handles MyBase.Loaded
    Dim customerSchema = db.GetTableSchema("Customers").ToList()

    Dim UI = _
      <Grid xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns=          "http://schemas.microsoft.com/winfx/2006/xaml/presentation">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="100*"/>
          <ColumnDefinition Width="200*"/>
        </Grid.ColumnDefinitions>
        <StackPanel Name="StackLabels" Margin="3">
          <%= From column In customerSchema _
            Where column.IsPrimaryKey = 0 _
            Select <Label
              Height="28"
              Name=<%= column.ColumnName & "Label" %>
              HorizontalContentAlignment="Right">
            <%= column.ColumnName %>:</Label> %>
          </StackPanel>
          <StackPanel Grid.Column="1" Name="StackFields" Margin="3">
            <%= From column In customerSchema _
              Where column.IsPrimaryKey = 0 _
              Select GetUIElement(column) %>
          </StackPanel>
      </Grid>

    Me.DynamicContent.Content = XamlReader.Load(UI.CreateReader)

  End Sub

  Private Function GetUIElement(ByVal column As TableSchema) As XElement
    Select Case column.DataType
      Case "datetime", "int", "smallint", "money"
        Return <TextBox
          Height="28"
          Name=<%= "txt" & column.ColumnName %>
          Text=<%= "{Binding Path=" & column.ColumnName & "}" %>/>
      Case "bit"
        Return <CheckBox
          HorizontalContentAlignment="Left"
          Name=<%= "chk" & column.ColumnName %>
          IsChecked=<%= "{Binding Path=" & column.ColumnName & "}" %>>
          <%= column.ColumnName %>
          </CheckBox>
      Case "image"
        Return <Image
           Height="150"
           Width="150"
           Stretch="Fill"
           HorizontalAlignment="Left"
           Name=<%= "img" & columnInfo.ColumnName %>
           Source=<%= "{Binding Path=" & columnInfo.ColumnName & "}" %>/>
      Case Else
        Return <TextBox
          Height="28"
          Name=<%= "txt" & column.ColumnName %>
          MaxLength=<%= column.MaxLength %>
          Text=<%= "{Binding Path=" & column.ColumnName & "}" %>/>
    End Select
  End Function

  Private Sub btnFind_Click() Handles btnFind.Click
    If Me.txtSearch.Text <> "" Then
      Me.CustomerData = (From cust In db.Customers _
        Where cust.CustomerID = Me.txtSearch.Text).FirstOrDefault()

      Me.DataContext = Me.CustomerData
    Else
      Me.DataContext = Nothing
    End If
  End Sub

  Private Sub btnSave_Click() Handles btnSave.Click
    If Me.DataContext IsNot Nothing Then
      Try
        db.SubmitChanges()
        MsgBox("Saved")

      Catch ex As Exception
        MsgBox(ex.ToString)
      End Try
    End If
  End Sub
End Class
この時点でアプリケーションを実行すると、UI にはすべての顧客フィールドが表示されます (図 5 を参照)。GetTableSchema ストアド プロシージャは、データベースに格納されているあらゆるテーブルに対して使用できるので、さらに抽象化することも可能だということを覚えておいてください。たとえば、スキーマを問わず、データベースに含まれている任意の管理テーブルの編集が可能な単一のウィンドウが必要な場合は、どうすればよいでしょうか。
図 5 UI の顧客フィールド

動的なデータ入力
前の例のフォームは、データの読み込みと編集のために、依然として Customer のオブジェクト モデルに関連付けられています。UI は動的に生成できますが、データ入力はそうはいきません。皆さんが求めているのは、どの管理テーブルのデータベース スキーマを変更した場合にも、オブジェクト モデルを更新したり、すべてのコードを再コンパイルしたりする必要のない、より抽象化されたフォームではないでしょうか。
これを実現する方法の 1 つに、型指定のないシンプルな DataTable の読み込みと編集を実行時に行う方法があります。この場合も、XML リテラルを使用するとうまくいきます。ここでは、SqlDataAdapter の SELECT ステートメントと UPDATE ステートメントを生成します。ここで注意すべきなのは、この手法では検証ルールを適用するためにデータベースを利用するという点です。そのため、私は非常にシンプルなテーブル (管理テーブルやルックアップ テーブルなど) を編集する場合にのみ、このような動的フォームを使用するようにしています。とはいえ、WPF Binding.ValidationRules プロパティをいろいろと操作して、検証ルールをコントロールに宣言で追加してみることをお勧めします。
TableSchema メタデータに対応する型指定された DataTable も同様に作成できるため、プロジェクトに LINQ to SQL クラスが必要なくなります。これを作成するには、プロジェクトを右クリックし、新しいアイテムを追加し、[データセット] を選択します。そのアイテムに TableSchemaDataSet という名前を付け、GetTableSchema ストアド プロシージャをサーバー エクスプローラからデザイン サーフェイスにドラッグします。この操作により、型指定された DataTable を手間をかけずに自動的に作成できます。DataTable の名前を TableSchema に変更し、保存してください。
さて、データを編集するには、動的に生成された UI に型指定のない DataTable を読み込む必要があります。何らかの主キーがある場合を除き、編集するテーブルのスキーマについて関知する必要はありません。WPF では DataTable と DataSet をうまく処理できますが、これらは実行時に読み込まれるので、手動で設定しなければならないものが何点かあります。
その 1 つが、TableName というパブリック プロパティです。編集対象のテーブルの名前を表示するために、このプロパティをフォームに設定する必要があります。このプロパティは、コードの呼び出しによって簡単に設定できます。さらに、必要な ADO.NET オブジェクトを参照するために、いくつかのクラス レベルのプライベート変数を作成してください。この例のウィンドウの XAML マークアップは、以前のものとまったく変わりません。図 6 に、必要なクラス レベルの変数と、Shippers の既定値に設定されている TableName プロパティを示します。
図 6 動的なデータ入力のための変数とプロパティの設定
Imports <xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
Imports <xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Imports System.Windows.Markup
Imports System.Data.SqlClient
Imports System.Data

Partial Public Class Window2
  'This is the metadata table we created in the DataSet Designer
  Private TableSchema As New TableSchemaDataSet.TableSchemaDataTable
  'ADO.NET objects used to load and save the table we're editing
  Private TableDataAdapter As New SqlDataAdapter
  Private TableConnection As New _
          SqlConnection(My.Settings.NorthwindConnectionString)
  Private Table As DataTable
  'This is the key field used in searching for a row in this example
  Private PKField As TableSchemaDataSet.TableSchemaRow

  'This property can be set before the Form.Show() to edit any table
  Private m_tableName As String = "Shippers"
  Public Property TableName() As String
    Get
      Return m_tableName
    End Get
    Set(ByVal value As String)
      m_tableName = value
    End Set
  End Property
  End Class
これで、Loaded イベント ハンドラを通じてメタデータを読み込み、XAML の作成と読み込みを行って前の例のような UI を表示し、TableDataAdapter の UpdateCommand を設定できるようになりました。図 7 に、Loaded イベント ハンドラと、XAML を生成するコードを示します。
図 7 動的なデータ入力に対応した UI の生成
Private Sub Window1_Loaded() Handles MyBase.Loaded
  Try
    'Get the schema of the database table we want to edit
    Dim taSchema As New _      TableSchemaDataSetTableAdapters. TableSchemaTableAdapter
    taSchema.Fill(Me.TableSchema, Me.TableName)

    'Create the DataTable that will hold the record we're editing
    Me.Table = New DataTable(Me.TableName)
    Me.Title = Me.TableName
    Me.LoadUI()
    Me.SetPrimaryKey()
    Me.SetUpdateCommand()

  Catch ex As Exception
    MsgBox(ex.ToString)
    Me.Close()
  End Try
End Sub

Private Sub LoadUI() _
  Dim UI = _
    <Grid xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      Name="Grid1">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100*"/>
        <ColumnDefinition Width="200*"/>
      </Grid.ColumnDefinitions>
      <StackPanel Name="StackLabels" Margin="3">
        <%= From column In Me.TableSchema _
          Where column.IsPrimaryKey = 0 AndAlso _              column.DataType <> "timestamp" _          Select <Label
            Height="28"
            Name=<%= column.ColumnName & "Label" %>
            HorizontalContentAlignment="Right">
          <%= column.ColumnName %>:</Label> %>
        </StackPanel>
        <StackPanel Grid.Column="1" Name="StackFields" Margin="3">
          <%= From column In Me.TableSchema _
            Where column.IsPrimaryKey = 0 AndAlso _               column.DataType <> "timestamp" _
            Select GetUIElement(column) %>
        </StackPanel>
      </Grid>

  Me.DynamicContent.Content = XamlReader.Load(UI.CreateReader())
End Sub

Private Function GetUIElement(ByVal columnInfo As _  TableSchemaDataSet.TableSchemaRow) As XElement
  Select Case columnInfo.DataType.ToLower
    Case "datetime", "int", "smallint", "money"
      Return <TextBox
        Height="28"
        Name=<%= "txt" & columnInfo.ColumnName %>
        Text=<%= "{Binding Path=" & columnInfo.ColumnName & "}" %>/>
    Case "bit"
      Return <CheckBox
        HorizontalContentAlignment="Left"
        Name=<%= "chk" & columnInfo.ColumnName %>
        IsChecked=<%= "{Binding Path=" & columnInfo.ColumnName & "}" %>>
        <%= columnInfo.ColumnName %>
        </CheckBox>
    Case "image"
      Return <Image
         Height="150"
         Width="150"
         Stretch="Fill"
         HorizontalAlignment="Left"
         Name=<%= "img" & columnInfo.ColumnName %>
         Source=<%= "{Binding Path=" & columnInfo.ColumnName & "}" %>/>
    Case Else
      Return <TextBox
        Height="28"
        Name=<%= "txt" & columnInfo.ColumnName %>
        MaxLength=<%= columnInfo.MaxLength %>
        Text=<%= "{Binding Path=" & columnInfo.ColumnName & "}" %>/>
  End Select
End Function
これで、XAML UI は生成できました。次に、主キーのフィールド (これは TableSchemaDataRow オブジェクトです) を設定し、ユーザーがフォームの [Find] (検索) ボタンをクリックした際の SELECT クエリや UPDATE ステートメントで主キーを使用できるようにします。一般に、主キーは代理キー (自動的に増分する整数など) であり、ユーザーにとって特に意味はないので、検索フィールド名をキャプチャする別のパブリック プロパティを作成してもかまいません。
すべてのテーブルに 1 つの主キー フィールドしか定義されていない場合は、クエリでインデクサ 0 を使用できます。この場合、シーケンスの最初の要素が返されます。
Private Sub SetPrimaryKey()
    'Grab the Primary Key column of the table we want to
    ' edit so we can use it in the search
    Me.PKField = (From column In Me.TableSchema _
                  Where column.IsPrimaryKey = 1)(0)
  End Sub
SELECT ステートメントを作成するために、もう一度 XML リテラルを使用します。ただし、ここでは XML 自体は作成しません。代わりに、XElement の .Value プロパティを呼び出すことで文字列を作成できます。その後で、SqlCommand を作成し、型指定のない DataTable に結果を挿入できます。ウィンドウの DataContext を設定すると、XAML の生成時に指定されたフィールドへのデータ バインドが設定されます。図 8 に、SELECT ステートメントを生成し、DataContext を結果に設定するコードを示します。
図 8 SELECT ステートメントの生成
Private Sub btnFind_Click() Handles btnFind.Click
  If Me.txtSearch.Text <> "" Then
    Try
      'Create the SELECT command
      Dim cmdText = <s>
        SELECT * FROM <%= Me.TableName %>
        WHERE <%= Me.PKField.ColumnName %> =
          <%= If(Me.PKField.DataType.Contains("char"), _
            "'" & Me.txtSearch.Text & "'", _
            Me.txtSearch.Text) %>
        </s>.Value

      Dim cmd As New SqlCommand(cmdText, Me.TableConnection)
      Me.Table.Clear()
      Me.TableDataAdapter.SelectCommand = cmd
      Me.TableDataAdapter.Fill(Me.Table)

      Me.DataContext = Me.Table
      Dim view = CollectionViewSource.GetDefaultView(Me.Table)
      view.MoveCurrentToFirst()

    Catch ex As Exception
      MsgBox(ex.ToString)
      Me.DataContext = Nothing
    End Try
  Else
    Me.DataContext = Nothing
  End If
End Sub
このレコードを編集および保存する機能も必要なので、UPDATE コマンドを生成する必要があります。この例のフォームではレコードの編集だけを行うため、コードでは SqlDataAdapter の UpdateCommand だけを設定していますが、Delete コマンドや Insert コマンドも同じ方法で簡単に作成できます。図 9 に、UPDATE ステートメントを作成し、UpdateCommand を設定するコードを示します。
図 9 UPDATE ステートメントの生成
Private Sub SetUpdateCommand()
  'Set the UpdateCommand so that we can save edited records in the table
  Dim cmdText = <s>
    UPDATE <%= Me.TableName %>
    SET <%= From column In Me.TableSchema _
      Where column.IsPrimaryKey = 0 AndAlso _        column.DataType <> "timestamp" _
      Select <c>
        <%= column.ColumnName %> = @<%= column.ColumnName %>
        <%= If(Me.TableSchema.Rows.IndexOf(column) < _
          Me.TableSchema.Rows.Count - 1, ", ", "") %>
        </c>.Value %>
    WHERE <%= Me.PKField.ColumnName %> = @<%= Me.PKField.ColumnName %>
      <%= From column In Me.TableSchema _
        Where column.IsPrimaryKey = 0 AndAlso _          column.DataType = "timestamp" _
        Select <c>
          AND <%= column.ColumnName %> = @<%= column.ColumnName %>
          </c>.Value %>
    </s>.Value

  Dim cmd As New SqlCommand(cmdText, Me.TableConnection)
  Dim p As SqlParameter

  For Each column In Me.TableSchema
    If column.IsPrimaryKey = 0 AndAlso column.DataType = "timestamp" Then
    'Note: It's recommended to use a TimeStamp column in your tables for     'concurrency checking
      p = New SqlParameter("@" & column.ColumnName, SqlDbType.Timestamp)
      p.SourceVersion = DataRowVersion.Original
      p.SourceColumn = column.ColumnName
      cmd.Parameters.Add(p)
    Else
      p = New SqlParameter("@" & column.ColumnName, _
        CType([Enum].Parse(GetType(SqlDbType), _          column.DataType, True), SqlDbType))
      p.SourceColumn = column.ColumnName
      p.SourceVersion = DataRowVersion.Current
      cmd.Parameters.Add(p)
    End If
  Next

  Me.TableDataAdapter.UpdateCommand = cmd
End Sub
ここで、次の点に注意してください。UpdateCommand を作成するコードでは、TimeStamp フィールド (私はこれを自分の Northwind に追加しました) を使用して同時実行チェックを行うことを想定していますが、元の値を現在の値と比較してチェックする長いバージョンを作成することもできます。
ここで、このフォームの TableName プロパティを設定できます。また、UI を動的に生成し、[Find] (検索) ボタンのクリック時にデータを読み込み、変更内容をデータベースに保存できるようになりました。データベース テーブルのスキーマを変更し、アプリケーションを再コンパイルせずに再度実行してみてください。うまくいくはずです。
この記事で説明した両方の例のプロジェクトをコード ギャラリーにアップロードしました。code.msdn.microsoft.com/dynamicWPF にアクセスしてみてください。

ご意見やご質問は instinct@microsoft.com まで英語でお送りください。

Beth Massi は、マイクロソフトの Visual Studio コミュニティ チームのオンライン コンテンツおよびコミュニティ プログラム マネージャです。彼女は、ビジネス アプリケーション開発者向けのコンテンツを発信し、MSDN デベロッパー センターにコミュニティの機能を導入する業務に携わっています。彼女のブログを blogs.msdn.com/bethmassi でご覧いただけます。


Cutting Edge
WPF および Silverlight 2 でコードを再利用する
Dino Esposito

コードのダウンロード : MSDN Code Gallery (604 KB)
オンラインでのコードの参照

このコラムは、Silverlight 2 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。

目次
WPF と Silverlight 2 の互換性
Visual State Manager の概要
コードの共有
WPF アプリケーションについての考え方
クロス プラットフォーム WPF コードの作成
マネージ コードの分析
重要なコードに対する Strategy の取得
例の説明
最後の考慮事項
Silverlight 2 では、XAML (Extensible Application Markup Language) を使用してユーザー インターフェイスを設計およびレンダリングします。同時に、組み込みコア CLR を利用してブラウザ内のマネージ コードを処理します。その結果、Web ベースの Silverlight 2 アプリケーションとデスクトップの WPF (Windows Presentation Foundation) アプリケーションにはよく似たところがあります。類似のプログラミング モデルを作成する目的の 1 つは、2 つの間でコードの再利用を容易にすることです。このコラムでは、Silverlight 2 と WPF でできる限り簡単にコードと XAML マークアップを共有するいくつかのパターンについて説明します。
Silverlight での CoreCLR
Silverlight 2 には CLR が組み込まれていますが、他の .NET アプリケーションおよびアセンブリで使用されるものと同じ CLR ではありません。Silverlight の CLR は CoreCLR と呼ばれ、異なる目標で設計されています。クロス プラットフォームの相互運用性のために設計された CoreCLR は、CLR と同時に動作でき、異なるセキュリティ モデルおよび異なるバージョンの基本クラス ライブラリをサポートします。詳細については、2008 年 8 月号のよくわかる「CLR 徹底解剖」コラムを参照してください (msdn.microsoft.com/magazine/cc721609)。
Silverlight と .NET アプリケーションでは背後の CLR が異なるということは、.NET アプリケーションと Silverlight アプリケーションを対象とする 2 つのプロジェクトで同じアセンブリを参照することはできません。最大の問題は mscorlib アセンブリに関するものです。Silverlight は、その目的からしてはるかに小さい機能のセットであるコア スタッフだけを必要とします。一方、.NET アセンブリは標準バージョンの mscorlib にリンクし、これによって問題が生じます。
このコラムで説明するサンプル アプリケーションには、Windows Presentation Foundation アプリケーションと Silverlight アプリケーションで共有する 1 つのインターフェイスがあります。共通に参照するアセンブリは使用できないので、唯一の回避策は、インターフェイス定義を含む C# を 2 つのプロジェクト間でコピーすることです。したがって、Silverlight アセンブリと .NET アセンブリの間のバイナリ互換性のための基礎とするため、.NET Framework のバージョンにおいて、標準 mscorlib アセンブリの機能をコア スタッフとデスクトップ スタッフの 2 つの部分に分割することをお勧めします。

WPF と Silverlight 2 の互換性
Silverlight 2 の導入により、XAML は新世代の UI モジュールの API となっています。Silverlight 2 は、リッチなレイアウト管理、データ バインド、スタイル、メディア、アニメーション、グラフィックス、テンプレートの機能など、完全な WPF フレームワークのサブセットをサポートします。
ただし、Silverlight 2 での XAML のフル サポートは、ダウンロード可能なプラグインのサイズによって制限されます。ほんの数メガバイト (Beta 2 では 5 MB 未満) で、Silverlight 2 プラグインは、コア CLR、WPF および WCF (Windows Communication Foundation) クライアント プラットフォームのサブセットを含む Microsoft .NET Framework 3.5 のバージョン、XAML パーサー、およびいくつかの Silverlight 固有コントロールを提供する必要があります。
Silverlight 2 では、WPF 3D グラフィックス機能はサポートされず、削除または縮小されている属性や要素があります。それでも、最後には、互換性のあるサブセットができあがるので、そこそこ複雑な WPF ユーザー インターフェイスの Silverlight バージョンを生成できます。互換性の重要な部分については、後でもう一度説明します。
ただし、Silverlight では新しい API がいくつか追加されており、現時点では WPF のデスクトップ バージョンに対応するものがない場合があります。最も関連のある機能は、DataGrid などのコントロールと、簡単な GET スタイルのネットワーク呼び出しのための WebClient などのクラスです。これらの機能も WPF に追加されます。

Visual State Manager の概要
Silverlight 2 に付属し、WPF に追加されるもう 1 つの機能は、コントロール用の非常に便利な VSM (Visual State Manager) です。VSM は、Visual State と状態遷移を導入することで対話形式コントロール テンプレートを簡単に開発できるようにします。WPF が導入されたことで、開発者は、テンプレートとスタイルを使用して、WPF のルック アンド フィールと動作をカスタマイズできます。たとえば、コントロールの形状や外観を変更できるだけでなく、コントロールがクリックされたときやフォーカスを得たり失ったりしたときの新しいアクションやアニメーションを定義することもできます。
VSM では、Visual State と状態遷移が作業の一部を自動的に行います。一般的な VSM の Visual State は、通常、マウス ポイント、無効、フォーカスありです。これらの状態のそれぞれについて、スタイルまたは形状を定義できます。状態遷移は、コントロールが状態間を移動する方法を定義します。通常は、アニメーションを使用して遷移を定義します。実行時に、Silverlight は適切なアニメーションを再生し、指定されたスタイルを適用して、コントロールを洗練された滑らかな動きである状態から別の状態に移動します。
Visual State を定義するには、次に示すようなマークアップをコントロール テンプレートに挿入します。
<vsm:VisualStateManager.VisualStateGroups>
  <vsm:VisualStateGroup x:Name="CommonStates">
    <vsm:VisualState x:Name="MouseOver">
      <Storyboard>
        ...
      </Storyboard>
    </vsm:VisualState>
  </vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
各コントロールは、CommonStates や FocusStates などの 1 つ以上の状態のグループを定義します。各グループでは、MouseOver、Pressed、Checked などの特定の Visual State が定義されています。各 Visual State および状態間の遷移に対し、Silverlight で必要に応じて自動的に再生されるストーリーボードを定義できます。
つまり、WPF の機能の中には Silverlight 2 がサポートしていないものがあり、Silverlight 2 の機能の中には WPF には存在しないものがあります。これらの違いは、主に XAML レベルでの互換性に影響を与えます。共通のサブセットを使用することで完全な互換性が得られ、さいわいこの共通部分はほとんど何でも行うことができる十分な大きさです。

コードの共有
WPF のデスクトップ バージョンで VSM が使用できるようになれば、Silverlight アプリケーションと Windows デスクトップ アプリケーションの両方で同じアプローチを使用し、WPF プロジェクトと Silverlight プロジェクトでコントロール テンプレートを共有できるようになります。それまでは、WPF デスクトップと Web プロジェクトでコードを再利用できる方法があります。
WPF アプリケーションは、XAML とマネージ コードで構成されます。マネージ コードの対象は、サポートされる .NET のバージョンのクラスです。ユーザーは、デスクトップ WPF と Silverlight 2 の両方で認識される XAML の共通サブセットを意識して使用する必要があります。同様に、バックエンド フレームワークでの違いが適切に処理されるように分離コード クラスを編成することも、ユーザーの役割です。
WPF コードの再利用には、基本的に 2 つのシナリオがあります。1 つは、既存の WPF デスクトップ アプリケーションがあり、Web でそれを提供して保守と展開を簡単にする場合です。もう 1 つは、既存システムのフロントエンドを開発し、それを Windows クライアントと Web クライアントの両方に提供する場合です。
ここでは、WPF から Silverlight へのコードの再利用という視点から説明します。コードとマークアップのリファクタリングのパターンに関しては、違いはほとんどありません。

WPF アプリケーションについての考え方
一般的な WPF アプリケーションは、オブジェクトのツリーから構築され、ツリーのルートは Window です。一方、Window 要素には、さまざまな方法でレイアウトまたはスタックされる子要素のメンバが含まれます。要素は、基本的な形状、レイアウト マネージャ、ストーリーボード、およびコントロール (カスタム サードパーティおよびユーザー コントロールを含みます) を示します。次に、基本的な例を示します。
<Window x:Class="Samples.MyTestWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Test App" Height="300" Width="350">
    <StackPanel Margin="10">
        ...
    </StackPanel>
</Window>
このコードをそのまま Silverlight 2 アプリケーションに組み込むことはできません。まず、Window 要素は Silverlight ではサポートされません。Silverlight アセンブリでは、System.Windows 名前空間にこのようなクラスはありません。すべての Silverlight アプリケーションのルート タグは UserControl です。要素は、System.Windows.Controls 名前空間で定義されている UserControl クラスにマップされます。
Silverlight アプリケーションの変更した先頭部分を次に示します。
<UserControl x:Class="Samples.MyTestWindow"
    xmlns="http://schemas.microsoft.com/client/2007"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Test App" Height="300" Width="350">
    <StackPanel Margin="10">
        ...
    </StackPanel>
</UserControl>
<Window> 要素と <UserControl> 要素の境界内のすべてのマークアップ コードは、一致している必要があります。元のデスクトップ XAML を変更して Silverlight XAML パーサーの要件に一致させるのはユーザーの責任です。この作業は、2 つの構文の間に高レベルの互換性があるおかげで、かなり楽になります。XAML の調整のほとんどは、若干の要素と属性の修正に関するものです。例を 2 つ示します。
デスクトップ WPF の <label> 要素は、Silverlight では認識されません。このマークアップを Silverlight に移植するときは、ラベルの回避策が必要です。内部にテキスト ブロックを含む四角形は、有効な解決策になる場合があります。
WPF では、次に示すように、ToolTip 属性を使用してツールヒントとコントロールを関連付けることができます。
<Button Tooltip="Click me" ... />
Silverlight 2 では、ToolTip 属性はサポートされていません。次のコードで示すように、ToolTipService を利用する必要があります。
<Button ToolTipService.ToolTip="Click me" ... />
デスクトップ WPF ではどちらの方法も動作することに注意してください。さらに、ToolTipService では、配置、オフセット、初期遅延、期間などのデスクトップ WPF の追加のツールヒント プロパティも提供されています。ただし、これらの追加プロパティはいずれも Silverlight ではサポートされていません。
WPF と Silverlight の間の互換性の問題はこれだけでしょうか。それは WPF の使用方法によって決まります。一般に、重要な WPF アプリケーションを Silverlight に移植することは難しく、実際には主要な目標でさえありません。まず、Silverlight 2 では、必要とされる場所でトリガを使用できません。たとえば、UI 要素には Triggers コレクション (FrameworkElement の子孫) がありますが、スタイル、データ、およびコントロール テンプレートにはありません。
同様に、Silverlight はデータ バインドをサポートしていますが、WPF とは方法が異なります。たとえば、Binding 要素や、データ コンテキスト、データ テンプレート、識別可能なコレクションなどがあります。既に説明したように、トリガや簡略化された XAML マークアップは使用できず、WPF よりも頻繁にコードを作成する必要があります。さらに、内部の実装が大幅に異なります。Silverlight の Binding オブジェクトには、WPF よりはるかに少ないプロパティしかありません。
グローバリゼーションはもう 1 つの頭痛の種です。パフォーマンスのため、コア CLR にはサポートされるすべてのカルチャについて独自のグローバリゼーション データが含まれません。代わりに、Silverlight の CultureInfo クラスは、基になっているオペレーティング システムで提供されるグローバリゼーション機能に依存します。つまり、異なるオペレーティング システムで同じグローバリゼーション設定をアプリケーションに提供する方法はありません。
最後に、WPF のリッチなコントロールのセットの中には、Silverlight では使用できないものがあります。その良い例が RichTextBox コントロールです。
まとめると、Silverlight アプリケーションを WPF に移植することは比較的簡単です。ただし、よりリッチなオブジェクト モデルによって発生する可能性のあるパフォーマンスの問題には注意する必要があります。留意する必要のある重要なことは、Silverlight はデスクトップ WPF によって提供される可能性のサブセットをサポートしているということです。適切な機能を選択してクロスプラットフォームのソリューションを考案する作業はユーザーに委ねられます。

クロス プラットフォーム WPF コードの作成
WPF プロジェクトから Silverlight プロジェクトにコードを移植する最も簡単な方法は、ユーザー コントロールを使用することです。異なる XML 名前空間とマークアップの違いを別にすれば、ユーザー コントロールはデスクトップと Web ベースの WPF でマークアップとコードを共有する唯一の手段です。
WPF アプリケーションが、ユーザー コントロールを広範に使用するように、もともと編成されている場合、またはリファクタリングできる場合は、Silverlight へのコードの移植は、カット アンド ペーストというあまり魅力的でない他のオプションよりはるかに簡単です。
それでは、WPF プロジェクトと Silverlight プロジェクトでアセンブリを共有できるのでしょうか。Silverlight 2 では、アセンブリにパックするカスタム クラス ライブラリを作成できます。ただし、このようなライブラリは Silverlight バージョンの .NET Framework を対象としたものであり、異なるセキュリティ モデルを使用することを理解しておく必要があります (詳細については、今月号の「CLR 徹底解剖」を参照してください)。
デスクトップ WPF アプリケーションで使用しているすべてのアセンブリを Silverlight のクラス ライブラリとして再コンパイルして、正しいアセンブリを参照し、正しくクラスを呼び出すようにする必要があります。言うまでもありませんが、再コンパイルを行うことで、サポートされていないクラスの呼び出しを修正する追加作業が発生する可能性があります。
つまり、若干の作業により、WPF アプリケーションを取り出し、Silverlight を使用して Web を介して公開できます。また、これにより、クライアントに .NET Framework の完全なインストールを強いることなく、Windows 以外のさまざまなプラットフォームでコードを実際に公開することになります。図 1 は、簡単な WPF スタンドアロン デスクトップ アプリケーションを示しています。図 2 は、同じアプリケーションを、Silverlight を使用して Internet Explorer でホストした場合です。
図 1 サンプル WPF アプリケーション
図 2 Silverlight 用に適合された WPF アプリケーション (クリックすると拡大画像が表示されます)

マネージ コードの分析
何らかの理由で、WPF と Silverlight の間のコードを再利用すると、XAML の問題が発生します。説明したように、コードを適用するときは XAML の問題がいくつかありますが、最大の課題は分離コード クラスです。
Windows および Silverlight のどちらも、XAML ファイルは、C# または別のマネージ言語で記述された分離コード クラスとペアになっています。残念ながら、これらの分離コード クラスは異なるバージョンの .NET Framework が対象です。デスクトップ バージョンの WPF は .NET Framework 3.5 の完全な基本クラス ライブラリ (BCL) に依存しますが、Silverlight 2 は軽量バージョンの BCL を使用します。
Silverlight バージョンの BCL は非常に小型ですが、それでも、コレクション、リフレクション、正規表現、文字列操作、スレッド、タイマなどの基本機能をサポートします。また、XML Web サービス、WCF サービス、ADO.NET Data サービスなどのさまざまなサービスを呼び出すツールを備えています。さらに、Plain Old XML (POX) サービスおよび Representational State Transfer (REST) サービスを使用して HTTP 経由で通信するためのリッチなネットワーク サポートを備え、通常は、任意のパブリック HTTP エンドポイントに到達できます。ネットワーク サポートには、(クロスドメイン) ソケットおよび二重通信も含まれます。
最後に、Silverlight BCL には、アドホック バージョンの XmlReader および XmlWriter クラスなど、XML データを操作するための便利なサポートもあります。これらのクラスは、デスクトップ バージョンの .NET Framework の類似クラスと非常によく似ています。
Silverlight 2 でこれらのコア機能を基にすると、LINQ to Objects、LINQ to XML、および式ツリーが完全にサポートされます。Beta 2 からは、JavaScript Object Notation (JSON) データに対して LINQ クエリを直接実行する新しい LINQ to JSON プロバイダも追加されました。
もう 1 つ指摘しておく必要があるのは、Silverlight でのネットワークは非同期でのみ実行できるということです。同期呼び出しを行うには、可能な場合は常に、ブラウザの相互運用層を呼び出し、ブラウザの XMLHttpRequest の実装にアクセスする方法を使用する必要があります (詳細については、go.microsoft.com/fwlink/?LinkId=124048 を参照してください)。
重要なのは、WPF と Silverlight の分離コード クラスが大きく異なるクラス ライブラリを利用していることです。この事実は、XAML の違いよりも、はるかに大きく再利用性を妨げます。次に、この問題に取り組みます。

重要なコードに対する Strategy の取得
Silverlight と Windows のランタイムが共有するコードを作成するときは、各プラットフォームがサポートするものをよく理解しておく必要があります。できれば、各プラットフォームでの特別な処理を必要とする要求されたタスクの一覧を作成します。次に、オブジェクト内のこれらのコードを分離し、インターフェイスを抽出します。
結果として、重要な機能について異なるコンポーネントを呼び出す完全に再利用可能なコードのブロックができあがります。このようなアプリケーションを Silverlight (または WPF) に移植することは、重要なコンポーネントをプラットフォーム固有の他のコンポーネントに置き換えるのと同じくらい簡単です。これらのコンポーネントはすべて共通のインターフェイスを公開するので、その実装はコードのメイン ブロックに対して透過的です。
この方法の背後にあるのは "Strategy" パターンです。その正式な定義については、go.microsoft.com/fwlink/?LinkId=124047 を参照してください。つまり、あるアプリケーションで特定のタスクを実行するために使用するアルゴリズムを動的に変更する必要がある場合は常に、Strategy パターンが役に立ちます。
実行時の条件によって変化する可能性のあるコードの部分を識別した後、コードの抽象的な動作を指定するインターフェイスを定義します。次に、そのインターフェイスを実装する戦略クラスを 1 つ以上作成し、それによって抽象的な動作を実現する異なる方法を表します。戦略クラスを変更することで、インターフェイスによって定義されているアルゴリズムを解決する方法を簡単に変更できます。このようにすると、動作の実際の実装 (戦略) が、それを使用するコードから分離されます。
たとえば、ASP.NET では、メンバシップ、ロール、ユーザー プロファイルなどのプロバイダ モデルの実装において、Strategy パターンが使用されています。ASP.NET ランタイムは、メンバシップやユーザーなどを処理するための特定のインターフェイスがあることを認識しています。また、これらのインターフェイスの種類に対する具象クラスを探してインスタンス化する方法についても認識しています。ただし、ランタイム コンポーネントはインターフェイスにのみ依存するので、具象クラスの詳細は ASP.NET とは関係なくなります。

例の説明
図 1 および 2 に示すサンプル アプリケーションについて考えます。Silverlight および Windows のどちらのアプリケーションも、ユーザーが銘柄記号を入力して現在の価格を取得できるフォームを表しています。図 3 に、ユーザーがボタンをクリックしたときのアプリケーションのダイアグラムを示します。ユーザー インターフェイスはモデル ビュー プレゼンタ (MVP) パターンを使用し、UI の背後にあるすべてのロジックが単一のプレゼンタ クラスで伝達されます。プレゼンタは内部の QuoteService クラスを呼び出し、このクラスは最終的に、ユーザー インターフェイスに組み込まれるすべての情報を含む StockInfo オブジェクトを提供することで応答します (図 4 を参照)。
図 3 アプリケーションの動作のダイアグラム (クリックすると拡大画像が表示されます)
図 4 プレゼンタ クラス
namespace Samples
{
    class SymbolFinderPresenter
    {
        // Internal reference to the view to update
        private ISymbolFinderView _view;

        public SymbolFinderPresenter(ISymbolFinderView view) {
            this._view = view;
        }

        public void Initialize() { }

        // Triggered by the user's clicking on button Find
        public void FindSymbol() {
            // Clear the view before operations start
            ClearView();

            // Get the symbol to retrieve
            string symbol = this._view.SymbolName;
            if (String.IsNullOrEmpty(symbol)) {
                _view.QuickInfoErrorMessage = "Symbol not found.";
                return;
            }

            QuoteService service = new QuoteService();
            StockInfo stock = service.GetQuote(symbol);

            // Update the view
            UpdateView(stock);
        }

         private void ClearView() {
            _view.SymbolDisplayName = String.Empty;
            _view.SymbolValue = String.Empty;
            _view.SymbolChange = String.Empty;
            _view.ServiceProviderName = String.Empty;
        }

        private void UpdateView(StockInfo stock) {
             // Update the view
            _view.QuickInfoErrorMessage = String.Empty;
            _view.SymbolDisplayName = stock.Symbol;
            _view.SymbolValue = stock.Quote;
            _view.SymbolChange = stock.Change;
            _view.ServiceProviderName = stock.ProviderName;
        }
     }
}
QuoteService クラス自身は、価格の取得を試みません。最初にプロバイダ コンポーネントを作成してから、それに制御を渡します。QuoteService クラスが実装しているアルゴリズムは非常に単純です。インターネット接続がある場合は、パブリック Web サービスを使用して金融データを取得するプロバイダ クラスを使用します。インターネット接続がない場合は、ランダムな数値を返すだけの偽のプロバイダに切り替えます。したがって、図 5 に示すように、QuoteService クラスはいずれかの時点でインターネット接続を検査する必要があります。
図 5 QuoteService でインターネット接続を検査する
public StockInfo GetQuote(string symbol)
{
    // Get the provider of the service
    IQuoteServiceProvider provider = ResolveProvider();
    return provider.GetQuote(symbol);
}
private IQuoteServiceProvider ResolveProvider()
{
    bool isOnline = IsConnectedToInternet();
    if (isOnline)
        return new FinanceInfoProvider();

    return new RandomProvider();
}
ここまでは、Silverlight と WPF に違いはなく、すべてのコードは完全に再利用できます。.NET でインターネット接続を検査するには、NetworkInterface オブジェクトの静的メソッドを使用できます。このオブジェクトは、System.Net.NetworkInformation 名前空間で定義されています。特に、GetIsNetworkAvailable メソッドはネットワーク接続が使用可能であるかどうかを示すブール値を返します。残念ながら、インターネット接続の詳細については示されません。インターネットへのアクセスが可能であることを確認する唯一の確実な方法は、ホストに ping を試みることです (図 6 を参照)。
図 6 インターネット接続に対する ping
private bool IsConnectedToInternet()
{
    string host = "...";
    bool result = false;
    Ping p = new Ping();
    try
    {
        PingReply reply = p.Send(host, 3000);
        if (reply.Status == IPStatus.Success)
            return true;
    }
    catch { }

    return result;
}
図 6 の唯一の問題は、このコードが Silverlight 2 ではサポートされないことです (同様に、NetworkInterface オブジェクトでもサポートされません)。このコード (および考えられる互換性の問題に対処する他のコード) を、置き換え可能なクラスに分離する必要があります (この制限の詳細については、このコラムに付属の「Silverlight での CoreCLR」リンクを参照してください)。付属のソース コードでは、この種の問題があると考えられるメソッドに対するユーティリティ インターフェイスを作成し、各プラットフォーム用のインターフェイスを実装する戦略クラスを作成しています。図 7 に、これを示します。
図 7 インターネット接続を検査するためのコンポーネント
public partial class SilverCompatLayer : ICompatLib
{
    public bool IsConnectedToInternet()
    {
        string host = "...";
        bool result = false;
        Ping p = new Ping();
        try
        {
            PingReply reply = p.Send(host, 3000);
            if (reply.Status == IPStatus.Success)
                return true;
        }
        catch { }

        return result;
    }

    public string GetRawQuoteInfo(string symbol)
    {
        string output = String.Empty;
        StreamReader reader = null;

        // Set the URL to invoke
        string url = String.Format(UrlBase, symbol);

        // Connect and get response
        WebRequest request = WebRequest.Create(url);
        WebResponse response = request.GetResponse();

        // Read the response
        using (reader = new StreamReader(response.GetResponseStream()))
        {
           output = reader.ReadToEnd();
           reader.Close();
        }

        return output;
    }

    // A few other methods that require a different implementation
    // (See source code)
    ...
}
このインターフェイスは、プラットフォーム固有の戦略クラスを、ホストするアプリケーションから分離します。その後、具象的な戦略クラスをインスタンス化するコードをファクトリ メソッドの背後に隠すことで、WPF と Silverlight の両方で次のコードを安全に使用できます。
private bool IsConnectedToInternet()
{
    ICompatLib layer = ServiceResolver.ResolveCompatLayer();
    return layer.IsConnectedToInternet();
}
SilverCompatLayer クラスは別のアセンブリにあります。WPF コードを Silverlight に移植する場合、またはその逆の移植を行う場合は、このアセンブリを変更するだけで済みます。
プラットフォーム固有の必要な戦略クラスを作成したら、後はアプリケーションの Silverlight バージョンを作成するだけです。元の WPF アプリケーションから導出される Silverlight プロジェクトには、互換性アセンブリを除くすべてのファイルの正確なコピーが含まれています。
この記事に付属のコード サンプルを見ると、ファクトリ メソッドで明示的に具象クラスをインスタンス化することで、使用する戦略の実装を判別していることがわかります。このようにクラスを直接インスタンス化することも、構成ファイルからクラス名を読み込み、リフレクションを使用してインスタンスを取得することもできます。これらは主に、構築しているシステムの種類に依存する実装の詳細です。WPF と Silverlight の互換性に関する限り、理解すべき重要な概念は、戦略と階層化です。

最後の考慮事項
サンプル アプリケーションでは、ネットワーク呼び出しを行っています。元の WPF アプリケーションでは、呼び出しは同期的です。一方、Silverlight 2 では、同期ネットワーク呼び出しはまったくサポートされていません。同期呼び出しを可能にするには、ブラウザの XMLHttpRequest の実装の呼び出しに基づいた前述の方法を使用します (詳細についてはソース コードを参照してください)。
サンプル コードでは、元の WPF アプリケーションを適切に Web に移植できました。コードを移植するときは、Silverlight 環境のネイティブ機能を考慮し、必要であればアプリケーションの構造を変更する場合があります。この単純な例では、Silverlight のプログラミング モデルを使用してアプリケーションを書き直す方が、WPF アプリケーションからの適用よりも少量のコードと作業で済むことに注意してください。
WPF と Silverlight には、XAML などの純粋に視覚的な部分について多くの共通項があります。しかし、基になるプログラミング モデルは若干異なり、WPF で動作するソリューションは必ずしも Silverlight にそのまま適用できるわけではなく、その逆もまた同様です。そうは言っても、WPF および Silverlight アプリケーションの間で XAML とコードを共有できることには間違いありません。

ご意見やご感想は、cutting@microsoft.com まで英語でお送りください。

Dino Esposito は、IDesign 社のアーキテクトであり、『Programming ASP.NET 3.5 Core Reference』の著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos で読むことができます。


実践的なパターン
凝集度と結合度
Jeremy Miller
目次
結合度を低くする
凝縮度を高くする
不適切な親密性を除去する
デメテルの法則
命じろ、たずねるな
伝えるのは 1 回限り
まとめ
ソフトウェア設計の多くでは、このコードはどこに置けばよいのかということを繰り返し考えます。筆者はいつでも、記述しやすく、理解しやすく、後で変更しやすくなるようにコードを編成する最善の方法を模索しています。コードをうまく構成できると、気分は最高です。構造がよくないと、後を任される開発者は筆者のことを一生恨むことでしょう。
コードの具体的な構造に関しては、特に次の 3 つのことを実現したいと考えています。
  1. 一緒に変更する必要があるものは、できる限りコード内で近くなるようにする。
  2. コード内で関係のないことは個別に変更できるようにする (直交性ともいいます)。
  3. コード内での重複を最小限にする。
これら 3 つの目標を達成するには、新しいコードを置く場所を知ることのできるツールと、コードを間違った場所に置いたことを認識できるツールが必要です。
一般に、これらの目標は伝統的なコード品質である凝集度と結合度に深く関係しています。筆者は、高い凝集度と緩い結合度に移行することで、これらの目標を達成します。もちろん、まず最初にこれらの品質が何を意味し、凝集度と結合度が便利な概念である理由を理解する必要があります。その後で、より優れた構造に向けた移行に役立つ "設計ベクトル" と呼ぶものについて少し説明し、コードに紛れ込んだ悪い構造から抜け出すのがどのようなときに必要なのかを見分けます。
参考までに、筆者は主として StructureMap という名前のオープン ソースの IOC (制御の反転) ツールを開発しています。このベクトルで対処する設計上の問題を説明するために、StructureMap から実際にあった例を使用します。つまり、筆者と同じ過ちを犯さないでください、ということです。

結合度を低くする
ソフトウェア設計について話をすると、"緩やかな結合" または "密結合" という言葉をよく耳にします。クラス間またはサブシステム間の結合は、これらのクラスまたはサブシステムがどの程度相互接続されているのかを示す尺度です。密結合とは、関連するクラスが相互の内部的な詳細を知る必要があり、変更はシステム内を伝わって広がり、システムを理解することが困難になる可能性があることを意味します。図 1 は、ビジネス処理モジュールが、ビジネス ロジック以外のさまざまな処理に密接に結合されている例です。
図 1 密接に結合されたコード
public class BusinessLogicClass {
  public void DoSomething()  {
    // Go get some configuration
    int threshold =
      int.Parse(ConfigurationManager.AppSettings["threshold"]);

    string connectionString =
      ConfigurationManager.AppSettings["connectionString"];

    string sql =
      @"select * from things
        size > ";

    sql += threshold;

    using (SqlConnection connection =
      new SqlConnection(connectionString)) {

      connection.Open();

      SqlCommand command = new SqlCommand(sql, connection);
      using (SqlDataReader reader = command.ExecuteReader()) {
        while (reader.Read()) {
          string name = reader["Name"].ToString();
          string destination = reader["destination"].ToString();

          // do some business logic in here
          doSomeBusinessLogic(name, destination, connection);
        }
      }
    }
  }
}
つまり、本当は実際のビジネス処理に対処すべきであるにもかかわらず、このビジネス ロジックのコードはデータ アクセス処理や構成の設定と絡まり合っています。このようなスタイルのコードでは何が間違っている可能性があるのでしょうか。
第 1 に、異なる関心事が絡み合っているため、コードを理解するのが困難になります。これについては、凝集度に関する次のセクションで詳しく説明します。
第 2 に、すべてが 1 つのコード ファイルに入っているため、データ アクセス戦略、データベース構造、構成戦略などの変更が、ビジネス ロジックのコードにも波及します。このようなビジネス ロジックは、基になっているインフラストラクチャについて知りすぎています。
第 3 に、特定のデータベース構造と切り離されたり、または AppSettings キーが存在しない状態では、ビジネス ロジック コードを再利用できません。また、BusinessLogicClass に埋め込まれているデータ アクセス機能も再利用できません。このようなデータ アクセスとビジネス ロジックの結合は問題にならない場合もありますが、アナリストが Excel スプレッドシートに直接入力したデータに対して、このビジネス ロジックを再利用する場合はどうでしょうか。ビジネス ロジックだけをテストまたはデバッグする場合はどうでしょうか。ビジネス ロジックがデータ アクセス コードと密接に結合されているため、これらはいずれも不可能です。ビジネス ロジックを他の関心事から分離できれば、ビジネス ロジックはずっと変更しやすくなることでしょう。
まとめると、クラスとモジュールの結合を緩くすることには次のような目的があります。
  1. コードを判読しやすくする。
  2. クラス内部の煩雑な処理を適切に定義された API の背後に隠すことで、他の開発者がクラスをより簡単に利用できるようにする。
  3. 変更の可能性のある部分をコード内の狭い範囲に分離する。
  4. まったく新しいコンテキストでクラスを再利用する。
コードのにおい
新しいコードを設計するときの正しい作法を知ることはもちろん良いことですが、既存のコードや設計が問題を起こしたことを認識することはそれ以上に重要な場合があります。不適切な親密性と同様に、『リファクタリング : プログラミングの体質改善テクニック』(ピアソンエデュケーション、2000 年) の中でマーチン・ファウラーが定義している "code smell (コードのにおい)" は、コード内の潜在的な問題を発見するために利用できるツールです。
コードのにおいは、コード内の何かが正しくない兆候です。すぐに既存のコードを取り去って捨てる必要があるということではなく、不快な "におい" を発しているコードを詳しく調べる必要があることを意味しています。コードのにおいは、簡単な検査で発見できます。コードのにおいについて知ることは、問題が大きくなる前にコードやクラスの設計に含まれる些細な問題を除去するための強力なツールです。
ほとんどではないにしても、よく取り上げられるコードのにおいの多くは、凝集度の不足または有害な密結合の表れです。次に他の例をいくつか示します。
発散的な変更 (Divergent Changes) さまざまな理由によりさまざまな方法で変更する必要のある 1 つのクラス。このにおいは、クラスが凝集していないことの兆候です。このようなクラスはリファクタリングして、異なる機能を新しいクラスに抽出します。
機能の嫉妬 (Feature Envy) ClassA のメソッドが、ClassB の処理やデータ フィールドに関心を持ちすぎているような場合です。ClassA から ClassB に対する機能の嫉妬は、ClassA から ClassB への密結合を示しています。一般的な解決策は、ClassA の該当するメソッドの機能を ClassB に移動することです。ClassB は、タスクに含まれる多くのデータと既に結び付いています。
ショットガン手術 (Shotgun Surgery) システムのある種の変更により、クラスのグループに対する多くの小規模な変更が繰り返し発生します。一般に、1 つの論理的なアイデアまたは機能が、複数のクラスに広がっていることを意味します。これを修正するには、変更する必要のあるすべての部分のコードを取り出して、単一の凝集したクラスにまとめます。

凝集度を高くする
学術的な定義では、凝集度とはクラスのすべての機能、データ、およびメソッドが相互にどの程度密接に関連しているかを示す尺度です。筆者の考えでは、凝集度とはクラスがシステム内で明確に定義された役割を持っているかどうかの尺度です。一般には、凝集度が高いのは良いことだと考えられ、"高凝集度" という言葉が呪文のように繰り返されます。しかし、なぜでしょうか。
コーディングすることを、コンピュータと会話することだと考えてみましょう。もっと正確に言えば、私たちはコンピュータと複数の会話を同時に行っています。それらは、セキュリティはどのように実行されているか、インフラストラクチャの機能ではどのような動作が想定されているか、ビジネス ルールはどうなっているか、などの会話です。
多数の異なる会話が一度に行われている騒がしいパーティでは、必要な 1 つの会話に注目するのが難しい場合があります。行われている会話が 1 つだけの静かな環境では、はるかに簡単に会話できます。
凝集度を簡単にテストするには、クラスを調べて、クラスのすべての内容が、クラス名に直接関係しており、クラス名によって説明されているものであるかどうかを判定します。InvoiceManager などの漠然としたクラス名は考えません。クラス名と関係のない機能が含まれている場合、おそらくその機能は別のクラスに属するものです。別のクラス名の下に独立して簡単にグループ化できるようなメソッドとフィールドのサブセットが見つかった場合は、それらのメソッドとフィールドを新しいクラスに抽出することが必要な場合もあります。
たとえば、TrainStation、Train、および Conductor の各クラスの中に、TrainSchedule クラスのテーマと最も正確に適合すると思われるメソッドやデータが見つかった場合は、それらのメソッドやデータを TrainSchedule に移動します。設計に関して筆者が気に入っている表現の 1 つは、"コードは、それが見つかってほしい場所に置くこと" というものですが、この場合はそれが該当します。列車のダイヤに関係のある機能は TrainSchedule クラスにあるのが最も論理的です。
先に示した会話の比喩を拡大するなら、凝集したクラスとサブシステムで構成されるシステムは、適切に設計されたオンライン ディスカッション グループのようなものです。オンライン グループの各分野は特定の 1 つのトピックに狭く集中しているため、ディスカッションを簡単にたどることができ、ある主題についての対話を探している場合、訪ねるべき部屋は 1 つだけです。

不適切な親密性を除去する
不適切な親密性とは、あるクラスのメソッドが別のクラスについて詳しすぎる知識を持っていることです。不適切な親密性は、クラス間の有害な密結合を示しています。たとえば、あるビジネス ロジック クラスが、ビジネス ロジックの処理に必要なデータを取得するために、DataServer1 クラスのインスタンスを呼び出すとします。図 2 に例を示します。この場合、Process メソッドは、DataServer1 の内部処理の多くと、SqlDataReader クラスに関する若干のことを知っている必要があります。
図 2 不適切な親密性
public void Process() {
  string connectionString = getConnectionString();
  SqlConnection connection = new SqlConnection(connectionString);
  DataServer1 server = new DataServer1(connection);

  int daysOld = 5;
  using (SqlDataReader reader = server.GetWorkItemData(daysOld)) {
    while (reader.Read()) {
      string name = reader.GetString(0);
      string location = reader.GetString(1);

      processItem(name, location);
    }
  }
}
それでは、図 2 のコードを書き直して、不適切な親密性を取り除いてみましょう。
public void Process() {
  DataServer2 server = new DataServer2();
  foreach (DataItem item in server.GetWorkItemData(5)) {
    processItem(item);
  }
}
このバージョンのコードを見るとわかるように、SqlConnection オブジェクトと SqlDataReader オブジェクトのすべての操作を DataServer2 クラスの内部にカプセル化しています。DataServer2 はそれ自体の構成も行うため、新しい Process メソッドは DataServer2 の設定について何も知る必要がありません。また、GetWorkItemData から返される DataItem オブジェクトも厳密に型指定されたオブジェクトです。
それでは、緩い結合のいくつかの目標について、2 つのバージョンの Process メソッドを分析してみましょう。ます、コードは読みやすくなっているでしょうか。第 1 と第 2 のバージョンの Process は同じ基本タスクを実行しますが、どちらの方が読みやすく簡単に理解できるでしょうか。個人的には、データ アクセス コードをかき分けないで済む方が、ビジネス ロジックの処理を読むのも理解するのも簡単です。
クラスは利用しやすくなっているでしょうか。DataServer1 のコンシューマは、SqlConnection オブジェクトの作成方法を知り、返される DataReader の構造を理解し、DataReader の反復処理とクリーンアップを行う必要があります。DataServer2 のコンシューマで必要な作業は、引数のないコンストラクタを呼び出した後、厳密に型指定されたオブジェクトの配列を返す 1 つのメソッドを呼び出すだけです。DataServer2 は、ADO.NET 接続の設定と、開いた DataReaders のクリーンアップを処理します。
それでは、変更の可能性のある部分をコードの狭い領域に分離することについてはどうでしょう。第 1 のバージョンのコードでは、DataServer の処理方法に対して加えられたほぼすべての変更が、Process メソッドに影響を与えます。第 2 の、よりカプセル化されたバージョンの DataServer では、Process メソッドに影響を与えることなく、データ ストアを Oracle データベースまたは XML ファイルに切り替えることができます。

デメテルの法則
デメテルの法則は、設計に関する経験則です。この法則を簡単に定義するなら、"直接知っている友人とだけ話せ" ということです。デメテルの法則は、図 3 に示すようなコードの潜在的な危険に対する警告です。
図 3 法則の違反
public interface DataService {
  InsuranceClaim[] FindClaims(Customer customer);
}

public class Repository {
  public DataService InnerService { get; set; }
}

public class ClassThatNeedsInsuranceClaim {
  private Repository _repository;

  public ClassThatNeedsInsuranceClaim(Repository repository) {
    _repository = repository;
  }

  public void TallyAllTheOutstandingClaims(Customer customer) {
    // This line of code violates the Law of Demeter
    InsuranceClaim[] claims =
      _repository.InnerService.FindClaims(customer);
  }
}
ClassThatNeedsInsuranceClaim クラスは、InsuranceClaim データを取得する必要があります。Repository クラスに対する参照を持ち、DataService オブジェクトを自分自身で持っています。ClassThatNeedsInsuranceClaim は Repository の内部に到達して内部の DataService オブジェクトを取得し、Repository.FindClaims を呼び出してデータを取得します。_repository.InnerService.FindClaims(customer) の呼び出しは、Repository フィールドのプロパティに対して ClassThatNeedsInsuranceClaim がメソッドを直接呼び出しているので、これはデメテルの法則に明らかに違反しています。次に図 4 を見てください。同じコードの別のサンプルですが、今度はデメテルの法則に従っています。
図 4 より適切な分離
public class Repository2 {
  private DataService _service;

  public Repository2(DataService service) {
    _service = service;
  }

  public InsuranceClaim[] FindClaims(Customer customer) {
    // we're simply going to delegate to the inner
    // DataService for now, but who knows what
    // we want to do in the future?
    return _service.FindClaims(customer);
  }
}

public class ClassThatNeedsInsuranceClaim2 {
  private Repository2 _repository;

  public ClassThatNeedsInsuranceClaim2(
    Repository2 repository) {
    _repository = repository;
  }

  public void TallyAllTheOutstandingClaims(
    Customer customer) {
    // This line of code now follows the Law of Demeter
    InsuranceClaim[] claims = _repository.FindClaims(customer);
  }
}
何が行われたのでしょうか。直接メソッドを呼び出して InsuranceClaim 情報を取得するようにしたため、Repository2 は Repository より簡単に使用できます。デメテルの法則に違反していたときに、Repository のコンシューマは Repository の実装に密接に結合されていました。改訂版のコードでは、Repository の実装の変更可能な範囲が広がり、キャッシュを追加したり、基になっている DataService をまったく別の何かに置き換えたりできます。
デメテルの法則は、潜在的な結合の問題を発見するのに役立つ強力なツールですが、無分別に従ってはいけません。デメテルの法則に違反すると強い結合がシステムに発生しますが、場合によっては、コードの安定した要素に対して結合を使用する潜在的なデメリットの方が、デメテルの法則への違反を除去するために大量の委任コードを作成するデメリットよりも小さい可能性があります。

命じろ、たずねるな
"命じろ、たずねるな (Tell, Don't Ask)" という設計の原則は、オブジェクトに実行する内容を伝えるよう促すものです。行ってはいけないのは、オブジェクトに内部状態をたずね、状態に関して何らかの決定を下し、オブジェクトに実行内容を伝えることです。"命じろ、たずねるな" スタイルのオブジェクトの相互作用に従うことは、機能を適切な場所に配置するのに役立つ良い方法です。
図 5 は、この原則に対する違反を示しています。このコードは、何かの購入についての情報を取得し、10,000 ドルを超える購入に対する割り引きの可能性を調べ、最後に口座データを調べて十分な資金があるかどうかを判定しています。前の DumbPurchase クラスと DumbAccount クラスは、何も知りません。口座と購入のビジネス ルールは、どちらも ClassThatUsesDumbEntities にエンコードされています。
図 5 質問が多すぎる
public class DumbPurchase {
  public double SubTotal { get; set; }
  public double Discount { get; set; }
  public double Total { get; set; }
}

public class DumbAccount {
  public double Balance { get; set;}
}

public class ClassThatUsesDumbEntities {
  public void MakePurchase(
    DumbPurchase purchase, DumbAccount account) {
    purchase.Discount = purchase.SubTotal > 10000 ? .10 : 0;
    purchase.Total =
      purchase.SubTotal*(1 - purchase.Discount);

    if (purchase.Total < account.Balance) {
      account.Balance -= purchase.Total;
    }
    else {
      rejectPurchase(purchase,
        "You don't have enough money.");
    }
  }
}
この種のコードでは、いくつかの問題が発生する可能性があります。このようなシステムでは、エンティティのビジネス ルールがエンティティの外部の手続きコードに分散しているので、重複の可能性があります。前にどこでビジネス ロジックを書いたのかはっきりしないため、知らず知らずのうちにロジックが重複する場合があります。
図 6 は同じコードですが、今度は "命じろ、たずねるな" のパターンに従っています。このコードでは、購入と口座のビジネス ルールを、それぞれ Purchase クラスおよび Account クラスに移動しました。購入を行うときは、Account クラスに購入金額を控除するよう伝えるだけです。Account と Purchase は、自分自身および内部ルールのことを理解しています。Account のコンシューマが知っていなければならないのは、Account.Deduct(Purchase, PurchaseMessenger) メソッドを呼び出すことだけです。
図 6 アプリケーションに行うことを伝える
public class Purchase {
  private readonly double _subTotal;

  public Purchase(double subTotal) {
    _subTotal = subTotal;
  }

  public double Total {
    get {
      double discount = _subTotal > 10000 ? .10 : 0;
      return _subTotal*(1 - discount);
    }
  }
}

public class Account {
  private double _balance;

  public void Deduct(
    Purchase purchase, PurchaseMessenger messenger) {
    if (purchase.Total < _balance) {
      _balance -= purchase.Total;
    }
    else {
      messenger.RejectPurchase(purchase, this);
    }
  }
}

public class ClassThatObeysTellDontAsk {
  public void MakePurchase(
    Purchase purchase, Account account) {
    PurchaseMessenger messenger = new PurchaseMessenger();
    account.Deduct(purchase, messenger);
  }
}
ビジネス ロジックを実行するために Account および Purchase クラスについて多くを知る必要はないため、これらのオブジェクトは使いやすくなっています。また、システム内の重複も減っている可能性があります。Accounts と Purchases のビジネス ルールは、これらのクラスを使用するコードの内部に埋め込まれるのではなく、自身のクラス内にあるため、Account クラスと Purchase クラスは簡単に再利用できます。さらに、Purchases と Accounts のビジネス ルールは、システム内の 1 か所だけに存在しているため、変更も簡単にできるはずです。
"命じろ、たずねるな" と密接な関係があるのは、情報エキスパートのパターンです。システムに新しい機能を追加する場合、どのクラスにその機能を持たせればいいでしょうか。情報エキスパート パターンでは、この機能を実行するために必要な情報を持っているのはだれか、ということをたずねます。つまり、新しい機能に対する第 1 の候補は、その機能によって影響を受けるデータ フィールドを既に持っているクラスです。購入のサンプルでは、割り引き率の可能性の判定に使用する購入に関する情報を知っているのは Purchase クラスであるため、割り引き率を計算する機能の候補としてまず最初に上がるのは Purchase クラス自体です。

伝えるのは 1 回限り
業界では、再利用可能なコードを慎重に作成するのは非常にコストが高いことがわかっていますが、それでも明らかなメリットがあるために再利用の試みは続いています。システム内の重複を探し、重複を除去または一本化する手段を探すことは、私たちにとって義務なのかもしれません。
システムの凝集度を改善する最善の方法の 1 つは、重複が見つかったら必ずそれを除去することです。システムが将来どのように変化するのかは正確にはわからないと開き直るのが一番いいのかもしれませんが、クラス構造の適切な凝集度と結合度を維持することで、コードの変更しやすさを向上させることはできます。
何年か前、工場のフロアでの貨物の流れを管理する大規模な配送アプリケーションの末席に加わったことがあります。元の状態のシステムは、受信メッセージ用のメッセージ キューを用意し、大きなビジネス ルール セットを適用して貨物の次の移動先を決定することで、メッセージに応答していました。
翌年、デスクトップ クライアントから貨物経路指定ロジックを開始することが必要になりました。残念ながら、ビジネス ロジックのコードは、MQ Series キューを読み書きするためのメカニズムとあまりにも密接に結合されていました。元のビジネス ロジックのコードを MQ Series インフラストラクチャから切り離すのは危険すぎると判断されたため、ビジネス ルールのコーパス全体が、新しいデスクトップ クライアント用の並列ライブラリに複製されました。この決定によって新しいデスクトップ クライアントは実現できるようになりましたが、同時に、将来のあらゆる作業がいっそう複雑になりました。貨物経路指定ロジックを変更するときは、非常に異なる 2 つのライブラリを並行して変更する必要があり、この種のビジネス ルールの変更は頻繁に発生します。
この現実のシナリオは、2 つの教訓を示しています。コードに含まれる重複によってシステムを構築した組織には現実のコストが発生し、そのような重複は主としてクラスの構造の結合度と凝集度の品質が悪いためでした。このことは、企業の利益と損失に直接影響する可能性があります。
コード内の重複を見つける 1 つの方法は、後で設計を改善するための機会として行うことです。一部の機能が重複しているクラスが 2 つ以上ある場合、重複している機能はまったく異なる役割である必要があります。コードベースの凝集度の品質を向上させる最善の方法の 1 つは、重複する部分を取り出して、コードベース間で共有できる別のクラスを作成することです。
無害に見える重複であっても頭痛の種になる可能性があることを、筆者は苦労して学習しました。Microsoft .NET Framework 2.0 でジェネリックが出現したとき、次のようなパラメータ化された Repository クラスが多く作成されました。
public interface IRepository<T> {
  void Save(T subject);
  void Delete(T subject);
}
このインターフェイスでは、T は Invoice、Order または Shipment などのドメイン エンティティになります。StructureMap のユーザーは、このコードを呼び出して、Invoice オブジェクトのような特定のドメイン エンティティを処理できる完全に形式化されたリポジトリ オブジェクトを取得できることを期待していました。
IRepository<Invoice> repository =
  ObjectFactory.GetInstance<IRepository<Invoice>>();
これはすばらしい機能のように見えたので、筆者はこの種のパラメータ化された型のサポートを追加しました。StructureMap に対するこの変更は、次のようなコードのために非常に難しいことがわかりました。
_PluginFamilies.Add(family.PluginType.FullName, family);
そして、次のようなコードもあります。
MementoSource source =
  this.getMementoSourceForFamily(pluginType.FullName);
また、次のようなコードもあります。
private IInstanceFactory this[Type PluginType] {
  get {
    return this[PluginType.FullName];
  }
  set {
    this[PluginType.FullName] = value;
  }
}
重複が見つかりましたか。このサンプルとあまり関係はありませんが、筆者には System.Type に関係のあるオブジェクトは Type.FullName プロパティをキーとして使用して Hashtable に格納するという暗黙のルールがありました。それは小さなちょっとしたロジックですが、コードベースのあらゆる場所にコピーしていました。ジェネリックを実装するとき、筆者は Type.FullName の代わりに実際の型でオブジェクトを内部的に格納する方がよいと判断しました。
この一見するとわずかな動作の変更には、少しばかりのデータをコピーしなければならなかった回数のせいで、予想していたような何時間かではなく、何日もかかりました。このことから得られた教訓は、どれほど些細に見えることであっても、システムでルールを表現するのは 1 回だけにしなければならない、ということです。

まとめ
凝集度と結合度は設計とアーキテクチャのすべてのレベルに適用されますが、ここでは主に、クラスとメソッドのレベルでの細部の詳細に注目しました。確かに、大きなアーキテクチャについて正しい決定を下すことはできます。技術の選択、プロジェクトの構造、および物理的な展開はすべて重要ですが、通常、これらは複数の選択によってしっかり拘束されており、トレードオフは一般によく理解されています。
筆者は、クラスやメソッドのレベルで行った何百、何千の小さい決定の効果が累積されて、オブジェクトの成功にいっそう大きな影響を与えることを知りました。そして、小さい事柄に関する方が、はるかに広い範囲の選択肢と代替案があります。現実の生活では一般的な真実ではないかもしれませんが、ソフトウェアの設計では、小さい事柄に気を配る必要があります。
開発者は一般に、これらの凝集度と結合度に関する心配は象牙の塔の理論であって、作業の遂行には邪魔になると考える傾向があります。筆者の考えでは、コードの凝集度と結合度の質が高ければ、コードの生産性が長い間維持されます。凝集度と結合度の品質に関する理解を、これらの品質に関して意識して考える必要がないくらいまで自分のものにすることを、強くお勧めします。さらに、設計スキルを向上させるためにお勧めする最善の練習の 1 つは、これまでに自分が行ったコーディング作業を見直し、古いコードを改善できた方法を探してみること、そしてコードの変更を容易にした、またはコードの調整を困難にした過去のコードの要素を思い出してみることです。

ご意見やご質問は、mmpatt@microsoft.com まで英語でお送りください。

Jeremy Miller は、C# の Microsoft MVP であり、.NET での依存関係のインジェクション用に公開されているオープン ソースの StructureMap (structuremap.sourceforge.net) ツール、および .NET での FIT テスト用に近日公開予定の意欲的な StoryTeller (storyteller.tigris.org) ツールの作成者でもあります。彼のブログ「日陰の開発者」は、CodeBetter サイト上の codebetter.com/blogs/jeremy.miller で公開されています。


サービス ステーション
WCF ベースのサービスでの承認
Dominick Baier および Christian Weyer

コードのダウンロード : MSDN Code Gallery (175 KB)
オンラインでのコードの参照

目次
ロールベースの承認
ASP.NET ロール プロバイダ
カスタム プリンシパル
承認ロジックの一元化
クレームベースの承認
クレーム
クレーム セット
承認コンテキスト
クレームの変換
セキュリティ トークン サービス
分散アプリケーションにサービス指向の原則を採用すると、セキュリティに関して、それ以前とは多少異なる課題に直面します。サービスを呼び出すごとにセキュリティの境界を越えるということを、すぐに実感するでしょう。
すべてに当てはまるわけではありませんが、一般的には、サービスの呼び出しの送信者と受信者の間にネットワークが存在します。通常は通信フレームワークによって自動的に認証が処理されますが、それに加えて、承認に関する固有の戦略とインフラストラクチャを用意する必要があります。
Windows Communication Foundation (WCF) は、サービスで承認を実装するための強力な機能を提供します。簡単に使用できるロールベース システムと、さらに強力かつ複雑なクレームベースの API のいずれかを選択できます。この記事では、両方のシステムを比較し、それぞれのシステムで堅牢なサービス承認を実装する方法について説明します。

ロールベースの承認
ロールベースの承認の基になっている考え方は、ロールの一覧をユーザーに関連付けるということです。実行時に、サービス コードは、ロールの一覧のクエリを実行してセキュリティ関連の決定を行います。これらのロールは、Windows セキュリティ システム (ロールはグループと呼ばれる) やデータベースなどのカスタム ストアから提供されます。
Microsoft .NET Framework のロール API は、IIdentity (ID 情報) と IPrincipal (ロール情報) という 2 つのインターフェイスに基づいています。IIdentity 派生クラスには、ユーザーの名前、ユーザーが認証済みであるかどうか、どのようにして認証されたか、などの情報が保持されます。
IPrincipal 派生クラスは、IsInRole という単一のメソッドを実装する必要があります。このメソッドで、ロール名を渡してブール値を取得します。また、プリンシパルでラップする ID クラスへの参照がプリンシパルに保持されます。
.NET Framework には、これらのインターフェイスの実装がいくつか付属します。WindowsIdentity および WindowsPrincipal は、Windows ユーザーの詳細をラップします。GenericIdentity および GenericPrincipal は、認証されたカスタム ユーザーを表すために使用できます。
さらに、プリンシパルを格納して、アプリケーション内で実行中のスレッド (WCF の操作要求など) にそのプリンシパルを関連付けることもできます。これは、System.Threading.Thread クラスの CurrentPrincipal スレッドの静的プロパティです。一般的な手順としては、アプリケーションの 1 つのパートで Thread.CurrentPrincipal を設定してから、別のパートでこのプロパティにアクセスし、格納された IPrincipal 実装を取得します。続いて、このクラスで IsInRole method を呼び出し、現在のユーザーのロールの一覧に対してクエリを実行して、呼び出し元がその操作に対して承認されていることを確認します。
WCF で行われる作業は、このようになります。構成された認証および資格情報の種類に基づき、WCF は、対応する IIdentity 実装を作成します (Windows 認証では WindowsIdentity、他のほとんどの場合は GenericIdentity)。次に、WCF サービスの動作 (ServiceAuthorization) の構成に基づき、この ID をラップしてロール情報を提供する IPrincipal 実装が作成されます。続いて、サービス操作からアクセスできるように、Thread.CurrentPrincipal に IPrincipal が設定されます。
プログラムで IsInRole を呼び出してロールベースのセキュリティ チェックを実行する方法とは別に、ロール要求によってサービス操作に注釈を付けるための PrincipalPermissionAttribute という属性もあります。PrincipalPermissionAttribute では、SecurityException をスローすることにより、サービス操作の実行よりも前にクライアントに承認の失敗を通知します。WCF は、この例外をキャッチして、アクセス拒否のエラー メッセージを生成します。WCF クライアント プラミングにより、この特定のエラーを SecurityAccessDeniedException という種類の .NET 例外に変換します。図 1 は、ロールのチェックとエラー処理を行うさまざまな方法を示しています。
図 1 ロールのチェックとエラー処理

サービス
class Service : IService {
  // only 'users' role member can call this method
  [PrincipalPermission(SecurityAction.Demand, Role = 'users')]
  public string[] GetRoles(string username) {
    // only administrators can retrieve the role information for other users
    if (ServiceSecurityContext.Current.PrimaryIdentity.Name != username) {
      if (Thread.CurrentPrincipal.IsInRole('administrators')) {
        ...
      }
      else {
        // access denied
        throw new SecurityException();
      }
    }
  }
}

クライアント
var factory = new ChannelFactory<IService>('*');
factory.Credentials.UserName.UserName = 'bob';
factory.Credentials.UserName.Password = 'bob';
var proxy = factory.CreateChannel();

try {
  Console.WriteLine('\nBob: roles for Bob --');
  proxy.GetRoles('bob').ToList().ForEach(i => Console.WriteLine(i));

  Console.WriteLine('\nBob: roles for Alice --');
  proxy.GetRoles('alice').ToList().ForEach(i => Console.WriteLine(i));
}

catch (SecurityAccessDeniedException) {
  Console.WriteLine('Access Denied\n');
}
ServiceAuthorization サービスの動作により、IPrincipal インスタンスの作成が要求スレッドに関連付けられるように制御されます。既定では、WCF は Windows 認証を前提としており、Thread.CurrentPrincipal に WindowsPrincipal を設定しようとします。
クライアントが Windows ユーザーでない場合は、ASP.NET ロール プロバイダからロールを取得する方法や、カスタム データベースからロールを取得するためのカスタム承認ポリシーを実装する方法を選択できます。または、カスタム IPrincipal 型を実装して、IsInRole 実装を完全に制御することもできます。

ASP.NET ロール プロバイダ
WCF では、ASP.NET ロール プロバイダを使用してユーザーのロールを取得できます。組み込みプロバイダのいずれかを使用するか (SQL Server 用のプロバイダや Microsoft 承認マネージャ)、または System.Web.Security.RoleProvider から派生させた固有のプロバイダを記述します。
ロール プロバイダに接続する場合、WCF では、IsInRole のすべての呼び出しをロール プロバイダの IsUserInRole メソッドに転送する実行中のスレッドに RoleProviderPrincipal をアタッチします。このメソッドは、対象となるユーザー名とロールを受け取り、ユーザーがそのロールのメンバである場合には true を返し、そうでない場合は false を返します。
図 2 では、カスタム ロール プロバイダの例が必要な構成エントリと共に示されています。WCF は、サービスでロールのチェックが発生するたびにロール プロバイダを呼び出します。ロールのチェックが数多く発生する場合は、ロール ストアへの過度のラウンドトリップを回避するために何らかのキャッシング手法を導入することをお勧めします。
図 2 ロール プロバイダと構成エントリの例

ロール プロバイダ
class CustomRoleProvider : RoleProvider {
  public override string[] GetRolesForUser(string username) {
    if (username == 'administrator') {
      return new string[] { 'administrators', 'users' };
    }
    else {
      return new string[] { 'sales', 'marketing', 'users' };
    }
  }

  public override bool IsUserInRole(string username, string roleName)  {
    return GetRolesForUser(username).Contains(roleName);
  }
  ...
}

構成
<behaviors>
  <serviceBehaviors>
    <behavior name='security'>
      <serviceAuthorization
        principalPermissionMode='UseAspNetRoles'
        roleProviderName='CustomProvider'
      </serviceAuthorization>
    </behavior>
  </serviceBehaviors>
</behaviors>

<system.web>
  <roleManager
    enabled='true'
    defaultProvider='CustomProvider'>
    <providers>
      <add name='CustomProvider'
        type='Service.CustomRoleProvider, Service' />
    </providers>
  </roleManager>
</system.web>
キャッシングなどのカスタマイズは、ロール プロバイダ自体に実装するか、カスタム IPrincipal 実装を経由して行うことができます。その方法について見ていきましょう。

カスタム プリンシパル
オプションの 1 つとして、WCF へのカスタム IPrincipal の実装があります。この実装では、各要求の認証段階で暗黙的にコードを実行できるようになります。その手順としては、まず、カスタム プリンシパルを作成して WCF プラミングに返す必要があります。これにより、サービス コードで Thread.CurrentPrincipal からカスタム プリンシパルが利用可能になります。カスタム プリンシパルを使用すると、ロールベースのセキュリティ全体をカスタマイズし、サービス開発者専用のセキュリティ ロジックを公開することができます。
カスタム プリンシパルの記述は簡単です。IPrincipal インターフェイスを実装して、カスタム機能を追加するだけです。プリンシパルを WCF に統合するには、ServiceAuthorization 要素の PrincipalPermissionMode 属性を "custom" に設定して、プリンシパルの作成および WCF への適用のための承認ポリシーを提供する必要があります。
承認ポリシーとは、System.IdentityModel.Policy.IAuthorizationPolicy インターフェイスを実装するクラスです。このインターフェイスの実装では、要求ごとに呼び出される Evaluate というメソッドを使用する必要があります。これで、WCF サービス セキュリティ コンテキストに到達してカスタム プリンシパルを設定することができます。図 3 は、カスタム プリンシパルとその承認ポリシーおよび対応する構成エントリを示しています。
図 3 カスタム プリンシパルと承認ポリシー

プリンシパル
class CustomPrincipal : IPrincipal {
  IIdentity _identity;
  string[] _roles;
  Cache _cache = HttpRuntime.Cache;

  public CustomPrincipal(IIdentity identity) {
    _identity = identity;
  }

  // helper method for easy access (without casting)
  public static CustomPrincipal Current {
    get {
      return Thread.CurrentPrincipal as CustomPrincipal;
    }
  }

  public IIdentity Identity {
    get { return _identity; }
  }

  // return all roles (custom property)
  public string[] Roles {
    get {
      EnsureRoles();
      return _roles;
    }
  }

  // IPrincipal role check
  public bool IsInRole(string role) {
    EnsureRoles();

    return _roles.Contains(role);
  }

  // cache roles for subsequent requests
  protected virtual void EnsureRoles() {
    // caching logic omitted – see the sample download
  }
}

承認ポリシー
class AuthorizationPolicy : IAuthorizationPolicy {
  // called after the authentication stage
  public bool Evaluate(EvaluationContext evaluationContext,
     ref object state) {
    // get the authenticated client identity from the evaluation context
    IIdentity client = GetClientIdentity(evaluationContext);

    // set the custom principal
    evaluationContext.Properties['Principal'] =
       new CustomPrincipal(client);

    return true;
  }

  // rest omitted
}

構成
<behaviors>
  <serviceBehaviors>
    <behavior name='security'>
      <serviceAuthorization principalPermissionMode='Custom'>
        <authorizationPolicies>
          <add policyType='Service.AuthorizationPolicy, Service' />
        </authorizationPolicies>
      </serviceAuthorization>
    </behavior>
  </serviceBehaviors>
</behaviors>

承認ロジックの一元化
ここまでは、サービス操作の中で、WCF セキュリティ システムから提供されるロール情報にアクセスする方法について説明してきました。ただし、場合によっては、受信した各要求の検査と承認の決定を行うことのできる、一元化されたロジックを利用する方法が便利です。すべてのサービス操作に対してロジックを適用する必要がなくなります。ここで、サービス承認マネージャについて説明します。
サービス承認マネージャは、System.ServiceModel.ServiceAuthorizationManager から派生したクラスです。CheckAccessCore メソッドをオーバーライドして各要求のカスタム承認コードを実行することができます。CheckAccess で、現在のセキュリティ コンテキストおよび受信メッセージ (ヘッダーを含む) にアクセスできます。CheckAccess で false が返された場合は、WCF によってアクセス拒否のエラー メッセージが生成され、そのメッセージがクライアントに送信されます。戻り値が true の場合は、サービス操作へのアクセスが許可されます。
WS-Addressing アクション ヘッダーでクライアントが呼び出そうとする操作には、一意の識別子があります。この値を取得するには、操作コンテキストで CheckAccess に渡される IncomingMessageHeader.Action プロパティを使用します。アクション値の形式は次のとおりです。
ServiceNamespace/ContractName/OperationName
たとえば、次のようになります。
urn:msdnmag/ServiceContract/GetRoles
WCF では、完全な要求メッセージを ref パラメータとして渡します。これにより、メッセージがサービス操作に到達する前に (または、承認に失敗したためにメッセージが戻ってくる前に)、メッセージの検査だけでなく変更も行うことができます。
WCF ではメッセージが常に読み取り専用であることに注目してください。つまり、メッセージの本文を検査するには、最初にメッセージのコピーを作成する必要があります。この操作を行うには、次のコードを使用してメッセージ バッファを作成します (サイズの大きいメッセージやストリーミング メッセージについては慎重に対応する必要があります)。
MessageBuffer buffer =
  operationContext.RequestContext.RequestMessage.CreateBufferedCopy(
  int.MaxValue);
メッセージのコピーを作成したら、標準の API を使用してメッセージ コンテキストにアクセスできます。図 4 には、サービス操作から中央の場所へと承認ロジックを移動するサービス承認マネージャの実装例が示されています。
図 4 サービス承認マネージャ

コード
class AuthorizationManager : ServiceAuthorizationManager {
  public override bool CheckAccess(
    OperationContext operationContext, ref Message message) {

    base.CheckAccess(operationContext, ref message);
    string action = operationContext.IncomingMessageHeaders.Action;

    if (action == 'urn:msdnmag/IService/GetRoles') {
      // messags in WCF are always read-once
      // we create one copy to work with, and one copy for WCF
      MessageBuffer buffer = operationContext.RequestContext.RequestMessage.
        CreateBufferedCopy(int.MaxValue);
      message = buffer.CreateMessage();

      // get the username value using XPath
      XPathNavigator nav = buffer.CreateNavigator();
      StandardNamespaceManager nsm = new
        StandardNamespaceManager(nav.NameTable);
      nsm.AddNamespace('msdn', 'urn:msdnmag');

      XPathNavigator node = nav.SelectSingleNode
        ('s:Envelope/s:Body/msdn:GetRoles/msdn:username', nsm);
      string parameter = node.InnerXml;

      // check authorization
      if (operationContext.ServiceSecurityContext.PrimaryIdentity.Name ==
        parameter) {
        return true;
      }
      else {
        return (GetPrincipal(operationContext).IsInRole(
          'administrators'));
      }
    }
  return true;
  }
  // rest omitted
}

構成
<serviceAuthorization
  serviceAuthorizationManagerType='Service.AuthorizationManager, Service'>

クレームベースの承認
サービスの量と複雑さがある限界に達すると、ロールベースのセキュリティの能力や柔軟性では対処できなくなります。エンタープライズ規模のサービス指向アプリケーションがきわめて複雑になるまで、さして時間はかかりません。さまざまなクライアントで多様なクライアント フレームワークおよび資格情報の種類が使用され、各クライアントが各種のバックエンド サービスとやり取りし、そのバックエンド サービスで別のサービスが呼び出されます。場合によっては、信頼されたサブシステムの手法を利用して直接的な呼び出し元のみが承認されますが、呼び出しチェーンで最初の呼び出し元 ID を転送する必要が生じることもあります。また、内部サービスへの外部クライアント (パートナーや顧客) からのアクセスが必要になると、複雑化が一気に進みます。
システムの複雑化が進んだ場合、たちまち問題となるのは、ID および承認データをモデル化する IIdentity と IPrincipal の機能が不十分であるという点です。ロールベースのセキュリティの使用は二分決定に限定されており、特定のサブジェクトへの任意のデータの関連付けには適用できません。分散システムに属しているエンティティの ID を表現できる、テクノロジに依存しない形式が必要です。
以上の要件に対応するために、.NET Framework Version 3.0 以降では、クレームベースの手法による新しい ID およびアクセス制御 API が導入されています。この新しい API は、System.IdentityModel アセンブリおよび名前空間に存在します。また、マイクロソフトでは、"Zermatt" というコード ネームを持つ新しい ID フレームワークのプレビューをリリースしたばかりです。この新しいフレームワークは、System.IdentityModel のクラスおよび概念に基づいて構築されており、サービスおよびアプリケーションに対するクレームベースのセキュリティをより簡単に構築するためのものです。注目すべき点は、System.IdentityModel および Zermatt は WCF にまったくバインドされておらず、任意の .NET アプリケーション用のクレームに使用できることです。これらの API が WCF に緊密に統合されていることは、意図的ではありません
理解する必要のある System.IdentityModel 名前空間の最も重要な構造クラスは、Claim、ClaimSet、AuthorizationContext、および AuthorizationPolicy です。ここからは、各クラスの詳細と、WCF セキュリティ システムにどのように統合されているかについて説明します。

クレームとは、システム内のエンティティに関連付けることのできる情報の集まりです。ほとんどの場合、エンティティはユーザーですが、サービスやリソースである場合もあります。クレームは、3 つの部分の情報から成っています。クレームの種類、クレームの内容、およびクレームがサブジェクトの ID または機能のどちらを表しているのかを示す情報です。このデータ構造は、System.IdentityModel.Claims.Claim というクラスによって表されます。また、Claim は DataContract であり、シリアル化対応です (サービス境界を越えてクレームをを転送する場合に重要です)。
[DataContract(Namespace =
  'http://schemas.xmlsoap.org/ws/2005/05/identity')]
public class Claim {
  [DataMember(Name = 'ClaimType')]
  public string ClaimType;

  [DataMember(Name = 'Resource')]
  public object Resource;
  [DataMember(Name = 'Right')]
  public string Right;
}
ClaimType は、クレームの種類を識別する URI です。固有の種類の URI を使用できますが、ClaimTypes クラスからいくつかの標準的な種類を利用できます。名前を表すクレームでは、ClaimTypes.Name によって保持される URI 値を使用できます。
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
Resource プロパティは、実際のクレーム値を格納します。Resource が型オブジェクトであることに注意してください。つまり、任意の複雑な情報をクレームに関連付けることができます (単純な文字列から複雑なグラフまで)。ただし、これがシリアル化では問題となる場合があることを頭に入れておいてください。問題が生じた場合は、プリミティブ型にしてください。Right プロパティの目的は、クレーム セットについて理解すると明らかになります。
新しいクレームを作成するには、Claim コンストラクタを使用するか、または Claim クラスの静的メソッドの 1 つを利用します (標準的なクレームの種類の場合)。
Claim purchaseLimitClaim = new Claim(
  'http://www.leastprivilege.com/claims/purchaselimit',
  5000,
  Rights.PossessProperty);
Claim nameClaim = Claim.CreateNameClaim('Alice');

一般的に、クレームは単体では使用されません。通常はクレーム セットにグループ化されます。ClaimSet クラスには、クレームの一覧および各クレームの発行者への参照が保持されます。
[DataContract(Namespace='http://schemas.xmlsoap.org/ws/2005/05/identity')]
public abstract class ClaimSet : IEnumerable<Claim>, IEnumerable {
  public abstract ClaimSet Issuer { get; }
  public abstract Claim this[int index] { get; }
  public static ClaimSet System { get; }
  public static ClaimSet Windows { get; }
}
発行者は、分散システムが複雑になった場合に重要な概念です。クレームは、さまざまなソースから提供されます (現在のサービス、呼び出し側サービス、セキュリティ トークン サービスなど)。発行者をクレーム セットに関連付けることにより、サービスではクレーム ソースを区別できます (信頼性の判断にも影響します)。
発行者は、クレーム セットとしても表現されます。System.IdentityModel には、System (システムから提供されるクレームの場合) と Windows (Windows セキュリティ サブシステムから提供されるクレームの場合) という 2 つの定義済み発行者クレーム セットが付属します。両方とも、ClaimSet クラスで静的プロパティとして利用可能です。
もう 1 つの重要な概念は、クレーム セットの ID です。クレーム セットは、通常、表現するサブジェクトを一意に識別する 1 つのクレームを必要とします。これには、Claim クラスの Right プロパティが使用されます。クレーム セットは、Identity の Right 値を持つ 1 つのクレーム (一意の識別子) と、PossessProperty の Right 値を持つ複数のクレーム (サブジェクトを説明する追加のクレーム) を保持する必要があります。
固有のクレーム セットを作成するには、DefaultClaimSet インスタンスを作成します。また、さらに制御を高めるには、抽象クラスの ClaimSet より派生させます。
DefaultClaimSet myClaimSet =
  new DefaultClaimSet(ClaimSet.System, new List<Claim> {
  new Claim(ClaimTypes.Name, 'Alice', Rights.Identity),
  new Claim(purchaseLimitClaimType, 5000, Rights.PossessProperty),
  Claim.CreateMailAddressClaim(new MailAddress('alice@leastprivilege.com'))
  });
System.IdentityModel には、Windows トークンおよび X.509 証明書をクレームに変換するための 2 つのクレーム セットが付属します。それぞれ、WindowsClaimSet と X509CertificateClaimSet です。WCF では、これらの 2 つのクレーム セットが Windows および証明書のクライアント資格情報の種類に使用されます。
ClaimSet は、クレームのクエリのために 2 つの処理プリミティブを提供します。FindClaims と ContainsClaim です。FindClaims は、指定されたクレームの種類に対してクレームのコレクションを返します。ContainsClaim は、特定のクレームの存在に関する情報を返します。
クレーム セットは基本的に汎用であるため、通常は、クレーム セット データ構造に対して動作するドメイン固有の拡張を独自に記述します。C# 3.0 の拡張メソッドは、これらの種類を拡張してカスタム機能を提供する場合に便利な方法です (私が作成した Claim、ClaimSet、および関連する型の拡張メソッドのライブラリを leastprivilege.com/IdentityModel からダウンロードできます)。

承認コンテキスト
クレームを使用して実際の作業を始める前に、IdentityModel の最後のキーワードである承認コンテキストについて理解する必要があります。承認コンテキストは、クレーム セットのコンテナとして、さらには変換ポリシー (後で説明します) として機能します。WCF では、スレッドの静的な ServiceSecurityContext によって承認コンテキストが利用可能になります。図 5 に示されているコード スニペットを使用して、現在のサービス操作要求に関連付けられているクレーム セットおよびクレームをダンプすることができます。
図 5 クレームとクレーム セットの検索
public void ShowAuthorizationContext() {
  AuthorizationContext context =
    ServiceSecurityContext.Current.AuthorizationContext;

  foreach (ClaimSet set in context.ClaimSets) {
    Console.WriteLine('\nIssuer:\n');
    Console.WriteLine(set.Issuer.GetType().Name);
    foreach (Claim claim in set.Issuer) {
      ShowClaim(claim);
    }

    Console.WriteLine('\nIssued:\n');
    Console.WriteLine(set.GetType().Name);
    foreach (Claim claim in set) {
      ShowClaim(claim);
    }
  }
}

private void ShowClaim(Claim claim) {
  Console.WriteLine('{0}\n{1}\n{2}\n',
    claim.ClaimType,
    claim.Resource,
    claim.Right);
}
このコードを WCF サービスに配置すると、構成されたクライアント資格情報の種類に応じて異なる出力が得られます。Windows 承認が有効になっている場合、WCF で生成されるクレーム セットには、ユーザーの SID (ID クレーム)、グループの SID、およびユーザー名が含まれます。クライアント証明書を使用して認証された要求の場合、クレーム セットには、サブジェクト名、パブリック キー、拇印 (ID クレーム)、有効期限の日付などを示す各クレームが含まれます。ユーザー名とパスワードの単純なペアで認証されるユーザーの場合は、1 つのユーザー名 ID クレームのみが出力されます。
おわかりのように、WCF に統合されたクレーム層により、Windows トークンや証明書などテクノロジ固有の ID 情報が、標準的な API でクエリを実行できる汎用データ構造に変換されます。これにより、複数の資格情報の種類に対応したサービスをより簡単に記述できるようになり、テクノロジ固有の API にこだわる必要がなくなります。また、新しい資格情報の種類を WCF に追加すると、対応するプラミングにより、その独自の形式がクレームに変換されます。

クレームの変換
一般的にサービス操作では、ユーザーの SID や証明書の拇印の情報は不要ですが、ドメイン固有の ID 情報 (ユーザー ID、電子メール アドレス、購入限度など) は必要です。クレームの変換は、テクノロジ固有の ID の詳細をアプリケーション固有の情報に変換するプロセスです。
すべての ID 情報が共通の形式で保持されているため、クレーム情報を解析および分析してアプリケーション コンテキストに適した新しいクレームを作成することは簡単です。WCF で生成されたクレーム セットの ID クレームに基づき、要求をアプリケーションのユーザー ID にマップして、適切な ID および承認情報を新しいクレーム セットに追加することができます。図 6 に示すように、その後は、サービス操作で WCF 承認コンテキストにアクセスして対象となる情報を取得するだけです。
図 6 クレームの変換 (クリックすると拡大画像が表示されます)
クレームの変換は、承認ポリシーで達成されます。既に、カスタム プリンシパルの作成で承認ポリシーを使用しましたが、今回は別の目的があります。承認ポリシーは、クレーム生成プロセスの一部となり、WCF の内部クレーム生成が完了した後で実行されます。つまり、呼び出し元の資格情報に関連付けられたクレームは既にアクセス可能であるということです。
既存のクレームでクエリを実行して ID 情報を取得した後で、その情報に基づき、自分のドメイン固有のクレームをモデル化した新しいクレーム セットを作成できます。作成したものをクレーム セットの一覧に追加できます。この一覧は、要求がサービス操作に到達する前に、承認コンテキストを作成するために使用されます。
IAuthorizationPolicy インターフェイスで実装が必要な 3 つのメンバについて説明します。Id は、ポリシーの一意の識別子を返します (通常は GUID)。Issuer は、このポリシーを作成する発行者を表すクレーム セットを返します。ここで最も重要なメソッドは、Evaluate です。このメソッドは評価コンテキストを受け取ります。評価コンテキストでは、基本的に、まだ構築プロセスにある承認コンテキストが表されます。
EvaluationContext.ClaimSets を通して、現在生成されているすべてのクレームにアクセスできます。Evaluate メソッド内から ServiceSecurityContext にアクセスしないでください。アクセスすると、承認ポリシーが再度トリガされて無限ループが発生してしまいます。新しいクレーム セットを評価コンテキストに追加するには、EvaluationContext.AddClaimSet を呼び出します。このクレーム セットは、後で承認コンテキストの一部となります。
図 7 には、一般的なパターンを実装する承認ポリシーの例が示されています。この例では、まず、最初のクレーム セットの ID クレームを取得します。このクレームは、マッピング コンポーネントに送信されます。そこで、クレームが調査され、アプリケーション ユーザー ID が返されます。その後で、ポリシーは ID を使用してデータ ストアを検出し、クレーム セットの一部となる情報を取得します。この情報は、新しいクレーム セットとしてパッケージ化され、評価コンテキストに追加されます。
図 7 承認ポリシー
class CustomerAuthorizationPolicy : IAuthorizationPolicy {
  Guid _id = Guid.NewGuid();
  // custom issuer claim set
  ApplicationIssuerClaimSet _issuer = new ApplicationIssuerClaimSet();

  public bool Evaluate(EvaluationContext evaluationContext,
    ref object state) {
    Claim id = evaluationContext.ClaimSets.FindIdentityClaim();
    string userId = Map(id);

    evaluationContext.AddClaimSet(this, new CustomerClaimSet(userId));

    return true;
  }

  public ClaimSet Issuer {
    get { return _issuer; }
  }

  public string Id {
    get { return 'CustomerAuthorizationPolicy: ' + _id.ToString(); }
  }
}
最後の手順では、serviceAuthorization の動作構成に承認ポリシーを追加します。複数のポリシーを追加すると、追加した順序で起動されます。つまり、あるポリシーで追加された値に次のポリシーが依存するといった、複数の手順から成る承認ポリシーを記述できます。これは、より複雑なシナリオに適した強力な手法です。
<serviceAuthorization>
  <authorizationPolicies>
    <add policyType='LeastPrivilege.CustomerAuthorizationPolicy, Service' />
    <add policyType='some_other_policy' />
  </authorizationPolicies>
</serviceAuthorization>
前のコードを再実行して承認コンテキストを調べると、新しいクレーム セットおよびクレームが見つかります。サービス操作では、類似のコードを使用し、承認に関するクレームをチェックします (図 8 を参照)。
図 8 承認に関するクレームをチェック
public void PlaceOrder(Order order) {
  int purchaseLimit = GetPurchaseLimit();

  if (Order.Total > purchaseLimit) {
    // do appropriate action
  }
}

private int GetPurchaseLimit() {
  AuthorizationContext context =
    ServiceSecurityContext.Current.AuthorizationContext;

  foreach (ClaimSet set in context.ClaimSets) {
    foreach (Claim claim in set.FindClaims(
      Constants.PurchaseLimitClaimType,
      Rights.PossessProperty)) {

      return int.Parse(claim.Resource.ToString());
    }
  }

  throw new Exception('Claim not found');
}
サービス承認マネージャは、クレームベースの承認でも動作します。WCF により、操作コンテキストが CheckAccessCore メソッドに渡されます。そこから、サービス セキュリティ コンテキストにアクセスすると、さらに承認コンテキストにアクセスできます。このように、承認の決定を特定の場所で一元的に行うことができます。

セキュリティ トークン サービス
セキュリティ トークン サービス (STS) とは、セキュリティ ロジックをさらに統合できるようにするツールです。STS の主要なタスクは、ユーザーを認証してセキュリティ トークンを作成することです。このセキュリティ トークンにはクレームを含めることができます。クライアントでは、最初に STS で認証してから、取得したトークンを通信先となるサービスに転送する必要があります。
対象のサービスが STS で認識されるため (この情報はトークン要求の一部)、一元的な承認が可能であり、サービスが依存するクレームを事前に生成できます。したがって、サービス エンドポイントでクレームを変換する必要はありません。STS によって一元的に行われます。この手法を導入すると、システムの複雑化が特定のレベルに達した場合に、セキュリティ インフラストラクチャを大幅に合理化できます。
セキュリティ トークン サービスは、複数の信頼されたドメインを統合するうえで重要なインフラストラクチャ コンポーネントでもあります。複数のトークン サービス間で信頼性を確立することにより、サービスで使用できる信頼境界をまたいでセキュリティ トークンを交換できます。
WCF には、STS を記述するために必要なすべての基本クラスおよび前のシナリオに対する自動的なクライアント側/サービス側のサポートがあります。ただし、関連する WS-* 仕様をすべて適切に実装することは複雑なタスクです。代わりに、市販の STS を購入するか、カスタム STS を構築するためのより高いレベルのツールキット (Zermatt など) を購入してください。次のバージョンの Microsoft Active Directory Federation Services は、完全な機能を備えた WCF 用 STSとなるように設計されています。

ご意見やご質問は、sstation@microsoft.com まで英語でお送りください。

Dominick Baier は、ドイツの thinktecture 社 (www.thinktecture.com) のセキュリティ コンサルタントです。また、DevelopMentor で、セキュリティおよび WCF のカリキュラムの指導も行っており、Developer Security MVP でもあり、『Developing More Secure Microsoft ASP.NET 2.0 Applications』の著者でもあります。彼のブログは leastprivilege.com で公開されています。

Christian Weyer は thinktecture の共同設立者かつ主設計者であり、Java、COM、DCOM、COM+、Web Services、WCF、およびその他のテクノロジを使用する分散アプリケーションのモデル化と実装を行ってきました。連絡先は、www.thinktecture.com/staff/christian です。


基礎
永続性のあるサービスで状態を管理する
Juval Lowy

コードのダウンロード : MSDN Code Gallery (240 KB)
オンラインでのコードの参照

目次
インスタンス ID と永続性のあるストレージ
明示的なインスタンス ID
インスタンス ID に対するコンテキスト バインディング
コンテキスト ID に対する標準キー
自動的な永続性のある動作
永続化プロバイダ
分散コンピューティングでは、長い時間にわたって実行されるプロセスがよく見られます。複数の実行シーケンスから構成され、数日間または数週間も続くビジネス プロセスもあります。そのようなプロセスに関係しているクライアント (場合によってはエンド ユーザー) の中には、アプリケーションに接続して少量の作業を実行し、ワークフローを新しい状態に遷移させた後、不確定な期間にわたって切断してから、再度接続してワークフローを続行するようなものがあります。クライアントは任意の時点でワークフローを終了して新しいワークフローを開始するよう決定したり、または、バックエンドのワークフロー サービスがワークフローを終了させたりすることもあります。そのような間欠的なフローに対して、クライアントからの呼び出しを待つ間、プロキシやサービスをメモリに保持しておく必要はあるでしょうか。
このコラムでは、そのような長時間実行されるサービスをサポートするいくつかの技法について説明し、一般的な疑問のいくつかに回答します。また、それに付随する Windows Communication Foundation (WCF) の機能、および著者が開発したいくつかのヘルパ クラスも紹介します。

インスタンス ID と永続性のあるストレージ
長時間実行されるサービスの場合、待っているプロキシやサービスをメモリに保持し続けることにほとんど意味がないのは明らかです。そのようなアプローチは、時の試練に耐えられません。最低でも、タイムアウトの問題によって接続は最終的に終了してしまい、いずれの側のコンピュータも簡単に再起動またはログオフできる方法がありません。解決策は、サービスの状態をメモリに保持することを避け、新しいインスタンスの各呼び出しをそれぞれ独自の一時的なメモリ内状態によって処理することです。すべての操作に対して、サービスはその状態を何らかの永続性のあるストレージ (ファイルやデータベースなど) から取得し、その操作に対して要求された作業の単位を実行した後、呼び出しの終わりでその永続的なストレージに状態を再度保存します。このように動作するサービスは、永続性のあるサービスと呼ばれます。また、このストレージは共有できるため、永続性のあるサービスは、スケーラビリティ、冗長性、保守など任意の目的で、複数のコンピュータ間でスケールを調整できます。
長時間続くプロセスの実際の作業が開始される前に、サービスはその状態を永続性のあるストレージに書き込んで、後続の操作でそれを参照できるようにする必要があります。ワークフローが終了したら、サービスはその状態を削除する必要があります。そうしないと、時間の経過と共にストレージが不要な状態情報でいっぱいになってしまいます。
各操作に対して新しいサービス インスタンスが作成されるため、インスタンスは、永続性のあるストレージから状態を検索して読み込む方法を備えている必要があります。したがって、クライアントは、インスタンスに対して何らかの状態識別子を提供する必要があります。この識別子はインスタンス ID と呼ばれます。サービスに間欠的に接続するクライアントや、呼び出し間で初期状態に戻るクライアント アプリケーションまたはコンピュータをサポートするために、一般にクライアントは、インスタンスID を独自の永続性のあるストレージに保存し、その ID をすべての呼び出しに対して提供します。ワークフローが終了すると、クライアントは ID を破棄できます。インスタンス ID は、シリアル化可能で同等視可能であることが重要です。サービスではその状態と共に ID を永続性のあるストレージに保存する必要があるため、ID はシリアル化可能である必要があります。サービスがストレージから状態を検索できるようにするため、ID は同等視可能である必要があります。
通常、永続性のあるストレージは、インスタンス ID をインスタンスの状態と対応付ける一種の辞書です。それを使用するサービスは一般に、個々のインスタンス メンバを固有の ID と共に格納するのではなく、1 つのデータ構造を使用してその状態のすべてを格納します。多くの場合、次にサービスは専用のヘルパ クラスまたは構造を使用してそのすべてのメンバ変数を集約し、その型を永続性のあるストレージから 1 回の操作で格納および取得します。複数のインスタンスが同時にストレージにアクセスして変更しようとする可能性があるため、永続性のあるストレージ自体へのアクセスは、スレッド セーフで同期されている必要があります。
単純な永続性のあるサービスの実装とサポートを支援するために、図 1 に示す FileInstanceStore<ID,T> クラスを作成しました。FileInstanceStore<ID,T> は、汎用のファイル ベースのインスタンス ストアであり、2 つの型パラメータを受け取ります。1 つは ID 型パラメータであり、これは制約として一様な型である必要があります。もう 1 つは、インスタンス状態を表す T 型パラメータです。これらの型は両方ともシリアル化可能である必要があります。
図 1 FileInstanceStore
public interface IInstanceStore<ID,T> where ID : IEquatable<ID>
{
   void RemoveInstance(ID instanceId);
   bool ContainsInstance(ID instanceId);
   T this[ID instanceId]
   {get;set;}
}

public class FileInstanceStore<ID,T> : IInstanceStore<ID,T>
                                       where ID : IEquatable<ID>
{

   public FileInstanceStore(string fileName);

   //Rest of the implementation
}
FileInstanceStore<ID,T> は、単純なインデクサを提供し、インスタンス状態をファイルから読み込んだりファイルに書き込んだりできるようにします。また、ファイルにインスタンス状態が存在するかどうかを確認したり、それを削除したりもできます。

明示的なインスタンス ID
クライアントがサービスにインスタンス ID を提供するための最も単純な方法は、状態にアクセスするすべての操作に対してそれぞれ明示的なパラメータを使用することです。そのようなクライアントおよびサービスを図 2 に示します。このサービスは、呼び出しごとに動作するポケット計算機であり、永続性のあるメモリがファイルに格納されます。
図 2 明示的なインスタンス ID による計算機
[ServiceContract]
interface ICalculator
{
   [OperationContract]
   double Add(double number1,double number2);

   /* More arithmetic operations */

   //Memory management operations

   [OperationContract]
   void MemoryStore(string instanceId,double number);

   [OperationContract]
   void MemoryClear(string instanceId);

}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   static IInstanceStore<string,double> Memory = new
     FileInstanceStore<string,double> (Settings.Default.MemoryFileName);

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(string instanceId,double number)
   {
      lock(typeof(MyCalculator))
      {
         Memory[instanceId] = number;
      }
   }
   public void MemoryClear(string instanceId)
   {
      lock(typeof(MyCalculator))
      {
         Memory.RemoveInstance(instanceId);
      }
   }
   //Rest of the implementation
}
計算機のすべてのインスタンスが、FileInstanceStore<string,double> という形式の同じ静的メモリを使用します。計算機は、サービス型に対するロックを使用して、すべてのインスタンスにわたる各操作でメモリへのアクセスを同期します。メモリをクリアするとワークフローの終了が通知されるため、計算機はストレージから状態をパージします。

インスタンス ID に対するコンテキスト バインディング
明示的なパラメータの代わりに、Microsoft .NET Framework 3.5 で導入された専用のコンテキスト バインディングを使用することもできます。これらのバインディング、BasicHttpContextBinding、NetTcpContextBinding、および WSHttpContextBinding は、System.WorkflowServices.dll アセンブリに含まれています。これらの各コンテキスト バインディングは、それぞれ通常のプロトコル固有のバインディングから派生していますが、専用のコンテキスト管理プロトコルに対するサポートを追加します。これらのバインディングは、コンテキストなしでも使用できます。
コンテキスト管理プロトコルを使用すると、(メッセージ ヘッダー内で) キー/値文字列のペアの辞書を渡すことができ、これがコンテキストと呼ばれます。これは、アプリケーション固有の実行コンテキストを表し、組み込みのセキュリティ コンテキストや操作コンテキストと同様なものです。コンテキスト バインディングでは、プロキシを開く (またはそれを初めて使用する) 前に、サービスに渡すコンテキストを 1 回だけ設定できます。その後は、辞書がキャッシュされ、それを変更しようとするとエラーになります。クライアントは、IContextManager インターフェイスを使用して、サービスに送信するコンテキストを設定します。これは次のように定義されます。
public interface IContextManager
{
   IDictionary<string,string> GetContext();
   void SetContext(IDictionary<string,string> context);
   //More members
}
クライアントは、プロキシの InnerChannel プロパティにアクセスすることで IContextManager インターフェイスへの参照を取得します。このプロパティは、GetProperty<T>() メソッドにより IChannel インターフェイスをサポートします。
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
class MyContractClient : ClientBase<IMyContract>,IMyContract
{...}

MyContractClient proxy = new MyContractClient();
IContextManager contextManager =
    proxy.InnerChannel.GetProperty<IContextManager>();
クライアントは、IContextManager を取得すると、GetContext メソッドを呼び出して現在のコンテキストを複製できます。GetContext から返される辞書は実際のコンテキストのコピーであるため、クライアントはそれを使用してコンテキストを変更することはできません。その代わり、クライアントは SetContext メソッドを呼び出し、新しいコンテキストを提供する必要があります。クライアントは古いコンテキストを無効にするか、単にそれに値を追加してから、図 3 に示すように再度コンテキストを設定します。
図 3 プロキシにコンテキストを設定する
MyContractClient proxy = new MyContractClient();
IContextManager contextManager =
    proxy.InnerChannel.GetProperty<IContextManager>();

IDictionary<string,string> context = contextManager.GetContext();
context["NumberContext"] = "123";
contextManager.SetContext(context);

proxy.MyMethod();

proxy.Close();
サービスは、受信したメッセージ プロパティからコンテキスト値を読み取ります。このプロパティには、操作コンテキストを通じてアクセスします。
public sealed class OperationContext : ...
{
   public MessageProperties IncomingMessageProperties
   {
      get;
   }
   //More members
}
MessageProperties は、タイプ セーフではない辞書であり、文字列キーを受け取って、一致するオブジェクト値を返します。
public sealed class MessageProperties : IDictionary<string,object>
{...}
コンテキスト プロパティを取得するために、サービスは静的文字列 ContextMessageProperty.Name を使用します。これは、次のように定義された ContextMessageProperty 型のオブジェクトを返します。
[Serializable]
public class ContextMessageProperty : IMessageProperty
{
   public IDictionary<string,string> Context
   {get;}
   public static string Name
   {get;}

   //More members
}
ContextMessageProperty の Context プロパティは、クライアントから渡されるものと同じパラメータの辞書です。図 3 で渡された数値コンテキストを読み取るために必要なサービス側の手順を図 4 に示します。クライアントがコンテキストに書き込むために必要となる手順は、図 5 に示す ContextManager 静的ヘルパ クラスを使用して合理化できます。
図 4 サービスでコンテキストを読み取る
class MyService : IMyContract
{
   public void MyMethod()
   {
      ContextMessageProperty contextProperty =
          OperationContext.Current
          .IncomingMessageProperties[ContextMessageProperty.Name]
          as ContextMessageProperty;

      string number = contextProperty.Context["NumberContext"];

      Debug.Assert(number == 123);
   }
}
図 5 ContextManager のクライアント側メソッド
public static class ContextManager
{
   public static void SetContext(IClientChannel innerChannel,
                                 string key, string value) {
      SetContext(innerChannel, UpdateContext(innerChannel, key, value));
   }

   public static void SetContext(IClientChannel innerChannel,
                                 IDictionary<string, string> context) {
      Debug.Assert((innerChannel as ICommunicationObject).State !=
                   CommunicationState.Opened);
      IContextManager contextManager =
         innerChannel.GetProperty<IContextManager>();
      contextManager.SetContext(context);
   }

   public static IDictionary<string, string> UpdateContext(
      IClientChannel innerChannel, string key, string value) {
      IContextManager contextManager =
         innerChannel.GetProperty<IContextManager>();
      IDictionary<string, string> context =
         new Dictionary<string, string>(contextManager.GetContext());
      context[key] = value;
      return context;
   }

   //Proxy extensions

   public static void SetContext<T>(this ClientBase<T> proxy,
             IDictionary<string, string> context) where T : class {
      SetContext(proxy.InnerChannel, context);
   }

   public static void SetContext<T>(this ClientBase<T> proxy,
                        string key, string value) where T : class {
      SetContext(proxy.InnerChannel, key, value);
   }

   //More helper methods
}
ContextManager は、SetContext メソッドをオーバーロードしたいくつかのメソッドを提供し、クライアントが 1 つのキー/値ペアを使用して、または辞書からペアのコレクションを使用して、プロキシの内部チャネルに新しいコンテキストを設定できるようにします。これらのメソッドは、プロキシ クラスとチャネル ファクトリの両方に対して有用です。また、ContextManager により、コンテキストをプロキシ クラスの拡張として設定することもできます。CreateContext メソッドを使用して新しい辞書を作成したり、既存のコンテキストにキー/値ペアを追加したりできます。ContextManager を使用すると、図 3 は次のように縮小されます。
MyContractClient proxy = new MyContractClient();
proxy.SetContext("NumberContext","123");
proxy.MyMethod();
proxy.Close();
ただし、このように SetContext を利用するには、プロキシのすべてのインスタンス化時にそれを明示的に使用する必要があります。それよりも、ContextManager を ContextClientBase<T> プロキシ クラスのような専用のプロキシ クラスにカプセル化する方が適切です。
public abstract class ContextClientBase<T> : ClientBase<T>
where T : class
{
   public ContextClientBase();
   public ContextClientBase(string endpointName);
   public ContextClientBase(string key,string value);
   public ContextClientBase(IDictionary<string,string> context);
   public ContextClientBase(string key,string value,
                                           string endpointName);
   public ContextClientBase(IDictionary<string,string> context,
                                           string endpointName);
   //More constructors
}
ContextClientBase<T> のコンストラクタは、エンドポイント名やバンディングおよびアドレスなどの通常のプロキシ パラメータを受け取りますが、それに加えて、サービスに送信するコンテキスト パラメータも受け取ります。これは、1 つのキー/値ペア、または辞書を使用したペアのコレクションです。プロキシは、ContextClientBase<T> から直接派生できます。
class MyContractClient : ContextClientBase<IMyContract>,IMyContract
{
   public MyContractClient(string key,string value) : base(key,value)
   {}
   /* More constructors */
   public void MyMethod()
   {
      Channel.MyMethod();
   }
}
ContextClientBase<T> を使用すると、図 3 は次のように縮小されます。
MyContractClient proxy = new MyContractClient("NumberContext","123");
proxy.MyMethod();
proxy.Close();
図 6 に、ContextClientBase<T> の実装を示します。
図 6 ContextClientBase<T> を実装する
public abstract class ContextClientBase<T> : ClientBase<T>
                                             where T : class
{
   public ContextClientBase(string key,string value,string endpointName):
              this(ContextManager.CreateContext(key,value),endpointName){}

   public ContextClientBase(IDictionary<string,string> context, string endpointName) : base(endpointName){
      SetContext(context);
   }

   /* More constructors */

   void SetContext(IDictionary<string,string> context){
      VerifyContextBinding();
      ContextManager.SetContext(InnerChannel,context);
   }

   void VerifyContextBinding(){
      BindingElementCollection elements = Endpoint.Binding.CreateBindingElements();

      if(elements.Contains(typeof(ContextBindingElement))){
         return;
      }
      throw new InvalidOperationException("Can only use context binding");
   }
}
ContextClientBase<T> のいくつかのコンストラクタは、ContextManager を使用して新しいコンテキストを作成し、それを別のコンストラクタに渡します。渡されたコンストラクタは、SetContext ヘルパ メソッドを呼び出します。SetContext は最初に、使用されているバインディングが本当にコンテキスト バインディングであることを確認してから、ContextManager を使用してコンテキストを設定します。バインディングがコンテキスト プロトコルをサポートすることの確認は、バインディング要素のコレクションに ContextBindingElement が含まれるかどうか検索することで行います。この確認方法は、バインディングの型を調べる方法よりも優れています。カスタム コンテキスト バインディングでも自動的に機能するためです。
サービスに対して、ContextManager ヘルパ クラスは、操作コンテキストとメッセージ プロパティの間のやり取りをカプセル化できます。ContextManager は、GetValue メソッドを提供します。
public static class ContextManager
{
   public static string GetValue(string key);

   //More members
}
GetValue を使用すると、図 4 のサービス コードは次のように縮小されます。
class MyService : IMyContract
{
   public void MyMethod()
   {
      string number = ContextManager.GetValue("NumberContext");

      Debug.Assert(number == "123");
   }
}
いくつかのエラー処理コードを削除した GetValue の実装を図 7 に示します。
図 7 GetValue を実装する
public static class ContextManager
{
   public static string GetValue(string key)
   {
      if(OperationContext.Current == null)
         return null;

      ContextMessageProperty contextProperty =
             OperationContext.Current.IncomingMessageProperties
                                  [ContextMessageProperty.Name]
                                     as ContextMessageProperty;

      if(contextProperty.Context.ContainsKey(key) == false)
         return null;

      return contextProperty.Context[key];
   }
}
GetValue は、状態およびエラー管理を追加することを除いて、図 4 に示した明示的な手順と似ています。コンテキストに要求キーが含まれていない場合 (またはコンテキストがまったく見つからない場合) には、GetValue は null を返します。
クライアントは ContextClientBase<T> を使用して、クライアントのコンテキスト バインディング プロトコル上でインスタンス ID を渡します。コンテキスト バインディングではすべてのコンテキスト パラメータに対してキーと値が必要であるため、クライアントはその両方をプロキシに提供する必要があります。キーの値はあらかじめサービスに知られている必要があるため、クライアントは、キーをプロキシ自体にハード コーディングしてもよいでしょう。サービスは ContextManager を使用した関連する操作で、インスタンス ID を取得します。
図 8 に、コンテキスト バインディングを使用した計算機サービスを示します。この図では、サービスが ContextManager とのやり取りを専用のコンテキスト クラスにカプセル化しています。
図 8 コンテキスト バインディングによるインスタンス ID を使用した計算
//Client Side:
[ServiceContract]
interface ICalculator
{
   [OperationContract]
   double Add(double number1,double number2);

   [OperationContract]
   void MemoryStore(double number);

   [OperationContract]
   void MemoryClear();
}
class MyCalculatorClient : ContextClientBase<ICalculator>,ICalculator
{
  public MyCalculatorClient(string instanceId) :  base("CalculatorId",instanceId)
   {}
  public MyCalculatorClient(string instanceId,string endpointName):
                             base("CalculatorId",instanceId,endpointName)
   {}

   //More constructors

   public double Add(double number1,double number2)
   {
      return Channel.Add(number1,number2);
   }

   public double MemoryStore (double number)
   {
      return Channel.MemoryStore (number);
   }

   //Rest of the implementation
}

//Service Side:
class CalculatorContext
{
   public static string Id
   {
      get
      {
         return ContextManager.GetValue("CalculatorId") ?? String.Empty;
      }
   }
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   static IInstanceStore<string,double> Memory = new
      FileInstanceStore<string,double>(Settings.Default.MemoryFileName);

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(double number)
   {
      lock(typeof(MyCalculator))
      {
         Memory[CalculatorContext.Id] = number;
      }
   }
   public void MemoryClear()
   {
      lock(typeof(MyCalculator))
      {
         Memory.RemoveInstance(CalculatorContext.Id);
      }
   }
   //Rest of the implementation
}

コンテキスト ID に対する標準キー
インスタンス ID に使用されるキーをハード コーディングする必要性と、それをあらかじめ知っておく必要性は、それぞれ義務です。そのため、永続性のあるサービスのディスパッチャ ロジックは、Guid の形 (文字列形式) でインスタンス ID を自動的に作成します。これは、instanceId という名前の予約キーによってアクセスできます。クライアントとサービスは、インスタンス ID として同じ値を取得します。バインディングがクライアントとサービスの間でインスタンス ID を相関付ける機会を得た後、プロキシの最初の呼び出しが戻ると、インスタンス ID の値は初期化されます。コンテキスト バインディングで渡される他のパラメータと同様に、インスタンス ID の値は、プロキシの有効期間全体を通じて不変です。標準インスタンス ID とのやり取りを合理化するため、図 9 に示すように、ContextManager を ID 管理メソッド、プロパティ、およびプロキシ拡張によって拡張しました。
図 9 標準インスタンス ID の管理
public static class ContextManager
{
   public const string InstanceIdKey = "instanceId";

   public static Guid InstanceId
   {
      get
      {
         string id = GetValue(InstanceIdKey) ?? Guid.Empty.ToString();
         return new Guid(id);
      }
   }
   public static Guid GetInstanceId(IClientChannel innerChannel)
   {
      try
      {
        string instanceId = innerChannel.GetProperty<IContextManager>()
         .GetContext()[InstanceIdKey];
         return new Guid(instanceId);
      }
      catch(KeyNotFoundException)
      {
         return Guid.Empty;
      }
   }
   public static void SetInstanceId(IClientChannel innerChannel,
                                                Guid instanceId)
   {
      SetContext(innerChannel,InstanceIdKey,instanceId.ToString());
   }

   //More members
}
ContextManager は、クライアントがコンテキストに対してインスタンス ID を読み取りまたは書き込みできるように、GetInstanceId および SetInstanceId メソッドを提供します。読み取り専用の InstanceId プロパティは、サービスが ID を取得する際に使用されます。ContextManager は、インスタンス ID を文字列ではなく Guid として扱うことでタイプ セーフとし、エラー処理も追加しています。
クライアントは図 8 のように ContextClientBase<T> を使用して標準 ID を渡すことができますが、図 10 に示されるように、それをさらに厳しくし、標準インスタンス ID に対する ContextClientBase<T> の組み込みサポートを提供する方が適切です。図 11 に、標準 ID を使用した永続性のある計算機クライアントおよびサービスを示します。
図 10 標準 ID に対する ContextClientBase<T> のサポート
public abstract class ContextClientBase<T> : ClientBase<T>
                                             where T : class
{
   public Guid InstanceId
   {
      get
      {
         return ContextManager.GetInstanceId(InnerChannel);
      }
   }
   public ContextClientBase(Guid instanceId) :
     this(ContextManager.InstanceIdKey,instanceId.ToString())
   {}

   public ContextClientBase(Guid instanceId,string endpointName) :
     this(ContextManager.InstanceIdKey,instanceId.
                                       ToString(),endpointName)
   {}

   //More constructors
}
図 11 標準 ID を使用した計算機
//Client Side:
class MyCalculatorClient : ContextClientBase<ICalculator>,ICalculator
{
   public MyCalculatorClient()
   {}
   public MyCalculatorClient(Guid instanceIdId) : base(instanceId)
   {}
   public MyCalculatorClient(Guid instanceId,string endpointName) :
       base(instanceId,endpointName)
   {}

   //Rest same as Figure 8
}

//Service Side:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   static IInstanceStore<Guid,double> Memory = new
       FileInstanceStore<Guid,double>(Settings.Default.MemoryFileName);
   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(double number)
   {
      lock(typeof(MyCalculator))
      {
         Memory[ContextManager.InstanceId] = number;
      }
   }
   public void MemoryClear()
   {
      lock(typeof(MyCalculator))
      {
         Memory.RemoveInstance(ContextManager.InstanceId);
      }
   }
   //Rest of the implementation
}

自動的な永続性のある動作
永続性のあるサービスに対してここまでに示したすべての手法では、サービスにかなりの量の作業が要求されます。そのほとんどは、永続性のある状態ストレージを提供し、操作ごとにそのストレージに対してインスタンス状態を明示的に管理するための作業です。この作業は反復的な性質を持つため、WCF では標準コンテキスト ID を使用して、各操作でのサービス状態を指定された状態ストレージとの間でシリアル化および逆シリアル化することにより、作業を自動化できます。クライアントが有効な ID を提供すると、各操作について WCF はインスタンスをストレージから逆シリアル化し、操作を呼び出した後、操作によって変更された新しい状態をストレージに再度シリアル化します。クライアントが ID を提供しない場合、WCF はコンストラクタを呼び出して新しいサービス インスタンスを作成しますが、その後すぐにそのインスタンスを状態ストレージにシリアル化します。
クライアントからプロキシに提供された ID に一致する状態が既にストレージに含まれている場合、WCF はインスタンス コンストラクタを呼び出すことはありません。その代わり、状態ストレージから逆シリアル化された新しいインスタンスに対して呼び出しが処理されます。状態ストレージに含まれない ID がクライアントから提供された場合、WCF は例外をスローします。
WCF は、DurableService 動作属性を提供します。これは次のように定義されます。
public sealed class DurableServiceAttribute :
    Attribute,IServiceBehavior,...
{...}
この属性は、サービス クラス上で直接適用します。最も重要なこととして、サービス クラス自体は Serializable としてマークするか、または、永続性のある状態管理を必要とするすべてのメンバにそれぞれの DataMember 属性を持つデータ コントラクトとして、マークする必要があります。
このインスタンスは、通常のインスタンスの場合と同様に、その状態をメンバ変数で管理でき、図 12 に示すように、WCF がそれらのメンバを管理してくれます。
図 12 DurableService 動作を使用する
[Serializable]
[DurableService]
class MyCalculator : ICalculator
{
   double Memory
   {get;set;}

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(double number)
   {
      Memory = number;
   }
   [DurableOperation(CompletesInstance = true)]
   public void MemoryClear()
   {
      Memory = 0;
  }
   //Rest of the implementation
}
自動的な永続性のある状態管理を利用するすべてのサービスは、セッションごとに構成される必要がありますが、動作は常に呼び出しごとに行われます (WCF は各呼び出し後にコンテキストの非アクティブ化を実行します)。また、サービスはすべてのエンドポイントでコンテキスト バインディングの 1 つを使用して標準インスタンス ID を有効にする必要があります。コントラクトはセッションを許可または要求する必要があり、禁止することはできません。この 2 つの制約は、サービスの読み込み時に確認されます。
サービスはオプションで DurableOperation 操作動作属性を使用して、WCF がワークフローの終了時にストレージから状態をパージするよう指示できます。
[AttributeUsage(AttributeTargets.Method)]
public sealed class DurableOperationAttribute : Attribute,...
{
   public bool CanCreateInstance
   {get;set;}

   public bool CompletesInstance
   {get;set;}
}
CompletesInstance プロパティを true に設定することで、WCF は操作呼び出しの終了後にインスタンス ID をストレージから削除します。CompletesInstance プロパティの既定値は false です。クライアントがインスタンス ID を提供しない場合に備えて、CanCreateInstance プロパティを false に設定することで、操作によって新しいインスタンスが作成されるのを防ぐことができます。図 12 に、計算機の MemoryClear 操作に対して CompletesInstance プロパティを使用する例を示します。
CompletesInstance を利用する際の問題点は、コンテキスト ID が不変であることです。クライアントが、CompletesInstance が true に設定された操作を呼び出した後でプロキシに対して別の呼び出しを実行しようとすると、ストレージにはそのインスタンス ID がもう含まれていないため、呼び出しは失敗します。
クライアントは、プロキシを引き続き使用できないことを認識する必要があります。クライアントがサービスに対してさらに呼び出しを実行する必要がある場合は、まだインスタンス ID を持っていない新しいプロキシで行う必要があります。それにより、クライアントは新しいワークフローを開始します。これを行うには、単にワークフローの完了後にクライアント プログラムを閉じるか、または新しいプロキシ参照を作成します。

永続化プロバイダ
DurableService 属性は WCF に対して、インスタンスをいつシリアル化および逆シリアル化すればよいかを指示しますが、それをどこで行うかについては何も指示しません。その代わり、WCF はプロバイダ モデルという形のブリッジ パターンを使用して、状態ストレージをこの属性とは別に指定できるようにしています。それにより、属性がストレージから切り離されるため、自動的な永続性のある動作を任意の互換性のある記憶媒体と共に使用できます。
サービスが DurableService 属性で構成されている場合は、永続化プロバイダ ファクトリを使用してホストを構成する必要があります。このファクトリは、図 13 に示すように、抽象クラス PersistenceProviderFactory から派生し、抽象クラス PersistenceProvider のサブクラスを作成します。
図 13 PersistenceProviderFactory
public abstract class PersistenceProviderFactory : CommunicationObject
{
   protected PersistenceProviderFactory();
   public abstract PersistenceProvider CreateProvider(Guid id);
}

public abstract class PersistenceProvider : CommunicationObject
{
   protected PersistenceProvider(Guid id);

   public Guid Id
   {get;}

   public abstract object Create(object instance,TimeSpan timeout);
   public abstract void   Delete(object instance,TimeSpan timeout)
   public abstract object Load(TimeSpan timeout);
   public abstract object Update(object instance,TimeSpan timeout);

   //Additional members
}
永続化プロバイダ ファクトリを指定する最も一般的な方法は、それをホストの .config ファイルに (サービス動作として) 指定し、その動作をサービス定義で次のように参照することです。
<behaviors>
   <serviceBehaviors>
      <behavior name = "DurableService">
         <persistenceProvider
            type = "...type...,...assembly ..."
            <!--  Provider-specific parameters  -->
         />
      </behavior>
   </serviceBehaviors>
</behaviors>
永続化プロバイダ ファクトリが指定されていない場合、WCF はサービス ホストの作成を中止します。ホストに永続化プロバイダ ファクトリが指定されると、WCF は作成された PersistenceProvider を呼び出しごとに使用して、インスタンスをシリアル化および逆シリアル化します。単純な永続化プロバイダ ファクトリとして、次に示す FilePersistenceProviderFactory を作成しました。
public class FilePersistenceProviderFactory : PersistenceProviderFactory
{
   public FilePersistenceProviderFactory();
   public FilePersistenceProviderFactory(string fileName);
   public FilePersistenceProviderFactory(
                                        NameValueCollection parameters);
}
public class FilePersistenceProvider : PersistenceProvider
{
   public FilePersistenceProvider(Guid id,string fileName);
}
FilePersistenceProvider は、FileInstanceStore<ID,T> をラップします。FilePersistenceProviderFactory のコンストラクタでは、目的のファイル名を指定する必要があります。ファイル名が指定されないと、FilePersistenceProviderFactory は既定で Instances.bin を使用します。
.config ファイルでカスタム永続化ファクトリを使用する際の鍵は、パラメータの NameValueCollection を受け取るコンストラクタを定義することです。これらのパラメータは、.config ファイルのプロバイダ ファクトリ動作セクションに指定されたキーおよび値のペアを単純なテキスト形式にしたものです。ほとんど任意の自由な形式のキーおよび値が有効です。たとえば、ファイル名が何であるかを指定するには、次のように記述します。
<persistenceProvider type =
      "FilePersistenceProviderFactory,ServiceModelEx"
         fileName = "MyService.bin"
 />
これによりコンストラクタは、パラメータ コレクションを使用してこれらのパラメータにアクセスできます。
string fileName = parameters["fileName"];
WCF には、インスタンス状態を専用の SQL Server テーブルに格納する永続化プロバイダが付属しています。Visual Studio 2008 の既定のインストール後には、データベースのインストール スクリプトが C:\Windows\Microsoft.NET\Framework\v3.5\SQL\EN に格納されています。状態ストレージには SQL Server 2005 または SQL Server 2008 のみを使用できることに注意してください。
SQL プロバイダは、SqlPersistenceProviderFactory および SqlPersistenceProvider の形で用意され、System.ServiceModel.Persistence 名前空間の System.WorkflowServices アセンブリに含まれています。必要なことは、図 14 に示されるように、アプリケーション設定から SQL プロバイダ ファクトリと接続文字列名を指定することだけです。
図 14 SQL プロバイダと接続文字列を指定する
<connectionStrings>
   <add name = "DurableServices"
      connectionString = "..."
      providerName = "System.Data.SqlClient"
   />
</connectionStrings>

<persistenceProvider
   type = "System.ServiceModel.Persistence.SqlPersistenceProviderFactory,
                System.WorkflowServices,Version=3.5.0.0,Culture=neutral,
                 PublicKeyToken=31bf3856ad364e35"
                 connectionStringName = "DurableServices"
/>
serializeAsText パラメータを true に設定することにより、診断や分析の目的などで、(既定のバイナリ シリアル化を使用する代わりに) インスタンスをテキストとしてシリアル化するよう WCF に指示することもできます。
<persistenceProvider
   serializeAsText = "True"
  ...
/>

ご意見やご質問は mmnet30@microsoft.com まで英語でお送りください。

Juval Lowy は、WCF のトレーニングとアーキテクチャ コンサルティングを行う IDesign 社に所属するソフトウェア アーキテクトです。最新の著書は、『Programming WCF Services, Second Edition』です。彼は、シリコン バレー地域の Microsoft Regional Director も務めています。連絡先は www.idesign.net です。


Windows と C++
高性能アルゴリズムについて調べる
Kenny Kerr

同時実行領域では、調整、非同期動作、応答性、スケーラビリティなどの問題が最大の注意点になっています。これらは、アプリケーションのデザイン時に開発者が検討する上位レベルのトピックです。ただし、おそらくは経験不足や優れたパフォーマンス ツールがないために、同じように重要なトピックがしばしば見過ごされます。その適例が高性能アルゴリズムです。
エンタープライズ レベルでは、開発者は分散ファイル システムとキャッシュ、クラスタ、メッセージ キュー、データベースなどの問題について熟考します。しかし、根本的にアルゴリズムとデータ構造が不十分な場合は、それらが何の役に立つでしょうか。
アルゴリズムの効率は、皆さんが考えるほど簡単ではありません。シングル プロセッサ上の適切にデザインされたアルゴリズムは、複数プロセッサ上の不十分な実装よりもパフォーマンスが高い場合がよくあります。しかし、今日の適切にデザインされたアルゴリズムは、複数プロセッサが使用可能な場合にもかなりのスケーラビリティと効率を示す必要があります。問題をさらに複雑にするのは、シングル プロセッサ用に最適化されたアルゴリズムは並列化が難しいことが多く、若干効率の劣るアルゴリズムの方が複数プロセッサ上で高いパフォーマンスを示す可能性があることです。
たとえば、ここでは Visual C++ を使用し、かなり単純なアルゴリズムの開発を紹介しますが、これは一見するほど簡単ではありません。実装する必要のある内容を次に示します。
void MakeGrayscale(BYTE* bitmap,
                   const int width,
                   const int height,
                   const int stride);
bitmap パラメータは、ピクセルあたり 32 ビットで構成されるイメージを指します。これについてもこの記事で注目します。stride の絶対値は、メモリ内の 1 行のピクセルから次の行までのバイト数を示します。各行の終わりには埋め込み文字が存在する場合があります。stride の符号は、これらの行がメモリ内で正のストライドを持つトップダウンであるか、負のストライドを持つボトムアップであるかを示します。
まず、基本事項を見てみましょう。次の構造体を使用して、メモリ内のピクセルを表すことができます。
typedef unsigned char BYTE; // from windef.h

struct Pixel
{
    BYTE Blue;
    BYTE Green;
    BYTE Red;
    BYTE Alpha;
};
簡単な Web 検索で、特定の色ピクセルに 30% の赤、59% の緑、および 11% の青を混合すると合理的なグレー スケール値を取得できることがわかります。次に、1 つのピクセルのみをグレー スケールに変換する単純な関数を示します。
void MakeGrayscale(Pixel& pixel)
{
    const BYTE scale = static_cast<BYTE>(0.30 * pixel.Red +
                                         0.59 * pixel.Green +
                                         0.11 * pixel.Blue);

    pixel.Red = scale;
    pixel.Green = scale;
    pixel.Blue = scale;
}
ビットマップ内の特定のピクセルのバイト オフセットは、水平位置とピクセル サイズの積、および垂直位置とストライドの積を計算し、それらの値を加算することで取得できます。
offset = x * sizeof(Pixel) + y * stride
次に、MakeGrayscale 関数を実装するにはどうすればよいでしょうか。深く考えずに進んだ場合は、図 1 に示すようなアルゴリズム行を作成する可能性があります。一見したところ、このコードは合理的なように思え、小さなビットマップが渡された場合はかなりうまく機能するように見える可能性があります。では、より大きなビットマップについてはどうでしょう。20,000 × 20,000 ピクセルのビットマップについてはどうでしょうか。
図 1 非効率的なシングルスレッド アルゴリズム
void MakeGrayscale(BYTE* bitmap,
                   const int width,
                   const int height,
                   const int stride)
{
    for (int x = 0; x < width; ++x)
    for (int y = 0; y < height; ++y)
    {
        const int offset = x * sizeof(Pixel) + y * stride;

        Pixel& pixel = *reinterpret_cast<Pixel*>(bitmap + offset);

        MakeGrayscale(pixel);
    }
}
筆者は Quad-Core Intel Xeon X3210 プロセッサを搭載した Dell PowerEdge を持っています。このマシンのクロック速度は 2.13 GHz であり、1066 MHz のフロントサイド バス、8 MB の L2 キャッシュ、およびその他さまざまなすばらしい機能を搭載しています。確かに、これは最新の Intel Xeon プロセッサではありませんが、侮れるものではありません。64 ビット バージョンの Windows Server 2008 で駆動され、パフォーマンス テストでは優れた結果を出しています。
その全能力を利用して、図 1 のアルゴリズムを幅 20,000 ピクセル、高さ 20,000 ピクセルのビットマップに対して実行しました。10 回の反復で平均約 46 秒かかりました。確かに、このビットマップはかなり大きく、1.5 GB ほどになります。しかし、それが問題でしょうか。筆者のサーバーには 4 GB の RAM があるため、ディスクにページングされることはありません。しかし、図 2 では、プロセッサ使用状況の見慣れたビューが示されています。
図 2 非効率的なシングルスレッド アルゴリズムのプロセッサ使用状況 (クリックすると拡大画像が表示されます)
アルゴリズムは猛烈に動作しますが、コンピュータの処理能力の 4 分の 1 しか使用していません。この解決方法は明らかです。最も一般的な対応は、問題を解決するためにより多くのスレッドを投入することです。これは合理的な見解です。結局のところ、アルゴリズムは 1 つのスレッドしか使用しないため、テスト システムの 4 つのコアのうち 1 つしか使用されていません。図 3 のコードではスレッドを追加します。
図 3 非効率的なマルチスレッド アルゴリズム
void MakeGrayscale(BYTE* bitmap,
                   const int width,
                   const int height,
                   const int stride) {
    #pragma omp parallel for
    for (int x = 0; x < width; ++x)
    for (int y = 0; y < height; ++y) {
        const int offset = x * sizeof(Pixel) + y * stride;

        Pixel& pixel = *reinterpret_cast<Pixel*>(bitmap + offset);

        MakeGrayscale(pixel);
    }
}
違いは、OpenMP プラグマの追加だけです。外側の for ループはスレッドに分割されるため、並列で実行でき、ビットマップの異なる部分を変換します。OpenMP では、既定でプロセッサあたり 1 つのスレッドが作成されます (OpenMP の使用にこだわる必要はありません。これは for ループを各スレッドに分割する非常に簡単な方法であるというだけです。コンパイラとランタイムに応じて、同じ結果を実現するための手法が他にもあります)。図 4 に示されるように、このわずかな変更によって大きな違いが現れます。
図 4 非効率的なマルチスレッド アルゴリズムのプロセッサ使用状況 (クリックすると拡大画像が表示されます)
パフォーマンス向上のリソース
ここで説明しているトピックの詳細については、次のリソースを参照してください。

  • 局所性の最大化、競合の最小化
  • CPU キャッシュ
  • OpenMP と C++: まったく作業せずにマルチスレッドを利用する
  • Quad-Core Intel Xeon X3210
  • 色をグレースケールに変換する
しかし結果は理想的とは言えません。シングルスレッド バージョンの 46 秒に比べて、マルチスレッド アルゴリズムでは約 30 秒かかっています。34% のパフォーマンス向上は、処理コストの 300% の増加に値するとはとうてい思えません。ここで、効率的な実装では、(同じテスト システムで) シングルスレッドのみ使用すると約 2 秒で完了し、複数スレッドを利用すれば約 0.5 秒で完了すると言ったらどうでしょう。これは 30 秒よりもはるかに高速です。
思っているよりも簡単です。アルゴリズムを実行している物理ハードウェアと、メモリ内でのデータの編成方法について考えるだけです。開発者は、コードを最終的に実行するハードウェアに対して無関心になりがちです。効率的なアルゴリズムについて説明し、それがはるかに優れている理由を説明します。
図 5 を見てください。違いは、for ループの順序だけであることに驚かれるかもしれません。つまり、外側のループが Y 軸に対して反復し、内側のループが X 軸に対して反復するように図 3 のアルゴリズムを変更しました。このように一見些細な変更で、時間が 30 秒から 1 秒未満に短縮されるのはなぜでしょうか。その答えは、アルゴリズムが操作対象のデータの形状と局所性、およびハードウェアの特性によって深刻な影響を受ける可能性があるという事実に基づきます。
図 5 効率的なアルゴリズム
void MakeGrayscale(BYTE* bitmap,
                   const int width,
                   const int height,
                   const int stride) {
    #pragma omp parallel for
    for (int y = 0; y < height; ++y)
    for (int x = 0; x < width; ++x) {
        const int offset = x * sizeof(Pixel) + y * stride;

        Pixel& pixel = *reinterpret_cast<Pixel*>(bitmap + offset);

        MakeGrayscale(pixel);
    }
}
これを理解するために、アルゴリズムの操作対象のデータ構造を再検証する必要があります。ビットマップのメモリ バッファは、論理的にピクセルの行を含みます。明らかに、メモリは表形式ではなく、特定の座標のピクセルを簡単かつ効率的にアドレス指定する方法が必要です。行について考えると、これを概念化するのに役立ちます。ただし、各行の先頭を適切に揃えるために必要な埋め込み文字がこれらの行に含まれる場合、特に、インデックス付きのカラー テーブルを持つビットマップを使用している場合には、この抽象化によって、その物理的な実装が明らかになります (図 6 を参照)。
図 6 行と埋め込み文字 (ボトムアップ) (クリックすると拡大画像が表示されます)
Y 軸の方向は、負のストライドで示されるボトムアップ レイアウトを示します。理論的には、この方向がパフォーマンスに影響する可能性もありますが、非常に優れたツール サポートがないと測定は困難です。
優れた同時実行の経験則によると、離れた位置にあるメモリはシングル プロセッサでまとめて使用するべきではなく、近くにあるメモリは異なるプロセッサで使用するべきではありません。つまり、共に使用するデータは近くに保持し、別々に使用されるデータは十分に離れた位置に保持します。このようにすると、ハードウェア キャッシュ マネージャは、競合を発生させずに、異なるプロセッサによって使用されるデータをキャッシュできます。
メモリ レイアウトがパフォーマンスにどのように影響するかを把握するには、メモリが表形式ではなく連続的であることを覚えておく必要があります。仮想アドレス空間はフラットです。図 7 に、図 3 の非効率的なアルゴリズムを使用するとビットマップがどのように分割されるかを示します。
図 7 非効率的なデータ分割 (クリックすると拡大画像が表示されます)
これで、このアルゴリズムのパフォーマンスが非常に悪い理由が明らかになりました。ループ実行の依存性はありませんが、アルゴリズムはハードウェア キャッシュ レベルでの競合がほぼ確実に発生するような方法で並列化されています。大きなビットマップでは競合が低減する可能性がありますが、反復的なキャッシュ ミスにより、キャッシュ マネージャはキャッシュ ラインを絶えず再読み込みせざるを得ないため、パフォーマンスに影響します。シングル プロセッサ上でも、キャッシュ ライン内のデータを絶えず置換する必要があるため、パフォーマンスが非常に劣悪になります。図 8 に、その局所性の欠陥をより明確に示します。パターンは、ビットマップ内の行と同じ数だけ繰り返されます。
図 8 非効率的なデータ分割 (反復) (クリックすると拡大画像が表示されます)
対照的に、図 5 の効率的なアルゴリズムは、ビットマップを Y 軸に沿って分割するため、各プロセッサは、近くにあるピクセルのストリームを処理し、離れた場所にあるピクセルは他のプロセッサによって処理されます。このため、キャッシュ マネージャは、競合を発生させずにプロセッサ間でビットマップを共有できます (図 9 および 10 を参照)。特に、図 10 は、各プロセスが単一の連続メモリ ブロックを処理し、検索とキャッシングのパフォーマンスが大幅に向上することを示しています。
図 9 効率的なデータ分割 (クリックすると拡大画像が表示されます)
図 10 効率的なデータ分割 (反復なし) (クリックすると拡大画像が表示されます)
前述の 0.5 秒の目標に達することは、既定で OpenMP により使用される静的スケジューリングでは困難です。OpenMP は、アルゴリズムからこれらの最後の数ミリ秒を搾り出すために使用できる動的なガイド付きスケジューリングも提供します。基本的な問題は、コンピュータはバックグラウンドでいくつかの小さなタスクを実行しているため、作業を使用可能なプロセッサ間で完全に分割しても、最適な結果が得られない可能性があるということです。これにより、スレッドの一部が他のスレッドよりも前に完了し、遅いスレッドの完了を待機している間アイドル状態になる場合があります。
一方、別のアルゴリズムやデータ構造を使用して、これをより高速にすることができます。ビットマップ パーティションを Non-uniform Memory Access (NUMA) システムの個々のノードの物理メモリに読み込むことができます。Windows Server ハイ パフォーマンス コンピューティング (HPC) グリッドの別のノードに送信することもできます。非効率的なアルゴリズムでは、このいずれも不可能です。ポイントは、データ構造とアルゴリズムについて、また実行するハードウェアによってパフォーマンスがどのような影響を受けるかについて少々考えることにより、パフォーマンスを劇的に改善できるということです。

ご意見やご質問は mmwincpp@microsoft.com まで英語でお送りください。

Kenny Kerr は、Windows 向けのソフトウェア開発を専門にしているソフトウェア設計者です。彼はプログラミングおよびソフトウェア設計に関して執筆を行い、開発者を指導しています。連絡先は weblogs.asp.net/kennykerr です。


いろいろな場所へ
Tablet PC 向けのインク対応アプリケーション
Gus Class
目次
Tablet PC での書き込みと認識
InkEdit と InkPicture
InkEdit コントロールを使用する
インク コントロールを Visual Studio に追加する
InkEdit コントロールを追加する
InkPicture コントロールを使用する
描画属性を変更する
まとめ
全体的に見ると、Tablet PC でのインクのキャプチャと分析のプロセスは単純です。マネージ コードを使用したオブジェクトの作成方法とアクセス方法には一貫性があり、しかも単純です。そのため、マネージ コードを使用すると、Tablet PC 向けのマネージ アプリケーションを簡単に開発することができます。経験豊かな Windows 開発者にとって、そのようなアプリケーションをアンマネージ コードで作成するのはそれほどたいへんなことではありません。ただし、十分な知識のない開発者にとっては、アンマネージ Tablet PC アプリケーションの作成作業はハードルの高いものとなる可能性があります。
ペン コンピューティングの開発者にとって、ペンから入力を受け取り、それを有意なデータに変換する処理はありふれたものです。しかし、ときには厄介な作業になることもあります。Tablet PC の場合、このプロセスに頭を悩ませる必要はありません。設計のオーバーヘッドを軽減するためにマイクロソフトが設計した、抽象化アーキテクチャを備えたペン コンピューティング システムがあるからです。
このコラムでは、Tablet PC のインク アーキテクチャを簡単に紹介し、Tablet PC アプリケーションでのインクのキャプチャ方法と認識方法について説明します。また、Tablet PC SDK を使用して手書き文字をテキストとして認識する方法と、単純なペンベースの図の入力を作成する方法を示したサンプル コードも提供します。

Tablet PC での書き込みと認識
Tablet PC の世界では、ペンから PC への入力は "インク" と呼ばれています。インクは、他の種類のデータ (数字、テキスト、および文字) に簡単に変換できるほか、そのままの形が適している場合にはインクとして残しておくこともできるため、便利な形式の入力データだと言えます。入力をインクとして保存する方がよいのは、図の交じった走り書きのメモなどの場合です。
インクは、x 座標、y 座標、ペンの状態など、あらゆる種類の高度な情報を含むパケットで構成されます。インクには、幅、色、透明度の値などの属性を共有するストローク コレクションも含まれます。図 1 は、Tablet PC 向けのペン入力 (インク) の高度な設計を示しています。
図 1 インク データ コンポーネント (クリックすると拡大画像が表示されます)
ストローク データの読み取りは、それ以外の入力ストリームの読み取りとよく似ています。ストロークは、OS のフックによるキャプチャ後、描画または認識できます。マイクロソフトは、先ほど述べた種類のデータを表示、描画、および認識するためのインターフェイス、クラス、およびマネージ オブジェクトを実装しています。これらのオブジェクトは、当初は Windows XP の Tablet PC SDK を通じて開発者に配布されていましたが、現在は Windows Vista SDK に含まれています。
インクの認識プロセス (ペン入力をテキストに変換するプロセス) は Tablet PC SDK に組み込まれています。インクを認識するには、インクをキャプチャし、インクを認識できるオブジェクトにそのインクを渡します。インクのキャプチャには、InkCollector または派生オブジェクト (InkOverlay など) を使用します。InkCollector または InkOverlay がキャプチャしたパケットは、ストロークに変換されます。その後で、それらのストロークを取得できます。さらに、ストロークを認識エンジンに渡し、キャプチャしたストロークを分析するためのメソッドを呼び出すことで、インク データを認識済みのテキストに変換します。図 2 に、インクの認識プロセスを示します。
インク分析の詳細については、Tablet PC SDK の記事 (msdn.microsoft.com/library/ms812486) を参照してください。Tablet PC 開発用のリソースとして、モバイル PC デベロッパー センター (msdn.microsoft.com/mobilepc) も非常に役立ちます。

InkEdit と InkPicture
アンマネージ コードを使用する開発作業で Tablet PC SDK の強力な機能を活用する最も簡単な方法は、InkEdit および InkPicture ActiveX コントロールを使用することです。これらのコントロールを使用すると、通常は Tablet PC オブジェクトを使用して行うさまざまな作業が簡略化されます。たとえば、InkEdit コントロールは、InkRecognition の処理を最適な方法で自動的に実行します。したがって、このようなコントロールを自分で作成する場合よりも早いペースで Tablet PC の開発を進めることができます。
コントロールごとに機能が異なるので、目的に応じて使い分けてください。これらのコントロールの違いを図 3 にまとめました。InkEdit コントロールでは手書き認識を行えますが、このコントロールは RichEdit コントロールに近い機能を備えています。InkEdit は Tablet PC を対象としたアプリケーションに統合できます。InkEdit が組み込まれているアプリケーションでは、ユーザーは Tablet PC 入力パネルを表示し、いつもと同じように手書き認識機能を利用できます。最終的にテキストに変換される入力を取得するアプリケーションを作成する場合は、InkEdit コントロールを使用することをお勧めします。
図 3 InkEdit と InkPicture
機能 | InkEdit | InkPicture
インクのキャプチャ | ○ | ○
インクの認識 | ○ | ×
インクの外観の変更 | × | ○
インクの消去 | × | ○
テキストの編集 (文字ごと) | ○ | ×
インクからテキストへの変換 | ○ | ×
テキストのジェスチャ | ○ | ×
フォント サイズの変更 | ○ | ×
イメージの処理 | × | ○
InkPicture コントロールでは図を入力できますが、インクの認識は自動的には行われません。ペンから描画の入力を受け取るアプリケーションを作成する場合は、InkPicture コントロールを使用することをお勧めします。ユーザーが入力したインクをそのままの形で保持する場合は、このコントロールが便利です。

InkEdit コントロールを使用する
InkEdit コントロールの機能とカスタマイズ方法を紹介するために、まず InkEdit コントロールをアプリケーションに追加し、ボタンを使用して認識を実行する方法について説明します。以下の説明を読むと、私が必要最低限の機能を備えた MFC アプリケーションとダイアログ ボックスを作成したときの手順をたどることができます。まず、Visual Studio で新しい MFC アプリケーション プロジェクトを作成します。プロジェクト名を "Simple Recognizer" (または任意の名前) に変更してください。
[OK] をクリックし、MFC アプリケーション ウィザードを表示します。このウィザードで、プロジェクトの設定をすばやく確認することができます。特に変更は必要ありません。
[アプリケーションの種類] ページで、アプリケーションの種類をダイアログに設定する必要があります。[次へ] をクリックし、[User Interface Features] (ユーザー インターフェイスの機能) ページを表示します。[User Interface Features] (ユーザー インターフェイスの機能) ページですべてのチェック ボックスをオフにします。
[次へ] をクリックし、[高度な機能] ページを表示します。このページで高度な機能の設定を変更する必要はありません。[次へ] をクリックし、設定を確認してから、[完了] をクリックしてプロジェクトを作成してください。
プロジェクトの [リソース] タブには、作成中のダイアログ アプリケーションの UI 情報が格納された .rc ファイルが表示されます。それをダブルクリックし、アプリケーションを実行したときの外観を表示します。[フォーム] ですべてのテキストとボタンを削除します。
ツールボックスから、ボタン (認識エンジンのトリガ)、グループ ボックス、および何らかの静的テキストを新しいダイアログ ボックスにドラッグします (図 4 を参照)。グループ ボックスの ID を IDC_PLACEHOLDER に変更します。このボックス (およびその関連の hWnd) の上にインク コントロールを配置する必要があるためです。以上で、Visual Studio でタブレット コントロールを設定し、コードを動作させる準備が整いました。
図 4 DESCRIPTION_GOES_HERE (クリックすると拡大画像が表示されます)

インク コントロールを Visual Studio に追加する
既存のインク コントロールを使用すると、非常に簡単にインク サポートをアプリケーションに追加することができます。ActiveX コントロール (InkEdit および InkPicture) は、Windows XP Tablet PC Edition だけでなく、Windows Vista (Home Premium 以上のバージョン) にも含まれています。コントロールをインストールし、Visual Studio で有効にすると、それらのコントロールをアプリケーションに簡単にドロップできるようになります。
まず、Tablet PC SDK または Windows Vista SDK をダウンロードしてインストールします。インストールの完了後、Visual Studio を再起動します。先ほど作成したプロジェクトを再度開き、.rc ファイルをダブルクリックしてリソース エディタに戻ります。そのファイルを開くと、アプリケーションの設計に使用できるさまざまなコンポーネントがツールボックスの領域に表示されます。この領域を右クリックし、[アイテムの選択] をクリックします。
[ツールボックス アイテムの選択] ウィンドウが表示されます。[COM コンポーネント] タブを選択し、下方向にスクロールして [Microsoft InkEdit Control] と [Microsoft InkPicture Control] を選択します。有効にしたコントロールはツールボックスに表示され、アプリケーションに追加できるようになります。

InkEdit コントロールを追加する
認識エンジンを使用するには、InkEdit コントロールをツールボックスからアプリケーションにドラッグし、グループ コントロール内に配置します。グループ コントロールの Caption プロパティを更新し、InkEdit 領域が書き込み用の領域であることがわかるようにします。また、InkEdit コントロールのサイズを変更し、その領域に収まるようにします。このアプリケーションでは、グループ コントロールが InkEdit コントロールとスペースを共有しないようにしてください。また、グループ ボックス コントロールを削除、再追加することで、コントロールの順序を誤って変えてしまわないように注意してください。InkEdit コントロールが最上位のコントロールである必要があります。
アプリケーションをビルドして実行します。図 5 に、まだ認識されていないテキストと、私が以前作成したアプリケーションで認識されたテキストを示します。
図 5 InkEdit ではテキストの認識が可能 (クリックすると拡大画像が表示されます)
InkEdit コントロールには自動タイムアウトの機能があり、そのタイムアウト時間を過ぎるとインクを認識します。そのため、インクの認識のために必要な作業は、ここまでのところコントロールをアプリケーションのダイアログ ボックスにドラッグ アンド ドロップすることだけです。次に、コードを追加して動作をカスタマイズします。
InkEdit コントロールの既定のタイムアウト時間に基づいてテキストを自動認識する代わりに、[Recognize] (認識) ボタンのクリック時に認識が行われるようにするには、アプリケーションの実行時に InkEdit の更新のタイムアウト時間を変更し、[Recognize] (認識) ボタンが押されたときにインクの認識をトリガする必要があります。これは便利な方法です。InkEdit コントロールは ActiveX コントロールなので、SendMessage メソッドを使用し、ActiveX コントロールをホストするウィンドウにこの機能を指定できるためです。
まず、次のコードをアプリケーションの OnInitDialog 関数に追加します。
// Controls are now initialized; set control settings.
CWnd* pInkWnd = NULL;
pInkWnd = GetDlgItem(IDC_INKEDIT3);
if (pInkWnd != NULL){
  // Set the timeout to a large number.
  // LPARAM specifies the timeout in millisecs.
  // WPARMA must be left at 0.
  LRESULT result  =
    pInkWnd->SendMessage(EM_SETRECOTIMEOUT, 1000000042, 0);
}else{
  // Indicate that an error occurred.
  ERROR_MSG(L'ERROR: Could not specify recognizer timeout');
}
続いて、次のコードを [Recognize] (認識) ボタンのボタンクリック ハンドラに追加します。最も簡単にボタンクリック ハンドラを追加する方法は、リソース エディタのリソース ビューでボタンをダブルクリックすることです。この操作を行うと、作成したボタンのコード セクションに移動します。
void Csimplest_recognizerDlg::OnBnClickedButton1() {
  CWnd* pInkWnd = NULL;
  pInkWnd = this->GetDlgItem(IDC_INKEDIT3);
  if (pInkWnd != NULL){
    // Trigger recognition within the InkEdit control.
    LRESULT result  = pInkWnd->SendMessage(EM_RECOGNIZE, 0, 0);
  }
}
ここでアプリケーションをビルドして実行すると、InkEdit コントロールが手書きのテキストを自動的には認識しないことがわかります。認識を実行するには、[Recognize] (認識) ボタンをクリックする必要があります。

InkPicture コントロールを使用する
InkPicture コントロールを使用すると、アプリケーション設計者は、画像として処理する入力をペンから受け取るアプリケーションを作成できます。この場合、ユーザーはコントロールの領域でオブジェクトを描画できます。また、それらのオブジェクトはコントロールで解釈または処理できます。この例では、ペンを使用したストロークの入力や、それらのストロークの削除と選択が可能な、非常にシンプルなアプリケーションの作成手順を紹介します (図 6 を参照)。
図 6 InkPicture を使用する (クリックすると拡大画像が表示されます)
ここでは、アプリケーションを起動すると自動的にインク モードになり、ユーザーがそのまま描画領域に書き込むことができるようにします。また、[Eraser] (消しゴム) ボタンをクリックすることで描画されているラインを消去したり、選択ボタンをクリックして選択モードを有効にし、描画されているオブジェクトを選択したりできるようにします。
テキスト認識アプリケーションをセットアップしたときと同じ方法で、このアプリケーションのダイアログを作成します。先ほどのアプリケーションとの違いは、InkEdit コントロールの代わりに InkPicture コントロールを使用し、3 つのボタンをアプリケーション ダイアログの下部に追加するという点だけです。白色の領域が InkPicture コントロールです。[Pen] (ペン)、[Eraser] (消しゴム)、または [Selection] (選択) ボタンをクリックしてみると、各ボタンがまだ機能していないことがわかります。また、ペンを切り替えても消しゴム機能は有効になりません。
そこで、InkPicture コントロールの状態をボタンで変更できるようにする必要があります。そのためには、変更するプロパティを特定しなければなりません。その後で、コントロールに関連付けられている CWnd を取得することでプロパティを設定し、SetProperty メソッドを使用してコントロールの状態を変更します。このプロセスを簡素化するために、InkOverlayEditingMode 列挙体から値を取得する setEditingMode という名前の関数を作成します (図 7 を参照)。
図 7 setEditingMode
void CInkPaintDlg::setEditingMode(short mode){
  CComPtr<IInkPicture> ip;

  // Retrieve the DISPID by using a COM Client for the InkPicture  // control.
  DISPID dispid = 0;
  OLECHAR FAR* szMember = OLESTR('EditingMode');
  ip.CoCreateInstance(CLSID_InkPicture);
  ip->GetIDsOfNames(IID_IInkPicture, &szMember, 1,
    LOCALE_SYSTEM_DEFAULT, &dispid);

  // Retrieve the CWnd for the InkPicture Control.
  CWnd* pInkWnd = NULL;
  pInkWnd = this->GetDlgItem(IDC_INKPICTURE1);

  pInkWnd->SetProperty(dispid, VT_I2, mode);
  ip.Release();
}
編集モードを設定する関数を作成したら、後は編集モードをさまざまな状態に変更するボタンを追加するだけです。リソース デザイナで作成したボタンをダブルクリックし、ハンドラを追加してモードを適宜設定します。次のコードは、ボタンクリック ハンドラを使用して InkPicture コントロールの編集モードを適切な状態に変更する方法を示しています。
// Pen mode
void CInkPaintDlg::OnBnClickedButton1() {
  setEditingMode(InkOverlayEditingMode::IOEM_Ink);
}
// Eraser mode
void CInkPaintDlg::OnBnClickedButton2() {
  setEditingMode(InkOverlayEditingMode::IOEM_Delete);
}
// Selection mode
void CInkPaintDlg::OnBnClickedButton3() {
  setEditingMode(InkOverlayEditingMode::IOEM_Select);
}
以上で、プログラムによる消去と選択が可能になりました。ユーザーは、ペン ツールを使用したストロークの描画以外の新しい操作を行うことができます。たとえば、消しゴム モードを選択するとラインを消去でき、選択ツールを選択するとストロークを選択できます。図 8 に、サイズを変更するコンテンツを選択したところを示します。
図 8 InkPicture でストロークのサイズを変更する (クリックすると拡大画像が表示されます)
プログラムの実行後に、ストローク消去モードではなくポイント消去モードで消しゴムを使用できるようにすることもできます。私は、Boolean 値を受け取ってストローク消去モードまたはポイント消去モードを有効にする関数を追加しました (図 9 を参照)。
図 9 消去モードを有効にする
void CInkPaintDlg::setPointEraseMode(bool setToPointMode){
  CComPtr<IInkPicture> ip;

  // Retrieve dispid.
  DISPID dispid = 0;
  OLECHAR FAR* szMember = OLESTR('EraserMode');
  ip.CoCreateInstance(CLSID_InkPicture);
  ip->GetIDsOfNames(IID_IInkPicture, &szMember, 1,
    LOCALE_SYSTEM_DEFAULT, &dispid);

  // Retrieve the CWnd for the InkPicture control.
  CWnd* pInkWnd = NULL;
  pInkWnd = this->GetDlgItem(IDC_INKPICTURE1);

  // Toggle the property based on the point mode.
  if (setToPointMode){
    pInkWnd->SetProperty(dispid, VT_I2,
      InkOverlayEraserMode::IOERM_PointErase);
  }else{
    pInkWnd->SetProperty(dispid, VT_I2,
      InkOverlayEraserMode::IOERM_StrokeErase);
  }

  ip.Release();
}
この関数は、ダイアログ アプリケーションの追加の初期化コードで呼び出されます。
BOOL CInkPaintDlg::OnInitDialog() {
  // TODO: Add extra initialization here.
  this->setPointEraseMode(true);
  ...
}
これで、消しゴムのモードはポイント消去モードに設定されました。ユーザーは、ライン全体ではなく図の一部分だけを消去できます。完全な機能を備えたアプリケーションでは、これらの設定をすべてコンテキスト メニューで切り替えられるようにしておくと、使い勝手が向上します。

描画属性を変更する
これでアプリケーションは機能するようになり、ユーザーがコンテンツを選択、消去、および描画できるようになりました。今度はその機能を拡張し、ペンの幅と色を変更できるようにしてみましょう。まず、サポートするさまざまな色 (赤、青、緑、黒など) に対応するボタンをいくつかアプリケーションに追加します。次に、ストロークの幅を変更するためのボタンを 1 つか 2 つ追加します。こうした処理を簡素化するために、私は setStrokeThickness と setColor という 2 つのユーティリティ関数を作成しました。setStrokeThickness のコードを図 10 に示します。
図 10 setStrokeThickness
void CInkPaintDlg::setStrokeThickness(bool isThick){

  CComPtr<IInkPicture> ip;
  CComPtr<IInkDrawingAttributes> spIInkDrawAttrs = NULL;

  // Set to thick or thin based on bool.
  // 50 is the default width.
  // 250 is a good thickness.
  float newWidth= isThick ? 250 : 50;

  DISPID dispid = 0;
  OLECHAR FAR* szMember = OLESTR('DefaultDrawingAttributes');

  ip.CoCreateInstance(CLSID_InkPicture);
  ip->GetIDsOfNames(IID_IInkPicture, &szMember, 1,     LOCALE_SYSTEM_DEFAULT, &dispid);

  CWnd* pInkWnd = NULL;
  pInkWnd = this->GetDlgItem(IDC_INKPICTURE1);

  // Retrieve the drawing attributes interface.
  pInkWnd->GetProperty(dispid, VT_UNKNOWN, &spIInkDrawAttrs);

  // Now use the interface to set the width.
  spIInkDrawAttrs->put_Width(newWidth);

  // Now pass the interface back to set the drawing attributes.
  pInkWnd->SetProperty(dispid, VT_DISPATCH, spIInkDrawAttrs);

  // Free.
  spIInkDrawAttrs.Release();
  ip.Release();
}
これらのユーティリティ関数には、インク コントロールのプロパティを設定する他の関数と大きく異なっている点が 1 つあります。この例で見てみると、親インターフェイスのプロパティを変更する代わりに、それ自体がプロパティである子インターフェイスを取得し、そのインターフェイスを使用して変更を行っています。以上で、ユーティリティ関数を通じてストロークの色と太さを簡単に変更できるようになりました。
// RED
void CInkPaintDlg::OnBnClickedButton4() {
  COLORREF color = RGB(255,0,0);
  setColor(color);
}

// Set thin strokes.
void CInkPaintDlg::OnBnClickedButton8() {
  this->setStrokeThickness(false);
}
図 11 に、さまざまな色と太さのストロークが表示されている完成したアプリケーションを示します。
図 11 カスタマイズした色とラインの太さ (クリックすると拡大画像が表示されます)

まとめ
アンマネージの C++、InkEdit コントロール、および InkPicture コントロールを使用した、MFC での Tablet PC 対応アプリケーションの作成作業は、いたって簡単です。これらのコントロールの機能を活用すると、最も一般的なタブレットベースの機能を備えたアプリケーションを強化することもできます。通常、コントロールは、何も手を加えなくてもお望みのとおりに動作します。コントロールの動作を変更する必要がある場合は、コントロールにメッセージを送信するか、コントロールの該当するプロパティを取得および設定してください。Tablet API の詳細については、MSDN ライブラリ (msdn.microsoft.com/library/ms950406) を参照してください。
このコラムを執筆するにあたって技術的に支援してくださった、Jason Groce、Eliot Graff の両氏に感謝いたします。

ご意見やご質問は goplaces@microsoft.com まで英語でお送りください。

Gus "gclassy" Class は、マイクロソフトのプログラミング関連のライター兼エバンジェリストです。マイクロソフトでは、Tablet PC、Windows メール、および DRM システムに関する業務に携わってきました。彼は、ブログ (gclassy.com) で開発者の課題を取り上げ、プログラミングの例を紹介しています。


.NET の問題
偽共有
Stephen Toub、Igor Ostrovsky、Huseyin Yildiz
目次
すべてはハードウェアの問題
子供のころの教えとは違い、共有することは良いこととは限らない
偽共有の検出方法
Visual Studio プロファイラを使用する
偽共有は理論上だけの存在か
偽共有のリスクが生じるメモリ ロケーション
連続したメモリ領域の割り当てを避ける
よほど人里を離れた生活でもしていない限り、"メニーコア シフト" という言葉を耳にしたことがあるでしょう。Intel や AMD などのプロセッサ製造元は、クロック速度をやみくもに向上させるのをやめて、プロセッサのコアの数を増やす方向でハードウェアの処理能力を向上させるようになりました。この変化により、ソフトウェア開発者は、処理能力の大幅な向上を活かすために並列処理を意識してアプリケーションを作成するようになりました。
この問題に対処するため、Microsoft .NET Framework の Parallel Extensions、PPL (Parallel Pattern Library)、CCR (Concurrency & Coordination Runtime)、Intel の TBB (Threading Building Block) など、並列処理に関するさまざまなライブラリや言語が登場しています。これらのライブラリはいずれも、Parallel.For や AsParallel などの構造体を提供することにより、効率的な並列アプリケーションを作成するために必要な定型コードの量を減らすことを目的としています。これらの構造体は並列処理の表現方法を進歩させる画期的な機能ではありますが、依然としてコードの内容と構造、およびアプリケーションのパフォーマンスに対するハードウェアの影響の大きさを開発者が意識する必要があるという難点が残ります。
ソフトウェア業界は、並列処理の新しいプログラミング モデルの開発において長足の進歩を遂げましたが、並列処理に関連する問題を完全に解消するプログラミング モデルはいまだ登場する兆しがありません。そのため、当面は、効率的な並列処理プログラムを作成するには、メモリとキャッシュのしくみを理解しておくことが重要です。

すべてはハードウェアの問題
アプリケーションの下位レベルで行われている処理を理解するという概念は何も新しいものではありません。パフォーマンスを最適化するには、メモリ アクセスなどの要素がアプリケーションのパフォーマンスに与える影響を十分に理解している必要があります。
メモリからの読み取りや書き込みについて話をするとき、私たちは、最近のハードウェアではコンピュータのメモリ バンクから直接読み書きを行うことはまれだという事実を方便にしがちです。メモリ アクセスには時間がかかり、ハード ディスクやネットワーク リソースへのアクセスと比べるとはるかに高速ですが、算術計算と比べると桁違いに低速です。
メモリ アクセスに時間がかかることを考慮して、最近の多くのプロセッサではメモリ キャッシュを使用してアプリケーションのパフォーマンスを向上させています。キャッシュには複数のレベルがあり、最近の一般的なコンシューマ用コンピュータでは最低でも L1 と L2 という 2 つのレベルに分かれており、コンピュータによってはさらに多くのレベルに分かれています。L1 は最速ですが、高価であるため、通常のコンピュータの L1 は小容量です (この記事の作成に使用したラップトップの L1 キャッシュは 128 KB です)。L2 は L1 より低速ですが、そのぶん価格が低いため、L2 の方が大容量です (前述のラップトップの L2 キャッシュは 2 MB です)。
メモリのデータが読み取られる場合、要求されたデータおよびそれに関連するデータ (キャッシュ ライン) はメモリからキャッシュに読み込まれ、プログラムはそのキャッシュからデータを取得します。バイト単位ではなく、キャッシュ ライン全体を読み込むことにより、アプリケーションのパフォーマンスは著しく向上します。筆者のラップトップでは、L1 と L2 のキャッシュ ライン サイズは 64 バイトです。アプリケーションではメモリのデータを数バイトずつ順番に読み取る処理を頻繁に行います (配列などへのアクセスで一般的な処理)。この場合、キャッシュ ラインの一連のデータを読み込むことにより、読み取ろうとしているデータが既にキャッシュに読み込まれている可能性があるため、アプリケーションは要求のたびにメイン メモリにアクセスする必要がありません。ただし、キャッシュを最大限に活かすには、開発者がアプリケーションのメモリ アクセス方法を理解している必要があります。
図 1 の C# プログラムについて考えてみましょう。ここでは、2 次元配列を作成し、この配列に 2 とおりの方法で書き込みを行っています。最初のコード セグメント (コメントに Faster と記述されている箇所) では、1 行ごとにループ処理し、各行の処理で 1 列ごとのループ処理を行っています。2 番目のコード セグメント (コメントに Slower と記述されている箇所) では、1 列ごとにループ処理し、各列の処理で 1 行ごとのループ処理を行っています。コメントが示すとおり、違いは for ループの順序が入れ替わっただけであるにもかかわらず、テストに使用したラップトップでは、Faster バージョンの処理速度は Slower バージョンの 2 倍高速でした。
図 1 メモリ アクセスのパターンが重要
using System;
using System.Diagnostics;

class Program {
  public static void Main() {
    const int SIZE = 10000;
    int[,] matrix = new int[SIZE, SIZE];

    while (true) {
      // Faster
      Stopwatch sw = Stopwatch.StartNew();
      for (int row = 0; row < SIZE; row++) {
        for (int column = 0; column < SIZE; column++) {
          matrix[row, column] = (row * SIZE) + column;
        }
      }
      Console.WriteLine(sw.Elapsed);

      // Slower
      sw = Stopwatch.StartNew();
      for (int column = 0; column < SIZE; column++) {
        for (int row = 0; row < SIZE; row++) {
          matrix[row, column] = (row * SIZE) + column;
        }
      }
      Console.WriteLine(sw.Elapsed);

      Console.WriteLine("=================");
      Console.ReadLine();
    }
  }
}
このようなパフォーマンスの違いは、メモリのマトリックス配列の構成方法が原因で生じます。配列は、メモリ内に行優先順で連続して保存されます。これは、マトリックスの各行がメモリ内で線状に順次レイアウトされるイメージです。アプリケーションが配列の値にアクセスする場合、関連するキャッシュ ラインには配列の値と要求された配列値に隣接する値 (多くの場合、同一行の直前または直後の値) が格納されます。そのため、行から次の値を取り出す処理が、別の行の値を取り出す場合よりはるかに高速になります。Slower バージョンでは、行間をジャンプして、常に異なる行の値にアクセスし、各行の同一列の値を取り出します。この現象の実例については、今月号の MSDN Magazine の Kenny Kerr のコラム (msdn.microsoft.com/magazine/cc850829) を参照してください。

子供のころの教えとは違い、共有することは良いこととは限らない
並列処理を行う場合、この問題がより明確になります。並列処理では、1 個のプロセッサがメモリのデータにアクセスし、キャッシュを利用する方法を認識するだけでなく、複数のスレッドまたはプロセッサがデータにアクセスする方法を認識する必要があります。
最近の多くのシステムでは、一部のキャッシュはコア間で共有されないようになっています。キャッシュにはメイン メモリからのデータのコピーが保持されるため、キャッシュの内容をメイン メモリや他のキャッシュと同期するメカニズムをプロセッサに実装する必要があります。このようなメカニズムを "キャッシュ コヒレンシー" プロトコルまたは "メモリ コヒレンシー" プロトコルと呼びます。
最近の多くの Intel プロセッサで使用されているプロトコルは MESI と呼ばれます。MESI とは、キャッシュ ラインの 4 つの状態 (Modified、Exclusive、Shared および Invalid) を表しています。MESI プロトコルでは、1 つのキャッシュがシステム内の他のキャッシュの動作を監視し、一貫性を維持するための適切な処理を行います。たとえば、変更されたデータが別のキャッシュの読み取り操作で要求されたときに、そのデータをメイン メモリにフラッシュするなどの処理が行われます。これらの操作には、最低でもメモリの読み取りおよび書き込みが必要であるため、前述したように高コストになる可能性があります。
データの共有がいつ発生するかは必ずしも明確でないため、問題は一見するより複雑です。真の共有は、2 つのスレッドが、キャッシュ コヒレンシー プロトコルによって適切に一貫性が保たれるメモリ ロケーションへのアクセスを要求したときに発生します。ところが、2 つのスレッドが異なるデータ セットにアクセスするときに、キャッシュ ラインのサイズなどのハードウェア アーキテクチャの詳細が原因で、キャッシュ コヒレンシー プロトコルではこれらのデータ セットで共有が発生しているように見えるだけの場合があり、このような場合には開発者はそれを認識できません。この現象を偽共有と呼び (図 2 を参照)、実際の並列処理アプリケーションに重大なパフォーマンスの問題をもたらす可能性があります。
図 2 キャッシュ コヒレンシーとキャッシュ ライン (クリックすると拡大画像が表示されます)
図 3 に示すコードについて考えてみましょう。次のコードでは、System.Random のインスタンスをプロセッサごとに 1 つずつ割り当てます。次に、Parallel Extensions に含まれる Task Parallel Library の Parallel.For 構造体を使用して、複数の反復処理を並列して実行します。各反復処理で 1 つの Random インスタンスを操作します。ここでは、Random インスタンスで Next を連続して呼び出します。
図 3 偽共有の例
static void WithFalseSharing() {
  int iterations = Environment.ProcessorCount;

  Random[] rands = new Random[iterations];
  for (int i = 0; i < iterations; i++) rands[i] = new Random();

  Parallel.For(0, iterations, i => {
    Random r = rands[i];
    DoWork(r);
  });
}

static void DoWork(Random rand) {
  for (int i = 0; i < WORKAMOUNT; i++) rand.Next();
}
この例では、共有が行われている場所が明確でなくなる可能性があります。アプリケーションは rands 配列から読み取りを行っており、同じキャッシュ ラインを 2 つの異なるキャッシュに配置して読み取る場合には、これでまったく問題ありません (MESI プロトコルでは、2 つのキャッシュ ラインが Shared (共有) 状態になります)。問題は Random インスタンス自体にあります。Random の各インスタンスには、擬似乱数値のシーケンスの次の値を生成するための状態が格納されます。Next を呼び出すたびに次の値が計算され、内部状態が更新されます。ソース コードの観点からはループ内で使用されるすべてのスレッドが独立していても、これらの Random インスタンスは、割り当て方法によっては、インスタンス間の偽共有が発生するほどメモリ内の互いに近い位置に割り当てられる可能性があります。
偽共有の問題に対処する方法はいくつかありますが、いずれの方法でも Random インスタンスをメモリ内の互いに十分離れた位置に配置し、同じキャッシュ ラインに出現しないようにします。.NET では、メモリ割り当ての詳細はランタイムによって制御されるため、何らかの回避策が必要な場合があります。
例として、図 4 のコードは、この問題が生じないバージョンを示しています。ここでは、使用する Random インスタンス間に多数の余分な Random インスタンスを割り当てて、(少なくとも筆者が使用したコンピュータ上では) 使用するインスタンス間に偽共有が生じないだけの十分な間隔が空くようにします。テストの結果、この更新バージョンでは結果が大幅に改善され、デュアルコアのテスト コンピュータで図 3 のコードを実行した場合より 6 倍高速になりました。
図 4 偽共有の例の修正
static void WithoutFalseSharing() {
  const int BUFFER = 64;
  int iterations = Environment.ProcessorCount * BUFFER;

  Random[] rands = new Random[iterations];
  for (int i = 0; i < iterations; i++) rands[i] = new Random();

  Parallel.For(0, iterations, BUFFER, i => {
    Random r = rands[i];
    DoWork(r);
  });
}

static void DoWork(Random rand) {
  for (int i = 0; i < WORKAMOUNT; i++) rand.Next();
}

偽共有の検出方法
偽共有は、パフォーマンスの問題として特定できるほど十分に独立した指標がないため、発見されにくいという問題がありますが、比較によって偽共有の有無を確認し、コード内の該当箇所を絞り込むために役立つプロファイリング メトリックスがあります。何よりも、開発者は偽共有の影響が疑われる場合にその可能性を認識し、並列処理コードのメモリ アクセス パターンを意識する必要があります。
並列処理のコード セグメントがパフォーマンスのボトルネックになっていると特定された場合を考えてみましょう。このコードが次のような状態にある場合には、共有されたキャッシュ ラインの影響に的を絞りたいところです。
  • CPU およびメモリへの依存度が比較的高く、I/O やブロッキング OS 呼び出しは少ない。
  • より高性能なハードウェア上で実行し、並列処理のレベルが高まっても、あまり処理能力が上がらない。
  • 処理量やメモリ アクセス回数が同一で、移動パターンが異なる別の入力データを使用した場合に、パフォーマンスが著しく低下することがある。
偽共有の影響を特定するために筆者が発見した最大のヒントは、CPU のパフォーマンス カウンタにありました。パフォーマンス カウンタは、CPU 自体から得られるハードウェア レベルの統計であり、CPU の各種サブシステムのパフォーマンスに関する情報を提供します。提供される情報には、L2 キャッシュ ミスの数や分岐予測ミスの数などがあります。これらのカウンタの種類はプロセッサ モデルによって異なる可能性がありますが、多くのプロファイラで公開される一般的なサブセットもあります。次に、いくつかの例 (L2 キャッシュ ミス、完了した命令数、プロセッサが停止されていないサイクル) を挙げて説明します。
L2 キャッシュ ミスは、L2 キャッシュからの読み取りの結果、メイン メモリから読み込まれた回数を示します。L2 ミスは、要求されたデータが L2 キャッシュに存在しない場合、または対応するキャッシュ ラインが別のプロセッサから更新されたために無効になっている場合に発生します。後者のミスは偽共有の場合に発生することが多いため、この問題の診断では、L2 キャッシュ ミス カウンタの過度なスパイクを監視する必要があります。
CPI (Cycles Per Instruction) は、アルゴリズム ベンチマークの全体的なパフォーマンス特性を把握するためによく使われる統計です。CPI は、一定期間のアルゴリズムで CPU が 1 命令の実行に要する平均クロック サイクルを示します。CPI は単独の CPU パフォーマンス カウンタとして公開されてはいませんが、Instructions Retired カウンタ (完了した命令数を示すカウンタ) および Non Halted Cycles カウンタ (プロセッサが停止されていないサイクルを示すカウンタ) から得ることができます。
このメトリックはハードウェア (CPU のモデルとクロック速度、キャッシュ アーキテクチャ、メモリ速度など) への依存性が高く、CPI の数字は通常これらのハードウェア特性に沿って評価され、特定のハードウェアに関するアルゴリズムのパフォーマンス効率を調べることができます。
偽共有の診断では、CPU が実際の命令実行に要するクロック サイクル以外に、CPI の測定にメモリ アクセスの遅延やプロセッサ間キャッシュ同期のオーバーヘッド (どちらの場合も CPU がアイドル状態になり、貴重なクロック サイクルが無駄になります) の影響が含まれているという事実も利用できます。他の条件が等しければ、偽共有が存在した場合はプロセッサ間通信のオーバーヘッドとメイン メモリへのアクセスが相当に増加するため、これらの指標は今回の目的に適うと考えられます。偽共有が存在する状態で同じ操作を実行すれば、CPI の数値は高くなります。
もう 1 つ注目すべき点は、L2 キャッシュ ミス カウンタと異なり、CPI メトリックは実行時間から独立しているということです。そのため、CPI を使用して、期間が異なる実行結果を比較できます。

Visual Studio プロファイラを使用する
Visual Studio では、L2 ミスをサンプリング ベースおよびインストルメンテーション ベースのプロファイリング モードで追跡できますが、CPI は 2 つの CPU カウンタを使用するため、インストルメンテーション ベースでプロファイリングする必要があります。
L2 ミスをサンプリング モードでプロファイリングするには、まず、使用するプロジェクトのサンプリング ベースのパフォーマンス セッションを Visual Studio で作成します。次に、セッションのプロパティを使用してサンプリング イベントをパフォーマンス カウンタに変更し、[Portable Events] (ポータブル イベント) の [メモリ イベント] カテゴリで [L2 Misses] (L2 ミス) を選択するか、[Platform Events] (プラットフォーム イベント) の [L2 キャッシュ] カテゴリで [L2 Lines In] (L2 ラインイン) を選択します。プロセッサによっては、[L2 Misses] (L2 ミス) がサポートされていない場合があります。
これでアプリケーションのプロファイリングを開始できます。数値は、1M の L2 ミス イベントごとに取得したプロファイリング サンプルに基づいて報告されます。そのため、排他のサンプルで並べ替えると、L2 ミスが最も多く発生した関数が先頭になります。
L2 ミスおよび CPI をインストルメンテーション モードでプロファイリングするには、インストルメンテーション ベースのパフォーマンス セッションを起動します。このセッションのプロパティで CPU カウンタの収集を有効にし、対象の CPU カウンタを選択します。ここでは、前述の [L2 Cache Misses] (L2 キャッシュ ミス) イベントと、[Portable Events] (ポータブル イベント) の [全般] カテゴリで [Instructions Retired] (完了した命令) および [Non Halted Cycles] (停止されていないサイクル) を選択します。
プロファイラで生成されるレポートには、これらの CPU カウンタの包括的測定値と排他的測定値、および通常の経過時間に関連するメトリックスを示す複数の新しい列が追加されます。ここで最も有用な情報は、各カウンタの関数ごとの排他的な値です。
関数がアクセスしたメモリ範囲が広ければ (特に L2 キャッシュ サイズと同等以上の広い範囲にアクセスした場合には)、L2 ミスが多く発生するのは当然です。ところが、メモリの狭い範囲にしかアクセスしていないことがわかっている関数の L2 ミスが多い場合は、キャッシュ ラインの共有が原因だと疑う十分な根拠があります。この単純なテストは、インストルメンテーション ベースおよびサンプリング ベースのどちらのプロファイリング データにも適用可能です。ただし、どのような結果が理想的であるかはわかりにくいため、同一のアルゴリズムの設定や並列処理レベルをさまざまに変えて実行結果を相互参照するなど、比較を使用する方法が最善です。
より高度な方法を試みることもできます。インストルメンテーション プロファイリング モードの場合、特定の関数で発生した L2 ミスの正確な数が [L2 Misses Exclusive] (排他的 L2 ミス) 列に表示されます。この関数から参照されているおおよそのメモリ容量の見当が付く場合は、その値を使用して、L2 ミスをメモリ読み取りの推定総容量と比較することもできます。ただし、この方法では、キャッシュ ライン サイズや使用している CPU カウンタの動作など、アーキテクチャの詳細に十分な注意を払う必要があります。
例として、テスト プロファイリング セッション用に収集された L2 ミスの統計を図 5 に示します。テスト対象のアプリケーションでは、メモリ内の合計サイズが約 100 KB の構造体配列のスキャンと更新を繰り返す 4 つのスレッドを実行します。テストに使用するコンピュータには、4 つの CPU (デュアル ソケット、デュアル コア) と合計 4 MB の L2 キャッシュが搭載されています。
図 5 テスト セッションの L2 ミスの統計
テスト シナリオ | メモリの読み取り総容量 | 参照される一意メモリ アドレスのサイズ | ホット スポット関数の "排他的な L2 ラインイン"
偽共有がある場合 (4 バイト間隔でアクセス) | ～ 1 GB | 100 KB | 310,000,000
偽共有が軽減された場合 (16 バイト間隔でアクセス) | ～ 1 GB | 100 KB | 95,000,000
偽共有がない場合 (十分に離れている場合) | ～ 1 GB | 100 KB | 240,000
この例の場合、アーキテクチャの詳細を検討して正確な計算を行わなくても、L2 統計から偽共有のケースを特定できます。読み取りの多くの部分で L2 ミスが発生していることが明らかです。アプリケーションが参照しているのはわずか 100 KB の一意メモリで、L2 キャッシュに十分な余裕があるにもかかわらず、報告された L2 ラインインの数は参照している総メモリに近似しています。
場合によっては、短い関数の頻繁な呼び出しによりインストルメンテーション プロファイリングに少なからぬオーバーヘッドが生じてランタイムの動作が不安定になり、プロファイリングの結果も不安定になることがあります。このような場合には、サンプリング ベースのプロファイリングで L2 統計を収集する方が賢明です。図 3 および 4 の例は、Random.Next の呼び出し回数が多いためにこのケースに属します。この例をサンプリング モードでプロファイルした結果、図 6 のようになりました。図 5 ほど極端な結果ではありませんが、ここで偽共有を示唆する現象は、L2 ミスの件数が Random.Next の呼び出しの相当部分 (3.5M/40M) を占めているということです。これは、各スレッドの観点からすると、参照されたデータがメモリ内で完全に静的なままであることがわかっているにもかかわらず、キャッシュが再読み込みされていることを示します。図 6 に示す修正版のサンプリング結果がこの主張を裏付けています。
図 6 Random.Next の例の L2 ミスの統計
テスト シナリオ | Random.Next の総呼び出し回数 | "排他的な L2 ラインイン" のトップ ヒット (L2 イベント 1,000 件あたりに 1 回のサンプリング ヒットを構成した場合)
偽共有がある場合 (図 3) | 40M (4 × 10M) | System.Random.InternalSample -- 3,525 Program.DoWork -- 1,828 System.Random.Next -- 271 [mscorwks.dll] -- 161 [ntdll.dll] -- 78
偽共有がない場合 (図 4) | 40M (4 × 10M) | [mscorwks.dll] -- 101 [ntdll.dll] -- 76 System.Random.InternalSample -- 53
特定の関数の CPI メトリックは、Exclusive Non Halted Cycles (排他的なプロセッサが停止されていないサイクル) と Exclusive Instructions Retired (排他的な完了した命令数) の 2 つのパフォーマンス カウンタの比率で計算できます。この場合、1 回の実行でプロファイルできるのは 1 つのカウンタのみであるため、2 つの独立したサンプリング ベース プロファイリング セッションを使用する必要があります。
前述したように、アルゴリズムの CPI は、実行に使用するハードウェアに高度に依存します。そのため、比較する場合には、同様のハードウェアのみを使用することが重要です。このメトリックの 1 つの使い方として、同じアルゴリズムの並列処理レベルをさまざまに変えて複数の実行を比較する方法があります。CPI は実行時間とは独立した指標であるため、並列処理レベルを低くしたときの実行時間の相違により L2 統計を比較するのが難しい場合には、この方法が役立ちます。
同一の入力データで、使用する CPU の数を減らして実行したときに、同じアルゴリズムで CPI の値が著しく小さくなる場合は、並列処理レベルを高くするとアルゴリズムのハードウェアの使用効率が低くなることを意味しています。計算処理が多いコードに CPI メトリックが集中し、ソフトウェアの同期の影響が除外された場合は、ハードウェアの効率低下の原因がキャッシュの影響である可能性が高いことを示しています。

偽共有は理論上だけの存在か
偽共有の問題の説明はやや難解で、実用性がないと思われるかもしれません。ところが、この問題は現実的なものであり、驚くほど日常的に出現します。
偽共有を考慮せずに並列処理アプリケーションを作成し、パフォーマンス テストで偽共有の問題を検出することも可能ですが、一般には、この問題をアプリケーション開発の設計および実装の段階で念頭に置いておくことが望まれます。並列処理アプリケーションの作成に頭を悩ませるときには、パフォーマンスの問題が気になることでしょう。複数のコアで適切にスケール アップできるアプリケーションの設計を完了したと思ったら、偽共有のせいで並列処理による高速化が相殺されてしまったというのでは落胆ものです。
また、偽共有の問題はテストで簡単に捕捉できるとは限りません。偽共有の動作には非確定的な部分があり、キャッシュ ライン サイズなどの相違があるためにハードウェアによって動作が異なるうえ、コードに無害な変更を加えた場合にもまったく動作が変わる可能性があります。実際、シングル スレッド アプリケーションでアプリケーションのパフォーマンスを向上させるために実装する多くの最適化が、マルチスレッド アプリケーションでは偽共有の問題を悪化させる要因になる可能性があります。高いパフォーマンスが要求されるアプリケーションを開発する場合、偽共有の問題を早期に検討することが最終的な成果につながります。

偽共有のリスクが生じるメモリ ロケーション
偽共有が問題になるのは、偽共有により生じる非効率性が処理時間を左右する場合のみです。キャッシュされた値がたまに不必要に無効化される程度であれば、全体的な実行時間に及ぼす影響はたいてい無視できます。ところが、スレッドが特定のメモリ ロケーションの読み取りおよび書き込みに多くの時間を費やす場合に、偽共有が原因で、そのメモリ ロケーションのキャッシュされたコピーが別のスレッドによって無効化される状態が続くときには、偽共有の問題を解決することによりアプリケーションのパフォーマンスが大幅に向上する可能性があります。
局所性の原則により、多くのプログラムでは、相互に近接するメモリ ロケーションへのアクセスに多くの時間を費やします。たとえば、複数のスレッドが連携して 1 つの結果を算出する一般的な並列処理の計算について考えてみましょう。この場合、各スレッドが計算した中間結果を結合して最終結果が求められます。各スレッドが中間結果の読み取りおよび書き込みに多くの時間を費やし、複数のスレッドが計算した中間結果がメモリ内の近いロケーションに置かれ、同一のキャッシュ ラインに存在する場合、偽共有はアルゴリズムのパフォーマンスに大きく影響します。
このパターンに属する単純な例として、配列内の整数の合計を並列処理で計算する場合が挙げられます。各スレッドはそれぞれ配列の 1 セクションを処理し、セクション内の各要素をアキュムレータに加算します。複数のスレッドからのアキュムレータが同じキャッシュ ラインに存在する場合、スレッドは相互のキャッシュの上書きに多くの時間を費やし、アプリケーションの実行がキャッシュ レベルでシリアル化されることになるため、パフォーマンスが低下する可能性があります。
より複雑な実例としては、PLINQ (Parallel LINQ) プロジェクトを操作している場合に発生する偽共有の問題が挙げられます。PLINQ は、コンピュータ上で使用可能なすべてのコアにクエリの評価処理を分散するという点を除いて、LINQ-to-Objects と同様の LINQ プロバイダです。詳細は省きますが、PLINQ では一連の列挙子 (IEnumerator<T> インターフェイスを実装するクラス) を使用して並列処理を表現することができます。反復処理の中で、各列挙子は入力のパーティションを確認して対応する出力を計算します。スレッドごとに異なる列挙子が処理を進め、その結果、複数のスレッド間で処理が分散されます。スレッドのスケジューリングに複数のコアを使用可能な場合、並列処理によって高速化を達成できる場合があります。
ところが、このシナリオには偽共有が生じやすい性質があります。複数のスレッドでは、それぞれが固有の列挙子でフィールドの読み取りや変更を行います。各列挙子がメモリ内の近い位置に存在すると、列挙子を作成してスレッドに渡す (前述の Random の例のような) 単純な実装の場合などは、並列処理による高速化が偽共有によって大幅に損なわれます。

連続したメモリ領域の割り当てを避ける
.NET メモリ アロケータによってメモリ レイアウトの詳細の大部分は隠蔽されていますが、多くの場合、メモリ レイアウトの一定のプロパティは推測がつき、中には確実にわかるプロパティもあります。次のことが想定されます。
  • 同じクラス インスタンスの 2 つのインスタンス フィールドのメモリ ロケーションは近い。
  • 同じ型の 2 つの静的フィールドのメモリ ロケーションは近い。
  • 配列内のインデックスが隣接する 2 つの要素のメモリ ロケーションは近い。
  • 連続して割り当てられたオブジェクトのメモリ ロケーションは近接している可能性が高く、時間が経つにつれて近づく場合がある。
  • クロージャで共に使用されるローカル変数は多くの場合インスタンス フィールドでキャプチャされるため、前述の説明から、メモリ ロケーションが近くなる傾向がある。
上記以外の場合には、2 つのメモリ ロケーションは遠く離れている可能性が高いと考えられます。たとえば、異なるスレッドによって割り当てられたオブジェクトのメモリ ロケーションは離れている傾向があります。
スレッドが 1 つのオブジェクトを割り当て、次に複数の中規模および大規模のオブジェクトを割り当て、その後に別のオブジェクトを割り当てた場合、最初のオブジェクトと最後のオブジェクトのメモリ ロケーションは両者の間に割り当てられたオブジェクトの合計分だけ離れている可能性があります。この動作は保証されてはいませんが、多くの場合、実際に発生します。ただし、中間のオブジェクトが大きすぎる場合、まったく別のヒープに格納される可能性があり、その場合には最初のオブジェクトと最後のオブジェクトのメモリ ロケーションは近接することがあります。
2 つのオブジェクトに未使用フィールドを埋め込み、その合計サイズが最低でもキャッシュ ラインのサイズと同じである場合、その 2 つのオブジェクトは別のキャッシュ ラインに配置されます。埋め込みフィールドはメモリの最後に配置されます。メモリ内のオブジェクトの配置順がわかっている場合は、最初のオブジェクトだけを埋め込めば十分です。
これらの前提に基づいて並列処理アプリケーションを設計すれば、後からデバッグが必要になるような偽共有の問題を極力少なくすることができます。
将来のプログラミング モデルでは、このような複雑さがユーザーに対して隠蔽されることが期待されますが、当面は、効率的な並列処理プログラムを作成するには、メモリとキャッシュのしくみを理解しておくことが重要です。

ご意見やご質問は、netqa@microsoft.com まで英語でお送りください。

Stephen Toub は、マイクロソフトの並列コンピューティング プラットフォーム チームのシニア プログラム マネージャ リードです。また、MSDN Magazine の寄稿編集者でもあります。

Igor Ostrovsky は、マイクロソフトの並列コンピューティング プラットフォーム チームのソフトウェア開発エンジニアであり、PLINQ (Parallel LINQ) の主力開発者です。

Huseyin Yildiz は、マイクロソフトの並列コンピューティング プラットフォーム チームのソフトウェア開発エンジニアであり、TPL (Task Parallel Library) に携わっています。


{ End Bracket }
恐れずに処理内容を破棄する
Josh Phillips

私たちは、"メニーコア シフト" についてよく話し合い、それが私たちに及ぼす影響とその対処方法について意見を交わしています。しかし、より充実したクライアント エクスペリエンスを実現するためにメニーコアをどのように活用できるか、という点に話題が及ぶと、話をそらしてしまいがちです。4 つのコア、64 個のコア、そして 1,000 個のコアを、私たちはどのように扱えばよいのでしょうか。
処理が速ければ速いほどアルゴリズムの魅力が高まるような特定の領域では、メニーコアは手放しで歓迎できます。しかし、それ以外の領域については、もう少し考えてみる必要があります。確かに、自然言語クエリやジェスチャなどの直感的なユーザー インターフェイシングは、より充実したクライアント エクスペリエンスを提供するうえで非常に役立ちます。しかし、これらのテクノロジは研究に時間がかかり、実装も困難です。では、クライアント アプリケーションの質を高めるために、他にどのような方法があるのでしょうか。一般的な UI の応答性の向上は脇に置いておいて、過激な提案をしたいと思います。その提案とは、処理内容を破棄することです。
どういう意味でしょうか。苦労して手に入れた CPU 時間を昨日の新聞のように捨ててしまうことを、本当に提案しているのだと思われますか。ええ、そのとおりです。より速いシングル コアが追い求められていた時代には、CPU 時間は非常に重要でした。CPU 時間を浪費すると、より重要性の高い処理が妨げられるためです。マルチコア コンピュータ、特にメニーコア コンピュータでは、豊富な処理リソースを使用して、重要な処理と "それほど重要ではないが意味のある処理" の両方を行うことができます。ここから、より魅力的なアプリケーションを開発するチャンスが生まれます。今や、計算を投機的に行えるようになりました。つまり、ユーザーが次に何をするのかを予測し、結果を事前に計算し、必要なときにそれを提供できるようなったのです。
写真の画像をイラストや鉛筆画のような外観に変換するフィルタを備えた、一般的な写真編集スイートを想像してください。フィルタを適用することにしてウィンドウを開くと、各フィルタが適用されたサンプル写真が表示されます。そのボックスをスクロールし、目的のフィルタを探します。それをクリックすると、アプリケーションによる処理が進みます。そのとき、少しずつ変換が行われているようすがわかります。その処理をしばらく待った後、あまり良くないと判断した場合は、元に戻します。そしてフィルタ ボックスに戻り、同じ手順を繰り返します。
この同じシナリオをもう一度考えてみましょう。今度は 128 個のコアで投機的な計算を実行してみます。フィルタ ウィンドウを開いたときには、表示されているすべてのフィルタが画像のサムネイルに既に適用されています。右へスクロールすると、フィルタが適用されたプレビューが次々に表示されます。さらに、現在表示されているフィルタを使用して、元のサイズの画像の変換も開始されています。そのため、フィルタをクリックしたときには、画像の変換は終わっています。このアプリケーションでは、フィルタが選択された後に、さまざまなパラメータを使用して画像を投機的に処理することもできます。もう一歩進めて、カメラを接続する場合を想像してください。あなたが休暇の写真に目を通している間に、コンピュータによってトリミング、赤目の補正、および自動調整が既に行われています。コンピュータによる処理が気に入らなければ、処理を止め、処理内容を破棄し、未編集の写真に戻すことができます。
投機的なコンピューティングは、写真編集アプリケーションに限定されません。車に乗り込む場面を想像してください。4 つのコアを備えたナビゲーション システムは、曜日、時刻、運転記録、Bluetooth 接続した携帯電話のカレンダーやアドレス帳に基づいて、あなたが選びそうな行き先を判断できます。ナビゲーション システムは、そのような行き先を特定するとき、毎回ルートを事前に計算し、交通量のデータを取得します。また、ガソリンが少なくなっていることを検出した場合は、最寄りの最も安いガソリン スタンドを記憶します。あなたが単に地元の食料品店に急いでいることが判明した場合には、計算内容を破棄できます。
ここで重要なのは、今や私たちは、CPU 時間の各サイクルに純金並みの価値があるという考えを捨て去ることができるという点です。もうすぐ、より魅力的なアプリケーションを作り上げるために、少しばかり CPU 時間を浪費することもいとわなくなるような、潤沢な処理能力を手に入れられるはずです。
何も 64 コア プロセッサの登場まで待つ必要はありません。クアッド コア プロセッサは既に販売されており、8 コア プロセッサの登場も間近なため、今日から処理内容を破棄することができます。マイクロソフトは、並列コンピューティング イニシアティブを通じて、難しいスレッド モデルを省略し、マルチコアおよびメニーコア プログラミングの簡略化に努めています。現在、Parallel Extensions to the Microsoft .NET Framework の CTP (Community Technology Preview) 版が提供されています。このプログラミング モデルは、タスク並列処理、データ並列処理、調整、キャンセル API など、投機的コンピューティングに非常に役立ちます。Parallel Extensions (およびネイティブ スタック向けの同様のテクノロジ) とツールは、次期バージョンの Visual Studio に搭載される予定です。
以上のような理由から、投機的な計算を行うことをお勧めします。ユーザーがつまらない作業をしなくて済むように、ユーザーへの提案を行うことが重要です。軽快、魅力的、革新的と三拍子揃った UI を提供してください。すばらしい体験を提供するためなら、処理時間をいくらか浪費することなど恐れてはなりません。

Josh Phillips は、マイクロソフトの並列コンピューティング プラットフォーム チームのプログラム マネージャです。開発者が生産的な方法で同時実行を活用し、機能が豊富で魅力的な次世代アプリケーションを開発できるようにするためのライブラリ、言語、およびツールの開発に携わっています。