MSDN マガジン > Home > 発行物 > 2007 > October >  WPF のスレッド: Dispatcher を使用して応答性の高いアプリケーションを構築する
WPF のスレッド
Dispatcher を使用して応答性の高いアプリケーションを構築する
Shawn Wildermuth

この記事の内容 : :
  • WPF におけるスレッド
  • Dispatcher を使用する
  • UI 以外のスレッド処理
  • タイマを使用する
この記事は次のテクノロジを使用しています:
.NET Framework 3.0、WIndows Presentation Foundation
直感的かつ自然で見栄えもするインターフェイスを何か月もかけて作成したのに、ユーザーが応答を待たされる間、共有デスクを指でコツコツたたいてばかりいるとしたら、非常に残念なことです。実行時間の長いプロセスのせいでアプリケーションが固まってしまうのを眺めるのは、まったくの苦痛です。しかし、応答性の高いアプリケーションを作成するには、注意深い計画が必要です。つまり通常は、実行時間の長いプロセスを別のスレッドで動作させ、UI スレッドはユーザーのために空けておきます。
筆者にとって、応答性に関する最初の実体験は、Visual C++® と MFC で初めて記述したグリッドにまでさかのぼります。当時、複雑な処方に含まれるすべての薬物を表示するような、薬局用アプリケーションの作成を手伝っていました。問題は、30,000 種類の薬物があったことです。そこで、応答性が高く見えるように、最初の画面に収まるだけの薬物を UI スレッドで処理し (所要時間は約 50 ミリ秒)、次にバックグラウンド スレッドを使用して表示外の薬物の処理を完了することにしました (所用時間は約 10 秒)。このプロジェクトは成功し、ユーザーの感じ方が事実より大事な場合もあるという貴重な教訓となったのです。
Windows® Presentation Foundation (WPF) は魅力的なユーザー インターフェイスの作成に適したテクノロジですが、だからと言って、アプリケーションの応答性を考慮する必要がないわけではありません。実行時間の長いプロセスには、データベースからの大規模な結果取得、非同期 Web サービスの呼び出し、その他多数の潜在的に作業負荷が大きい操作などがありますが、その種類にかかわらず言えるのは、アプリケーションの応答性を高めることが、長い目で見て必ずユーザーの満足度向上につながるということです。しかし、WPF アプリケーションで非同期プログラミング モデルを使い始める前に、WPF のスレッド モデルを理解しておくことが重要です。この記事では、このスレッド モデルを紹介するだけでなく、Dispatcher ベースのオブジェクトの動作や、BackgroundWorker の使用方法について説明し、魅力的かつ応答性の高いユーザー インターフェイスを作成できるようにします。

スレッド モデル
すべての WPF アプリケーションは、レンダリング用とユーザー インターフェイス管理用の、2 つの重要なスレッドで開始されます。レンダリング スレッドは、バックグラウンドで実行される非表示のスレッドです。このため、通常、開発者が扱うのは UI スレッドだけです。WPF では、ほとんどのオブジェクトが UI スレッドに関連付けられています。これはスレッドの関係と呼ばれ、WPF オブジェクトは、そのオブジェクトを作成したスレッド上でのみ使用できます。他のスレッドで使用すると、ランタイム例外がスローされます。WPF スレッド モデルは、Win32® ベースの API との相互運用性に優れています。このため、HWND ベースの任意の API (Windows フォーム、Visual Basic®、MFC、Win32) を WPF でホストすることも、WPF をこれらの API でホストすることもできます。
スレッドの関係は、WPF アプリケーション用の優先順位付きメッセージ ループである Dispatcher クラスによって処理されます。通常、WPF プロジェクトには単一の Dispatcher オブジェクト (したがって単一の UI スレッド) があり、あらゆるユーザー インターフェイス作業がこれを通じて行われます。
一般的なメッセージ ループとは異なり、WPF に送信される各作業項目は、優先順位が付けられて Dispatcher 経由で送信されます。このため、項目を優先順位によって順序付けすることも、システムがその処理に時間を割けるようになるまで特定の種類の作業を遅延させることも可能です (たとえば、システムまたはアプリケーションがアイドル状態になるまで、一部の作業項目を遅延させることができます)。WPF では、項目の優先順位付けをサポートすることにより、スレッド上で特定の種類の作業に他の作業よりも多くのアクセスを (したがって時間も) 与えることができます。
後で示す例では、レンダリング エンジンによるユーザー インターフェイスの更新に、入力システムよりも高い優先順位を与えます。これは、マウス、キーボード、またはインク システムをユーザーがどのように操作しても、ユーザー インターフェイスのアニメーションが更新され続けることを意味します。こうすることで、ユーザー インターフェイスの応答性が高く見える場合があります。たとえば、音楽再生アプリケーション (Windows Media® Player のようなアプリケーション) を作成するとします。ほとんどの場合、ユーザーがインターフェイスを使用してもしなくても、音楽再生に関する情報 (進行状況バーなどの情報も含む) が表示されるようにするとよいでしょう。こうすると、ユーザーの最大の関心事 (ここでは音楽を聴くこと) に対し、インターフェイスの応答性を高く見せることができます。
すべての WPF オブジェクトは、Dispatcher のメッセージ ループを使用して作業項目をユーザー インターフェイス スレッドに送り込むだけでなく、オブジェクトがどの Dispatcher に関連付けられているのかを (したがって、どの UI スレッド上に存在するのかも) 認識しています。このため、それ以外のスレッドから WPF オブジェクトを更新しようとすると失敗します。これを担当するのが DispatcherObject クラスです。

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

        // Do some work
    }

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

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

Dispatcher を使用する
Dispatcher クラスは、WPF のメッセージ ポンプへのゲートウェイとなり、UI スレッドでの処理に作業をルーティングするメカニズムを提供します。スレッドの関係の要求を満たすためにはこれが必要です。ただし、Dispatcher で作業がルーティングされるたびに UI スレッドがブロックされるため、Dispatcher で行う作業のサイズを小さくして時間がかからないようにすることが重要です。ユーザー インターフェイスで行う大規模な作業は、複数の小さいブロックに分割して Dispatcher で実行することをお勧めします。UI スレッド上で行う必要がない作業は別のスレッドに移動し、バックグラウンドで処理されるようにします。
通常、作業項目を UI スレッドでの処理に送信するには、Dispatcher クラスを使用します。たとえば、Thread クラスを使用して別個のスレッド上で作業を行う場合、図 3 のように、作業を指定した ThreadStart デリゲートを新しいスレッド上に作成する方法が考えられます。
// The Work to perform on another thread
ThreadStart start = delegate()
{
    // ...

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

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

statusText コントロール (TextBlock) の Text プロパティの設定が UI スレッド上で呼び出されていないため、このコードは失敗します。コードで TextBlock の Text を設定しようとすると、呼び出しが UI スレッドから行われているかどうかを確認する VerifyAccess メソッドが、TextBlock クラスの内部で呼び出されます。呼び出しが別のスレッドから行われていると判断されると、例外がスローされます。では、UI スレッド上で呼び出しを行うには、Dispatcher をどのように使用したらよいのでしょう。
Dispatcher クラスを使用すると、コードを UI スレッド上で直接呼び出すことができるようになります。図 4 では、Dispatcher の Invoke メソッドを使用して、TextBlock の Text プロパティを自動的に変更するためのメソッド SetStatus を呼び出しています。
// The Work to perform on another thread
ThreadStart start = delegate()
{
  // ...

  // Sets the Text on a TextBlock Control.
  // This will work as its using the dispatcher
  Dispatcher.Invoke(DispatcherPriority.Normal, 
                    new Action<string>(SetStatus), 
                    "From Other Thread");
};
// Create the thread and kick it started!
new Thread(start).Start();
Invoke 呼び出しは 3 つの情報を受け取ります。実行する項目の優先順位、実行する作業を記述するデリゲート、および 2 番目のパラメータで記述したデリゲートに渡すパラメータです。Invoke を呼び出すと、このデリゲートが UI スレッド上で呼び出されるよう、キューに登録されます。Invoke メソッドを使用すると、作業が UI スレッド上で実行されるまで確実にブロックできます。
Dispatcher を同期で使用する代わりに、Dispatcher の BeginInvoke メソッドを使用して、作業項目を非同期で UI スレッドのキューに登録することもできます。BeginInvoke メソッドを呼び出すと、DispatcherOperation クラスのインスタンスが返されます。このクラスには、作業項目の現在の状態や実行結果 (完了した作業項目の場合) など、作業項目の実行に関する情報が格納されます。BeginInvoke メソッドおよび DispatcherOperation クラスの使用方法を、図 5 に示します。
// The Work to perform on another thread
ThreadStart start = delegate()
{
    // ...

    // This will work as its using the dispatcher
    DispatcherOperation op = Dispatcher.BeginInvoke(
        DispatcherPriority.Normal, 
        new Action<string>(SetStatus), 
        "From Other Thread (Async)");
    
    DispatcherOperationStatus status = op.Status;
    while (status != DispatcherOperationStatus.Completed)
    {
        status = op.Wait(TimeSpan.FromMilliseconds(1000));
        if (status == DispatcherOperationStatus.Aborted)
        {
            // Alert Someone
        }
    }
};

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

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

優先順位 説明
Inactive 作業項目はキューに登録されますが、処理されません。
SystemIdle システムがアイドル状態のときにのみ、作業項目が UI スレッドにディスパッチされます。実際に処理される項目としては、これが最低の優先順位です。
ApplicationIdle アプリケーション自体がアイドル状態のときにのみ、作業項目が UI スレッドにディスパッチされます。
ContextIdle 優先順位の高い作業項目が処理された後に、作業項目が UI スレッドにディスパッチされます。
Background レイアウト、レンダリング、および入力の項目がすべて処理された後に、作業項目がディスパッチされます。
Input ユーザー入力と同じ優先順位で、作業項目が UI スレッドにディスパッチされます。
Loaded レイアウトとレンダリングがすべて完了した後に、作業項目が UI スレッドにディスパッチされます。
Render レンダリング エンジンと同じ優先順位で、作業項目が UI スレッドにディスパッチされます。
DataBind データ バインドと同じ優先順位で、作業項目が UI スレッドにディスパッチされます。
Normal 通常の優先順位で、作業項目が UI スレッドにディスパッチされます。ほとんどのアプリケーション作業項目は、この優先順位でディスパッチされるようにしてください。
Send 最高の優先順位で、作業項目が UI スレッドにディスパッチされます。
一般に、UI の外観を更新する作業項目の優先順位としては、常に DispatcherPriority.Normal を使用します (前に使用した例を参照)。しかし、他の優先順位を使用すべき場合もあります。特に興味深いのは、3 種類のアイドル優先順位 (ContextIdle、ApplicationIdle、および SystemIdle) です。これらの優先順位を使用すると、作業負荷が非常に低いときにのみ作業項目が実行されるよう指定できます。

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

...

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

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

...

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

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

BackgroundWorker コンポーネントは WPF とうまく連動します。このコンポーネントの裏では AsyncOperationManager クラスが使用され、さらに AsyncOperationManager クラスにより SynchronizationContext クラスが使用されて、同期処理が行われるためです。Windows フォームでは、SynchronizationContext クラスから派生する WindowsFormsSynchronizationContext クラスが、AsyncOperationManager によって渡されます。同様に ASP.NET では、SynchronizationContext の別の派生クラス AspNetSynchronizationContext が処理されます。SynchronizationContext から派生するこれらのクラスは、メソッド呼び出しのスレッド間同期を処理できます。
WPF では、このモデルが DispatcherSynchronizationContext クラスで拡張されています。BackgroundWorker を使用すると、Dispatcher が自動的に使用され、複数スレッドにまたがるメソッド呼び出しが行われます。この共通パターンに既に精通している多くの読者にとって、新しい WPF プロジェクトでも引き続き BackgroundWorker を使用できることは朗報です。

DispatcherTimer
Microsoft® .NET Framework での開発において、コードの定期実行は一般的なタスクです。しかしこれまで、.NET でのタイマの使用は、よく言っても複雑でした。.NET Framework 基本クラス ライブラリ (BCL) で Timer クラスを探すと、3 つもの Timer クラスが見つかります。System.Threading.Timer、System.Timers.Timer、そして System.Windows.Forms.Timer です。これらのタイマはそれぞれに異なります。Alex Calvo による MSDN Magazine の記事では、これらの Timer クラスの使い分けについて説明されています (msdn.microsoft.com/msdnmag/issues/04/02/TimersinNET を参照)。
WPF アプリケーションのために、Dispatcher を利用する新しい種類のタイマ (DispatcherTimer クラス) が導入されました。他のタイマと同様に DispatcherTimer クラスでも、刻み間隔のほか、タイマのイベント発生時に実行するコードを指定できます。図 8 に、DispatcherTimer のごく一般的な使用方法を示します。
// Create a Timer with a Normal Priority
_timer = new DispatcherTimer();

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

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

// Start the timer
_timer.Start();

DispatcherTimer クラスは Dispatcher に関連付けられているため、DispatcherPriority および使用する Dispatcher も指定できます。DispatcherTimer クラスは、優先順位 Normal を現在の Dispatcher として既定で使用しますが、これらの値はオーバーライドできます。
_timer = new DispatcherTimer(
    DispatcherPriority.SystemIdle, form1.Dispatcher);
応答性の高いアプリケーションを作成するためには、ワーカー プロセスを計画する努力を惜しまないでください。初期段階で調査を行えば、さらに満足のいく計画を実現できます。着手する前に、「WPF スレッド関連リンク」に記載されているサイトを参照することをお勧めします。この記事と併せてこれらのサイトを読むことにより、応答性の高いアプリケーションを開発するための十分な基盤が得られます。

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

Page view tracker