このドキュメントはアーカイブされており、メンテナンスされていません。

スレッド モデル

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 のほとんどすべてのクラスは DependencyObject の子孫です。構築時に、DependencyObject は、現在実行中のスレッドにリンクされている Dispatcher への参照を格納します。実際には、DependencyObject は、このオブジェクトを作成するスレッドと関連付けられます。プログラムの実行中に、DependencyObject はパブリック VerifyAccess メソッドを呼び出すことができます。VerifyAccess は現在のスレッドに関連付けられた Dispatcher を調べ、それを構築中に格納された Dispatcher の参照と比較します。これらが一致しない場合、VerifyAccess は例外をスローします。VerifyAccess は、DependencyObject に属するすべてのメソッドの開始時に呼び出されることを想定しています。

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

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

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

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

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

次に例を示します。

Prime numbers screen shot

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

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

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

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

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

Dispatcher queue illustration

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

まず、ユーザー インターフェイスを作成する XAML です。

<Window x:Class="SDKSamples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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();
        }

        public 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 のイベント ハンドラです。

public 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 スレッドで更新を実行する必要があります。このような状況については、この次の部分で説明します。

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

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

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

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

Weather UI screen shot
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);
        }        
    }
}

1) ボタン ハンドラ

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 がイベントを処理できるようにします。

2) 天気の取得

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 スレッドに報告します。これは、UI スレッドで、このスレッドの Dispatcher を使用して、updateUserInterface の呼び出しをスケジュールすることによって行います。天気を説明する文字列をこのスケジュールされた関数呼び出しに渡します。

3) 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 の呼び出しを実行します。このメソッドにより時計のアニメーションが停止され、天気を表すイメージが選択されます。選択されたイメージが表示され、[Fetch Forecast] ボタンが復元されます。

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

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

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

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

WPF Frame コントロールを使用すると、Web ページを表示できます。単純な Internet Explorer の代用品を簡単に作成できます。重要な機能である、新しいエクスプローラ ウィンドウを開く機能から説明していきます。

ユーザーが [New Window] ボタンをクリックしたときに、別のスレッドでウィンドウのコピーを起動します。このようにすると、いずれかのウィンドウで長時間実行される操作またはブロック操作が他のウィンドウをロックすることはありません。

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

コードは次のとおりです。

<Window x:Class="SDKSamples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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("http://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] ボタンをクリックするまで待機している間は、元のアプリケーション ウィンドウはユーザーの入力に応答しません。ただし、描画メッセージの処理は続行します。元のウィンドウの上に何かを配置して、それを取り除いたときに、ウィンドウが自動的に再描画されることがわかります。

一部のスレッドはポップアップに使用されます。WPF では、ポップアップ専用の新しいスレッドを作成できますが、このスレッドは元のウィンドウで無効になっている要素を描画することはできません (ドキュメントの冒頭の相互排除のセクションを参照)。代わりに、WPF は入れ子になったメッセージ処理システムを使用します。Dispatcher クラスには、PushFrame と呼ばれる特殊なメソッドが含まれます。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 が完了した後で、handler1 を使用してイベントの処理を行う Canvas オブジェクトにイベントが渡されます。これは、handler2 が明示的にイベント オブジェクトを処理済みとしてマークしない場合にのみ発生します。

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

再入およびロック

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

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

Threading reentrancy diagram

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

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

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

参照

表示: