スレッド モデル

更新 : 2007 年 11 月

Windows Presentation Foundation (WPF) は、開発者が難解なスレッド化を処理する必要がなくなるように設計されています。結果として、ほとんどの WPF 開発者が複数のスレッドを使用するインターフェイスを記述する必要がなくなりました。マルチスレッド プログラムは複雑でデバッグが困難であるため、シングル スレッドで作成できる場合はマルチスレッド化を回避することをお勧めします。

しかし、どのように適切に設計されていたとしても、UI フレームワークでは、すべての種類の問題に対してシングル スレッド ソリューションを提供することはできません。WPF でも対策は講じられていますが、マルチスレッド化によってユーザー インターフェイス (UI) の応答性やアプリケーションのパフォーマンスが向上する場合があることは依然として事実です。ここでは、背景となる状況を紹介した後で、これらの状況の一部について考察し、最後に詳細について説明します。

このトピックには次のセクションが含まれています。

  • 概要とディスパッチャ
  • 実際のスレッド : サンプル
  • 技術的詳細および障害となる点
  • 関連トピック

概要とディスパッチャ

通常、WPF アプリケーションは、レンダリングを処理するスレッドと UI の管理をするスレッドの 2 つで開始されます。レンダリング スレッドは、UI スレッドが入力を受け取り、イベントを処理し、画面を描画して、アプリケーション コードを実行している間に、バックグラウンドで非表示の状態で効率的に実行されます。ほとんどのアプリケーションでは、単一の UI スレッドが使用されますが、状況によっては複数のスレッドを使用した方が適切な場合もあります。この点については、後で、例を使用して説明します。

UI スレッドは、Dispatcher と呼ばれるオブジェクト内の作業項目をキューに配置します。Dispatcher は作業項目を優先順位に従って選択し、それぞれを最後まで実行します。各 UI スレッドには少なくとも 1 つの Dispatcher が必要であり、各 Dispatcher は作業項目を 1 つのスレッドで実行できます。

応答性が高く、ユーザーにとって使いやすいアプリケーションを構築するための秘訣は、作業項目を小さく保つことによって Dispatcher のスループットを最大限に引き上げることです。こうすることにより、項目が Dispatcher キューで処理を待機し続けて、期限切れになることはなくなります。入力から応答までの間に遅延を感じると、ユーザーは苛立つことがあります。

では、WPF アプリケーションは、大きな操作をどのように処理することになっているのでしょうか。コードに大量の計算が含まれている場合や、コードがリモート サーバー上のデータベースを照会する必要がある場合は、どのようになるのでしょうか。通常は、UI スレッドを Dispatcher キュー内の項目に対処できる状態にしておいて、大きな操作は別のスレッドで処理します。大きな操作が完了したら、結果を UI スレッドに報告して表示できます。

従来から、Windows では、UI 要素にアクセスできるスレッドは、その要素を作成したスレッドだけです。つまり、長時間実行されるタスクを処理するバックグラウンド スレッドが、そのタスクが終了したときにテキスト ボックスを更新することはできません。Windows では、この制約により、UI コンポーネントの整合性を確保しています。たとえば、リスト ボックスのコンテンツが描画中にバックグラウンド スレッドによって更新された場合、そのリスト ボックスは不適切な外観になる可能性があります。

WPF には、これを調整する相互排除機構が組み込まれています。WPF のほとんどのクラスは、DispatcherObject から派生します。構築時に、DispatcherObject は、現在実行中のスレッドにリンクされている Dispatcher への参照を格納します。実際には、DispatcherObject は、このオブジェクトを作成するスレッドと関連付けられます。プログラムの実行中に、DispatcherObject はパブリック VerifyAccess メソッドを呼び出すことができます。VerifyAccess は、現在のスレッドに関連付けられた Dispatcher を調べ、それを構築中に格納された Dispatcher の参照と比較します。これらが一致しない場合、VerifyAccess は例外をスローします。VerifyAccess は、DispatcherObject に属するすべてのメソッドの開始時に呼び出されることを想定しています。

1 つのスレッドのみが UI を変更できる場合、バックグラウンド スレッドはユーザーとどのように対話するのでしょうか。バックグラウンド スレッドは、UI スレッドに代わりに操作を実行させることができます。これは、UI スレッドの Dispatcher に作業項目を登録することによって実現します。Dispatcher クラスには、作業項目の登録用として InvokeBeginInvoke という 2 つのメソッドがあります。これらのメソッドは共に、デリゲートの実行をスケジュールします。Invoke は、同期的に動作します。つまり、UI スレッドが実際にデリゲートの実行を完了するまで制御を戻しません。BeginInvoke は非同期であり、すぐに制御を戻します。

Dispatcher はキュー内の要素を優先順位に従って並べます。要素を Dispatcher キューに追加するときに指定できるレベルは、10 個あります。これらの優先順位は DispatcherPriority 列挙体で保持されます。DispatcherPriority レベルの詳細については、Windows SDK のドキュメントを参照してください。

実際のスレッド : サンプル

計算を長時間実行するシングル スレッド アプリケーション

ほとんどのグラフィカル ユーザー インターフェイス (GUI) は、ユーザーの操作に応答してイベントが生成されるまで、アイドル状態で待機しています。注意してプログラミングすることによって、このアイドル時間は、UI の応答性に影響を与えずに、建設的に使用できます。WPF のスレッド モデルでは、UI スレッドで発生する操作を入力によって中断することはできません。つまり、保留中の入力イベントが期限切れになる前に処理するには、必ず定期的に Dispatcher に戻る必要があります。

次に例を示します。

素数のスクリーン ショット

この単純なアプリケーションでは、素数を検索して 3 から上方にカウントします。ユーザーが [開始] ボタンをクリックすると、検索が開始されます。プログラムで素数が検索されると、検出された数字でユーザー インターフェイスが更新されます。任意の時点で、ユーザーは検索を停止できます。

この例は非常に単純ではありますが、素数の検索が無期限に継続される可能性があるため、いくつかの問題点も提示します。ボタンのクリック イベント ハンドラの内部で検索全体を処理した場合は、UI スレッドに他のイベントを処理する機会が与えられません。そのため、UI は、入力への応答またはメッセージの処理ができなくなります。また、再描画やボタンのクリックへの応答も行われなくなります。

素数の検索を別のスレッドで実行することもできますが、その場合は同期に関する問題を処理する必要が生じます。シングル スレッドによる方法なら、検索された最大の素数を表示するラベルを直接更新できます。

計算のタスクを管理可能なチャンクに分割すると、定期的に Dispatcher に戻り、イベントを処理できます。それにより、WPF で再描画および入力の処理を行うことができるようになります。

計算とイベント処理の間で処理時間を分割する最適な方法は、Dispatcher から計算を管理することです。BeginInvoke メソッドを使用することによって、UI イベントの取り出し元と同じキュー内で素数のチェックをスケジュールできます。この例では、一度に 1 つの素数のチェックのみをスケジュールします。素数のチェックが完了した後で、次のチェックをすぐにスケジュールします。このチェックは、保留中の UI イベントが処理された後にのみ続行されます。

ディスパッチャ キューの図

Microsoft Word は、この機構を使用してスペル チェックを実行します。スペル チェックは、UI スレッドのアイドル時間を使用して、バックグラウンドで行われます。以下にコードを示します。

ユーザー インターフェイスを作成する XAML の例を次に示します。

<Window x:Class="SDKSamples.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="Prime Numbers" Width="260" Height="75"
    >
  <StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
    <Button Content="Start"  
            Click="StartOrStop"
            Name="startStopButton"
            Margin="5,0,5,0"
            />
    <TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
    <TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
  </StackPanel>
</Window>

分離コード例を次に示します。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        public delegate void NextPrimeDelegate();

        //Current number to check 
        private long num = 3;   

        private bool continueCalculating = false;

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void StartOrStop(object sender, EventArgs e)
        {
            if (continueCalculating)
            {
                continueCalculating = false;
                startStopButton.Content = "Resume";
            }
            else
            {
                continueCalculating = true;
                startStopButton.Content = "Stop";
                startStopButton.Dispatcher.BeginInvoke(
                    DispatcherPriority.Normal,
                    new NextPrimeDelegate(CheckNextNumber));
            }
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            NotAPrime = false;

            for (long i = 3; i <= Math.Sqrt(num); i++)
            {
                if (num % i == 0)
                {
                    // Set not a prime flag to ture.
                    NotAPrime = true;
                    break;
                }
            }

            // If a prime number.
            if (!NotAPrime)
            {
                bigPrime.Text = num.ToString();
            }

            num += 2;
            if (continueCalculating)
            {
                startStopButton.Dispatcher.BeginInvoke(
                    System.Windows.Threading.DispatcherPriority.SystemIdle, 
                    new NextPrimeDelegate(this.CheckNextNumber));
            }
        }

        private bool NotAPrime = false;
    }
}

Button のイベント ハンドラの例を次に示します。

private void StartOrStop(object sender, EventArgs e)
{
    if (continueCalculating)
    {
        continueCalculating = false;
        startStopButton.Content = "Resume";
    }
    else
    {
        continueCalculating = true;
        startStopButton.Content = "Stop";
        startStopButton.Dispatcher.BeginInvoke(
            DispatcherPriority.Normal,
            new NextPrimeDelegate(CheckNextNumber));
    }
}

Button 上のテキストの更新以外に、このハンドラは、Dispatcher キューにデリゲートを追加することによって、最初の素数のチェックのスケジュールも行います。このイベント ハンドラが処理を完了した後、しばらくすると、Dispatcher はこのデリゲートを実行のために選択します。

前に説明したように、BeginInvoke は、デリゲートの実行のスケジュールのために使用される Dispatcher メンバです。この例では、優先順位として SystemIdle を選択します。Dispatcher は、処理の対象となる重要なイベントがない場合にのみ、このデリゲートを実行します。UI の応答は数のチェックより重要です。数のチェックのルーチンを表す新しいデリゲートも渡します。

public void CheckNextNumber()
{
    // Reset flag.
    NotAPrime = false;

    for (long i = 3; i <= Math.Sqrt(num); i++)
    {
        if (num % i == 0)
        {
            // Set not a prime flag to ture.
            NotAPrime = true;
            break;
        }
    }

    // If a prime number.
    if (!NotAPrime)
    {
        bigPrime.Text = num.ToString();
    }

    num += 2;
    if (continueCalculating)
    {
        startStopButton.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.SystemIdle, 
            new NextPrimeDelegate(this.CheckNextNumber));
    }
}

private bool NotAPrime = false;

このメソッドは、次の奇数が素数であるかどうかをチェックします。素数である場合、メソッドは直接 bigPrime TextBlock を更新して、検出結果を反映します。このような処理を行うことができるのは、このコンポーネントの作成に使用されたスレッドと同じスレッドで計算を実行するからです。計算用に別のスレッドを使用する場合は、さらに複雑な同期の機構を使用し、UI スレッドで更新を実行する必要があります。このような状況については、この次の部分で説明します。

このサンプルのソース コード例全体については、「計算を長時間実行するシングル スレッド アプリケーションのサンプル」を参照してください。

バックグラウンド スレッドを使用したブロック操作の処理

グラフィカル アプリケーションでのブロック操作の処理は難解になる場合があります。アプリケーションが停止したように見えるため、ブロック メソッドをイベント ハンドラから呼び出すのは適切ではありません。これらの操作を処理するために別のスレッドを使用できますが、そうした場合、ワーカー スレッドから UI を直接変更することはできないため、GUI スレッドとの同期が必要になります。Invoke または BeginInvoke を使用して、デリゲートを UI スレッドの Dispatcher に挿入できます。最終的に、これらのデリゲートは、UI 要素を変更するアクセス許可を使用して実行されます。

この例では、天気予報を取得するリモート プロシージャ コールを模倣します。別のワーカー スレッドを使用してこの呼び出しを実行し、それが完了したら UI スレッドの Dispatcher で更新メソッドをスケジュールします。

天気予報 UI スクリーン ショット

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        // Delegates to be used in placking jobs onto the Dispatcher.
        private delegate void NoArgDelegate();
        private delegate void OneArgDelegate(String arg);

        // Storyboards for the animations.
        private Storyboard showClockFaceStoryboard;
        private Storyboard hideClockFaceStoryboard;
        private Storyboard showWeatherImageStoryboard;
        private Storyboard hideWeatherImageStoryboard;

        public Window1(): base()
        {
            InitializeComponent();
        }  

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Load the storyboard resources.
            showClockFaceStoryboard = 
                (Storyboard)this.Resources["ShowClockFaceStoryboard"];
            hideClockFaceStoryboard = 
                (Storyboard)this.Resources["HideClockFaceStoryboard"];
            showWeatherImageStoryboard = 
                (Storyboard)this.Resources["ShowWeatherImageStoryboard"];
            hideWeatherImageStoryboard = 
                (Storyboard)this.Resources["HideWeatherImageStoryboard"];   
        }

        private void ForecastButtonHandler(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            hideWeatherImageStoryboard.Begin(this);

            // Start fetching the weather forecast asynchronously.
            NoArgDelegate fetcher = new NoArgDelegate(
                this.FetchWeatherFromServer);

            fetcher.BeginInvoke(null, null);
        }

        private void FetchWeatherFromServer()
        {
            // Simulate the delay from network access.
            Thread.Sleep(4000);              

            // Tried and true method for weather forecasting - random numbers.
            Random rand = new Random();
            String weather;

            if (rand.Next(2) == 0)
            {
                weather = "rainy";
            }
            else
            {
                weather = "sunny";
            }

            // Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(
                System.Windows.Threading.DispatcherPriority.Normal,
                new OneArgDelegate(UpdateUserInterface), 
                weather);
        }

        private void UpdateUserInterface(String weather)
        {    
            //Set the weather image
            if (weather == "sunny")
            {       
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "SunnyImageSource"];
            }
            else if (weather == "rainy")
            {
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "RainingImageSource"];
            }

            //Stop clock animation
            showClockFaceStoryboard.Stop(this);
            hideClockFaceStoryboard.Begin(this);

            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;     
        }

        private void HideClockFaceStoryboard_Completed(object sender,
            EventArgs args)
        {         
            showWeatherImageStoryboard.Begin(this);
        }

        private void HideWeatherImageStoryboard_Completed(object sender,
            EventArgs args)
        {           
            showClockFaceStoryboard.Begin(this, true);
        }        
    }
}

注意が必要な詳細を次に示します。

  • ボタン ハンドラの作成

    private void ForecastButtonHandler(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        hideWeatherImageStoryboard.Begin(this);
    
        // Start fetching the weather forecast asynchronously.
        NoArgDelegate fetcher = new NoArgDelegate(
            this.FetchWeatherFromServer);
    
        fetcher.BeginInvoke(null, null);
    }
    

ボタンがクリックされると、時計の図を表示し、そのアニメーション化を開始します。ボタンは無効にします。新しいスレッドで FetchWeatherFromServer メソッドを呼び出して制御を戻し、天気予報を収集するのを待機している間に Dispatcher がイベントを処理できるようにします。

  • 天気の取得

    private void FetchWeatherFromServer()
    {
        // Simulate the delay from network access.
        Thread.Sleep(4000);              
    
        // Tried and true method for weather forecasting - random numbers.
        Random rand = new Random();
        String weather;
    
        if (rand.Next(2) == 0)
        {
            weather = "rainy";
        }
        else
        {
            weather = "sunny";
        }
    
        // Schedule the update function in the UI thread.
        tomorrowsWeather.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.Normal,
            new OneArgDelegate(UpdateUserInterface), 
            weather);
    }
    

この例では、単純にするため、ネットワーク用のコードは使用していません。その代わり、新しいスレッドを 4 秒間スリープさせることによって、ネットワーク アクセスの遅延をシミュレートします。この時点ではまだ、元の UI スレッドは実行されており、イベントに応答しています。これを確認できるように、アニメーションの実行を続行させてあります。最小化ボタンと最大化ボタンも継続して動作しています。

  • UI の更新

    private void UpdateUserInterface(String weather)
    {    
        //Set the weather image
        if (weather == "sunny")
        {       
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "SunnyImageSource"];
        }
        else if (weather == "rainy")
        {
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "RainingImageSource"];
        }
    
        //Stop clock animation
        showClockFaceStoryboard.Stop(this);
        hideClockFaceStoryboard.Begin(this);
    
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;     
    }
    

UI スレッドの Dispatcher で時間がある場合、スケジュールされた UpdateUserInterface の呼び出しを実行します。このメソッドにより時計のアニメーションが停止され、天気を表すイメージが選択されます。選択されたイメージが表示され、予報を取得するボタンが復元されます。

このサンプルのソース コード例全体については、「ディスパッチャによる天気予報サービスのシミュレーションのサンプル」を参照してください。

複数のウィンドウ、複数のスレッド

一部の WPF アプリケーションには、複数のトップレベルのウィンドウが必要です。1 つのスレッド/Dispatcher の組み合わせが複数のウィンドウを管理することにまったく問題はありませんが、複数のスレッドの方が効率的な場合もあります。これは、特に、ウィンドウのいずれかがスレッドを専有する場合に当てはまります。

Windows エクスプローラはこのような形で機能します。新しいエクスプローラ ウィンドウはすべて元のプロセスに属しますが、独立したスレッドに制御されて作成されます。

WPF Frame コントロールを使用すると、Web ページを表示できます。単純な Internet Explorer の代用品を簡単に作成できます。重要な機能である、新しいエクスプローラ ウィンドウを開く機能から説明していきます。ユーザーが [New Window] ボタンをクリックしたときに、別のスレッドでウィンドウのコピーを起動します。このようにすると、いずれかのウィンドウで長時間実行される操作またはブロック操作が他のウィンドウをロックすることはありません。

実際には、Web ブラウザ モデルには、独自の複雑なスレッド モデルがあります。ここでは、多くの読者にわかりやすい例として、このモデルを取り上げます。

次にコード例を示します。

<Window x:Class="SDKSamples.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="MultiBrowse"
    Height="600" 
    Width="800"
    Loaded="OnLoaded"
    >
  <StackPanel Name="Stack" Orientation="Vertical">
    <StackPanel Orientation="Horizontal">
      <Button Content="New Window"
              Click="NewWindowHandler" />
      <TextBox Name="newLocation"
               Width="500" />
      <Button Content="GO!"
              Click="Browse" />
    </StackPanel>

    <Frame Name="placeHolder"
            Width="800"
            Height="550"></Frame>
  </StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using System.Threading;


namespace SDKSamples
{
    public partial class Window1 : Window
    {

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
           placeHolder.Source = new Uri("https://www.msn.com");
        }

        private void Browse(object sender, RoutedEventArgs e)
        {
            placeHolder.Source = new Uri(newLocation.Text);
        }

        private void NewWindowHandler(object sender, RoutedEventArgs e)
        {       
            Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
            newWindowThread.SetApartmentState(ApartmentState.STA);
            newWindowThread.IsBackground = true;
            newWindowThread.Start();
        }

        private void ThreadStartingPoint()
        {
            Window1 tempWindow = new Window1();
            tempWindow.Show();       
            System.Windows.Threading.Dispatcher.Run();
        }
    }
}

次のコードのスレッド部分は、この説明の中で最も重要な部分です。

private void NewWindowHandler(object sender, RoutedEventArgs e)
{       
    Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
    newWindowThread.SetApartmentState(ApartmentState.STA);
    newWindowThread.IsBackground = true;
    newWindowThread.Start();
}

このメソッドは、[New Window] ボタンがクリックされると呼び出されます。新しいスレッドが作成されて非同期に開始されます。

private void ThreadStartingPoint()
{
    Window1 tempWindow = new Window1();
    tempWindow.Show();       
    System.Windows.Threading.Dispatcher.Run();
}

このメソッドは、新しいスレッドの開始点です。このスレッドで制御される新しいウィンドウを作成します。WPF は自動的に新しい Dispatcher を作成し、新しいスレッドを管理します。ウィンドウを機能させるために行う必要があるのは、Dispatcher を開始することだけです。

このサンプルのソース コード例全体については、「Web ブラウザのマルチスレッド処理のサンプル」を参照してください。

技術的詳細および障害となる点

スレッドを使用したコンポーネントの作成

『Microsoft .NET Framework 開発者ガイド』では、コンポーネントが非同期の動作をクライアントに公開する方法についてのパターンを説明しています (「イベントベースの非同期パターンの概要」を参照)。たとえば、FetchWeatherFromServer メソッドを再利用可能な非グラフィカル コンポーネントにパッケージ化する場合について考えます。標準の Microsoft .NET Framework パターンに従うと、これは次のようになります。

public class WeatherComponent : Component
{
    //gets weather: Synchronous 
    public string GetWeather()
    {
        string weather = "";

        //predict the weather

        return weather;
    }

    //get weather: Asynchronous 
    public void GetWeatherAsync()
    {
        //get the weather
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
}

public class GetWeatherCompletedEventArgs : AsyncCompletedEventArgs
{
    public GetWeatherCompletedEventArgs(Exception error, bool canceled,
        object userState, string weather)
        :
        base(error, canceled, userState)
    {
        _weather = weather;
    }

    public string Weather
    {
        get { return _weather; }
    }
    private string _weather;
}

public delegate void GetWeatherCompletedEventHandler(object sender,
    GetWeatherCompletedEventArgs e);

GetWeatherAsync は、バックグラウンド スレッドの作成など、前に説明した手法のいずれかを使用して、非同期で、つまりスレッドの呼び出しをブロックせずに作業を行います。

このパターンで最も重要な部分の 1 つは、最初に MethodNameAsync メソッドを呼び出したスレッドと同じスレッドで MethodNameCompleted メソッドを呼び出すということです。これは、CurrentDispatcher を確保しておくことにより WPF を使用して簡単に行うことができますが、非グラフィカル コンポーネントは WPF アプリケーションのみで使用でき、Windows フォームまたは ASP.NET プログラムでは使用できません。 

DispatcherSynchronizationContext クラスは、この要件に対応します。これは、他の UI フレームワークとも動作する Dispatcher の簡略化されたバージョンとして考えることができます。

public class WeatherComponent2 : Component
{
    public string GetWeather()
    {
        return fetchWeatherFromServer();
    }

    private DispatcherSynchronizationContext requestingContext = null;

    public void GetWeatherAsync()
    {
        if (requestingContext != null)
            throw new InvalidOperationException("This component can only handle 1 async request at a time");

        requestingContext = (DispatcherSynchronizationContext)DispatcherSynchronizationContext.Current;

        NoArgDelegate fetcher = new NoArgDelegate(this.fetchWeatherFromServer);

        // Launch thread
        fetcher.BeginInvoke(null, null);
    }

    private void RaiseEvent(GetWeatherCompletedEventArgs e)
    {
        if (GetWeatherCompleted != null)
            GetWeatherCompleted(this, e);
    }

    private string fetchWeatherFromServer()
    {
        // do stuff
        string weather = "";

        GetWeatherCompletedEventArgs e =
            new GetWeatherCompletedEventArgs(null, false, null, weather);

        SendOrPostCallback callback = new SendOrPostCallback(DoEvent);
        requestingContext.Post(callback, e);
        requestingContext = null;

        return e.Weather;
    }

    private void DoEvent(object e)
    {
        //do stuff
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
    public delegate string NoArgDelegate();
}

入れ子になったポンプ処理

UI スレッドの完全なロックができない場合があります。MessageBox クラスの Show メソッドについて考えてみましょう。Show はユーザーが [OK] ボタンをクリックするまでは制御を返しません。ただし、ユーザーとの対話を実現するために、メッセージ ループを持つウィンドウを作成します。ユーザーが [OK] ボタンをクリックするまで待機している間は、元のアプリケーション ウィンドウはユーザーの入力に応答しません。ただし、描画メッセージの処理は続行します。元のウィンドウの上に何かを配置して、それを取り除いたときに、ウィンドウが自動的に再描画されます。

[OK] ボタンを含む MessageBox

一部のスレッドはメッセージ ボックス ウィンドウに使用されます。WPF では、メッセージ ボックス ウィンドウ専用の新しいスレッドを作成できますが、このスレッドは元のウィンドウで無効になっている要素を描画することはできません (前の相互排除に関する説明を参照)。代わりに、WPF は入れ子になったメッセージ処理システムを使用します。Dispatcher クラスには、PushFrame と呼ばれる特殊なメソッドが含まれます。これはアプリケーションの現在の実行ポイントを格納し、新しいメッセージ ループを開始します。入れ子になったメッセージ ループが完了すると、元の PushFrame の呼び出しの後に実行が再開されます。

この例では、PushFrameMessageBox.Show の呼び出し時点でのプログラム コンテキストを保持し、新しいメッセージ ループを開始して、背面ウィンドウの再描画およびメッセージ ボックス ウィンドウへの入力の処理を行います。ユーザーが [OK] をクリックし、ポップアップ ウィンドウを消去したときに、入れ子になったループが終了し、Show の呼び出しの後に制御が再開されます。

古いルーティング イベント

WPF のルーティング イベント システムは、イベントが発生したときにツリー全体に通知します。

<Canvas MouseLeftButtonDown="handler1" 
        Width="100"
        Height="100"
        >
  <Ellipse Width="50"
           Height="50"
           Fill="Blue" 
           Canvas.Left="30"
           Canvas.Top="50" 
           MouseLeftButtonDown="handler2"
           />
</Canvas>

楕円の上でマウスの左ボタンが押されると、handler2 が実行されます。handler2 が完了した後で、Canvas オブジェクトにイベントが渡され、そこで handler1 によってイベントは処理されます。これは、handler2 が明示的にイベント オブジェクトを処理済みとしてマークしない場合にのみ発生します。

handler2 がこのイベントを処理する際に、長い時間がかかる可能性があります。handler2 が、PushFrame を使用して、数時間にわたって制御を戻さない入れ子になったメッセージ ループを開始する場合が考えられます。このメッセージ ループが完了したときに handler2 がイベントを処理済みとしてマークしない場合、イベントは古くなっていてもツリーの上位に渡されます。

再入およびロック

共通言語ランタイム (CLR) のロック機構は、一般的な想定どおりには動作しません。通常は、ロックを要求したときにスレッドが操作を完全に停止すると想定されます。しかし実際には、スレッドは優先順位の高いメッセージの受け取りおよび処理を続行します。これによって、デッドロックが回避され、インターフェイスの応答を最小限に抑えることができますが、小さなバグが発生する可能性があります。ほとんどの場合、この点について認識する必要はありませんが、(通常は Win32 ウィンドウ メッセージまたは COM STA コンポーネントに関連する状況において) この点について知っておくと役立つことがあります。

開発者は UI には複数のスレッドからアクセスできないものと想定して作業するため、ほとんどのインターフェイスはスレッド セーフを考慮して構築されていません。この例では、シングル スレッドによって予期しない時点で環境が変更される可能性があり、これによって DispatcherObject の相互排除機構で解決されるはずの悪影響が発生します。次の擬似コードについて考えてみます。

スレッド処理再入のダイアグラム

ほとんどの場合においてはこのコードは正しいと言えますが、WPF では、このような予期しない再入が実際に問題を引き起こす場合があります。このため、特定のキーとなるタイミングで、WPF は DisableProcessing を呼び出します。このメソッドは、そのスレッドに対するロック命令を変更し、このメソッドは、そのスレッドに対するロック命令を変更し、通常の CLR ロックの代わりに WPF の再入されないロックを使用するように指示します。

なぜ CLR チームはこの動作を採用したのでしょうか。それは、COM STA オブジェクトと終了操作スレッドに関係があります。オブジェクトのガベージ コレクションが行われると、UI スレッドではなく、専用のファイナライザ スレッドで Finalize メソッドが実行されます。ここに問題があります。UI スレッドで作成された COM STA オブジェクトは UI スレッドでしか破棄できません。CLR は BeginInvoke と同じことを実行します (この例では Win32 の SendMessage を使用)。ただし、UI スレッドがビジーの場合、ファイナライザ スレッドの機能が停止し、COM STA オブジェクトを破棄できないため、深刻なメモリ リークが発生します。このため、CLR チームは、ロックを現状どおり機能させるほかない、という難しい判断を行いました。

WPF のタスクは、メモリ リークを再発生させずに予期しない再入を回避することであり、再入のブロックをすべての場所で行うわけではないのはそのためです。

参照

処理手順

計算を長時間実行するシングル スレッド アプリケーションのサンプル

ディスパッチャによる天気予報サービスのシミュレーションのサンプル

Web ブラウザのマルチスレッド処理のサンプル