スレッド モデル

Windows Presentation Foundation (WPF) は、スレッド処理の難しさから開発者を救うように設計されています。 その結果、大部分の WPF 開発者は、複数のスレッドを使用するインターフェイスを作成する必要がなくなります。 マルチスレッド プログラムは複雑でデバッグが困難なため、シングルスレッド ソリューションが存在する場合は回避することが推奨されます。

ただし、どれほど適切に設計したとしても、あらゆる種類の問題に対してシングルスレッドのソリューションを UI フレームワークで提供することはできません。 WPF はもう一歩のところですが、複数のスレッドで ユーザー インターフェイス (UI) の応答性またはアプリケーションのパフォーマンスが向上する状況がまだあります。 この記事では、いくつかの背景資料について説明した後、このような状況の一部について探り、最後にいくつかの下位レベルの詳細について説明します。

注意

このトピックでは、非同期呼び出しに BeginInvoke メソッドを使用したスレッド処理について説明します。 また、Action または Func<TResult> をパラメーターとして受け取る InvokeAsync メソッドを呼び出して、非同期呼び出しを行うこともできます。 InvokeAsync メソッドからは、Task プロパティを持つ DispatcherOperation または DispatcherOperation<TResult> が返されます。 await キーワードは、DispatcherOperation または関連する Task のいずれかと共に使用できます。 Task または DispatcherOperation によって返される DispatcherOperation<TResult> を同期的に待機する必要がある場合、DispatcherOperationWait 拡張メソッドを呼び出します。 Task.Wait を呼び出すと、デッドロックが発生します。 Task を使用して非同期操作を実行する方法の詳細については、「タスクベースの非同期プログラミング」を参照してください。 Invoke メソッドには、Action または Func<TResult> をパラメーターとして受け取るオーバーロードもあります。 Invoke メソッドを使用すると、デリゲートで Action または Func<TResult> を渡すことにより、同期呼び出しを行うことができます。

概要とディスパッチャー

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

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

応答性の高いユーザー フレンドリなアプリケーションを構築する秘訣は、作業項目を小さく保って Dispatcher のスループットを最大化することです。 このようにすると、処理の待機中に Dispatcher キューに格納されている項目が古くなることはなくなります。 入力と応答の間に知覚可能な遅延があると、ユーザーに不満が生じる可能性があります。

WPF アプリケーションでは、どのような方法で大規模な操作を処理することが想定されているでしょうか。 コードに大規模な計算が含まれる場合や、リモート サーバー上のデータベースに対してクエリを実行する必要がある場合は、どうすればよいでしょうか。 通常、その答えは、大規模な操作は別のスレッドで処理し、UI スレッドで Dispatcher キュー内の項目を処理できる余地を残すことです。 大規模な操作が完了すると、結果が UI スレッドに報告され、表示できるようになります。

従来、Windows では、UI 要素へのアクセスは、それらを作成したスレッドにのみ許可されています。 つまり、実行時間が長いタスクを担当するバックグラウンド スレッドでは、終了時にテキスト ボックスを更新できないことがあります。 Windows では、UI コンポーネントの整合性を確保するためにこれを行っています。 コンテンツが描画中にバックグラウンド スレッドによって更新された場合、リスト ボックスが適切に表示されない可能性があります。

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

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

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

動作中のスレッド:サンプル

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

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

次に例を示します。

素数のスレッド処理を示すスクリーンショット。

このシンプルなアプリケーションでは、素数を検索して、3 から数え上げます。 ユーザーが [Start](開始) ボタンをクリックすると、検索が開始されます。 プログラムによって素数が検出されると、その検出によってユーザー インターフェイスが更新されます。 ユーザーはいつでも検索を停止できます。

とてもシンプルですが、素数検索は永遠に続く可能性があり、いくつかの困難を伴います。 ボタンのクリック イベント ハンドラー内で検索全体を処理した場合、UI スレッドに他のイベントを処理する機会を与えないことになります。 UI では、入力に応答することや、メッセージを処理することができません。 再描画もボタンのクリックに対する応答も行われません。

別のスレッドで素数検索を行うこともできますが、その場合は同期の問題に対処する必要があります。 シングルスレッド方式では、ラベルを直接更新し、見つかった最大の素数を列挙することができます。

計算のタスクを扱いやすいチャンクに分割すると、定期的に Dispatcher に戻ってイベントを処理できます。 入力を再描画して処理する機会を WPF に与えることができます。

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

ディスパッチャー キューを示すスクリーンショット。

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();
        }

        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 true.
                    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;
    }
}
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Threading
Imports System.Threading

Namespace SDKSamples
    Partial Public Class MainWindow
        Inherits Window
        Public Delegate Sub NextPrimeDelegate()

        'Current number to check 
        Private num As Long = 3

        Private continueCalculating As Boolean = False

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
            If continueCalculating Then
                continueCalculating = False
                startStopButton.Content = "Resume"
            Else
                continueCalculating = True
                startStopButton.Content = "Stop"
                startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
            End If
        End Sub

        Public Sub CheckNextNumber()
            ' Reset flag.
            NotAPrime = False

            For i As Long = 3 To Math.Sqrt(num)
                If num Mod i = 0 Then
                    ' Set not a prime flag to true.
                    NotAPrime = True
                    Exit For
                End If
            Next

            ' If a prime number.
            If Not NotAPrime Then
                bigPrime.Text = num.ToString()
            End If

            num += 2
            If continueCalculating Then
                startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
            End If
        End Sub

        Private NotAPrime As Boolean = False
    End Class
End Namespace

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));
    }
}
Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
    If continueCalculating Then
        continueCalculating = False
        startStopButton.Content = "Resume"
    Else
        continueCalculating = True
        startStopButton.Content = "Stop"
        startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
    End If
End Sub

このハンドラーは、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 true.
            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;
Public Sub CheckNextNumber()
    ' Reset flag.
    NotAPrime = False

    For i As Long = 3 To Math.Sqrt(num)
        If num Mod i = 0 Then
            ' Set not a prime flag to true.
            NotAPrime = True
            Exit For
        End If
    Next

    ' If a prime number.
    If Not NotAPrime Then
        bigPrime.Text = num.ToString()
    End If

    num += 2
    If continueCalculating Then
        startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
    End If
End Sub

Private NotAPrime As Boolean = False

このメソッドでは、次の奇数が素数かどうかがチェックされます。 素数の場合、メソッドによって bigPrimeTextBlock が直接更新され、その検出が反映されます。 これを実行できるのは、コンポーネントの作成に使用されたものと同じスレッドで計算が行われているためです。 計算に別のスレッドを使用することを選択した場合、より複雑な同期メカニズムを使用して、UI スレッドで更新を実行する必要があります。 この状況を次に示します。

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

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

グラフィカル アプリケーションでのブロック操作の処理は困難な場合があります。 イベント ハンドラーからはブロック メソッドを呼び出したくありません。これは、アプリケーションがフリーズしたように見えるためです。 別のスレッドを使用してこれらの操作を処理できますが、ワーカー スレッドからは GUI を直接変更できないため、完了したら、UI スレッドと同期する必要があります。 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);
        }
    }
}

Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Media
Imports System.Windows.Media.Animation
Imports System.Windows.Media.Imaging
Imports System.Windows.Shapes
Imports System.Windows.Threading
Imports System.Threading

Namespace SDKSamples
    Partial Public Class Window1
        Inherits Window
        ' Delegates to be used in placking jobs onto the Dispatcher.
        Private Delegate Sub NoArgDelegate()
        Private Delegate Sub OneArgDelegate(ByVal arg As String)

        ' Storyboards for the animations.
        Private showClockFaceStoryboard As Storyboard
        Private hideClockFaceStoryboard As Storyboard
        Private showWeatherImageStoryboard As Storyboard
        Private hideWeatherImageStoryboard As Storyboard

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Load the storyboard resources.
            showClockFaceStoryboard = CType(Me.Resources("ShowClockFaceStoryboard"), Storyboard)
            hideClockFaceStoryboard = CType(Me.Resources("HideClockFaceStoryboard"), Storyboard)
            showWeatherImageStoryboard = CType(Me.Resources("ShowWeatherImageStoryboard"), Storyboard)
            hideWeatherImageStoryboard = CType(Me.Resources("HideWeatherImageStoryboard"), Storyboard)
        End Sub

        Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Change the status image and start the rotation animation.
            fetchButton.IsEnabled = False
            fetchButton.Content = "Contacting Server"
            weatherText.Text = ""
            hideWeatherImageStoryboard.Begin(Me)

            ' Start fetching the weather forecast asynchronously.
            Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)

            fetcher.BeginInvoke(Nothing, Nothing)
        End Sub

        Private Sub FetchWeatherFromServer()
            ' Simulate the delay from network access.
            Thread.Sleep(4000)

            ' Tried and true method for weather forecasting - random numbers.
            Dim rand As New Random()
            Dim weather As String

            If rand.Next(2) = 0 Then
                weather = "rainy"
            Else
                weather = "sunny"
            End If

            ' Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
        End Sub

        Private Sub UpdateUserInterface(ByVal weather As String)
            'Set the weather image
            If weather = "sunny" Then
                weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
            ElseIf weather = "rainy" Then
                weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
            End If

            'Stop clock animation
            showClockFaceStoryboard.Stop(Me)
            hideClockFaceStoryboard.Begin(Me)

            'Update UI text
            fetchButton.IsEnabled = True
            fetchButton.Content = "Fetch Forecast"
            weatherText.Text = weather
        End Sub

        Private Sub HideClockFaceStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showWeatherImageStoryboard.Begin(Me)
        End Sub

        Private Sub HideWeatherImageStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showClockFaceStoryboard.Begin(Me, True)
        End Sub
    End Class
End Namespace

注意する必要がある詳細の一部を次に示します。

  • ボタン ハンドラーの作成

    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 Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        hideWeatherImageStoryboard.Begin(Me)
    
        ' Start fetching the weather forecast asynchronously.
        Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)
    
        fetcher.BeginInvoke(Nothing, Nothing)
    End Sub
    

ボタンをクリックすると、時計の描画が表示され、アニメーションが開始されます。 このボタンを無効にします。 新しいスレッドで 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);
    }
    
    Private Sub FetchWeatherFromServer()
        ' Simulate the delay from network access.
        Thread.Sleep(4000)
    
        ' Tried and true method for weather forecasting - random numbers.
        Dim rand As New Random()
        Dim weather As String
    
        If rand.Next(2) = 0 Then
            weather = "rainy"
        Else
            weather = "sunny"
        End If
    
        ' Schedule the update function in the UI thread.
        tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
    End Sub
    

簡単にするために、この例にはネットワーク コードを含めていません。 代わりに、ネットワーク アクセスの待機時間をシミュレートするために、新しいスレッドを 4 秒間スリープさせます。 このとき、元の UI スレッドはまだ実行中であり、イベントに応答しています。 これを示すために、アニメーションを実行したままにしました。最小化と最大化のボタンも引き続き機能します。

待機時間が完了し、天気予報をランダムに選択したら、UI スレッドに報告します。 これを行うには、そのスレッドの Dispatcher を使用して、UI スレッドで UpdateUserInterface の呼び出しをスケジュールします。 天気を説明する文字列を、このスケジュールされたメソッド呼び出しに渡します。

  • 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;
    }
    
    Private Sub UpdateUserInterface(ByVal weather As String)
        'Set the weather image
        If weather = "sunny" Then
            weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
        ElseIf weather = "rainy" Then
            weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
        End If
    
        'Stop clock animation
        showClockFaceStoryboard.Stop(Me)
        hideClockFaceStoryboard.Begin(Me)
    
        'Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weather
    End Sub
    

UI スレッド内の Dispatcher に時間がある場合、スケジュールされた UpdateUserInterface の呼び出しが実行されます。 このメソッドによって、時計のアニメーションが停止され、天気を説明する画像が選択されます。 この画像が表示され、"天気予報のフェッチ" ボタンが復元されます。

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

一部の WPF アプリケーションには、複数の最上位ウィンドウが必要です。 1 つのスレッドと Dispatcher の組み合わせで複数のウィンドウを管理することは完全に許容されていますが、複数のスレッドの方が適切にジョブを実行できる場合もあります。 これは、ウィンドウの 1 つがスレッドを独占する可能性がある場合に特に当てはまります。

Windows エクスプローラーはこの方法で動作します。 新しいエクスプローラー ウィンドウはそれぞれ元のプロセスに属しますが、独立したスレッドの制御下で作成されます。

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

実際、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();
        }
    }
}

Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Data
Imports System.Windows.Threading
Imports System.Threading


Namespace SDKSamples
    Partial Public Class Window1
        Inherits Window

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub OnLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
           placeHolder.Source = New Uri("http://www.msn.com")
        End Sub

        Private Sub Browse(ByVal sender As Object, ByVal e As RoutedEventArgs)
            placeHolder.Source = New Uri(newLocation.Text)
        End Sub

        Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
            newWindowThread.SetApartmentState(ApartmentState.STA)
            newWindowThread.IsBackground = True
            newWindowThread.Start()
        End Sub

        Private Sub ThreadStartingPoint()
            Dim tempWindow As New Window1()
            tempWindow.Show()
            System.Windows.Threading.Dispatcher.Run()
        End Sub
    End Class
End Namespace

このコードの次のスレッド セグメントは、このコンテキストで最も興味深いものです。

private void NewWindowHandler(object sender, RoutedEventArgs e)
{
    Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
    newWindowThread.SetApartmentState(ApartmentState.STA);
    newWindowThread.IsBackground = true;
    newWindowThread.Start();
}
Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
    Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
    newWindowThread.SetApartmentState(ApartmentState.STA)
    newWindowThread.IsBackground = True
    newWindowThread.Start()
End Sub

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

private void ThreadStartingPoint()
{
    Window1 tempWindow = new Window1();
    tempWindow.Show();
    System.Windows.Threading.Dispatcher.Run();
}
Private Sub ThreadStartingPoint()
    Dim tempWindow As New Window1()
    tempWindow.Show()
    System.Windows.Threading.Dispatcher.Run()
End Sub

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

技術的な詳細とつまずくポイント

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

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);
Public Class WeatherComponent
    Inherits Component
    'gets weather: Synchronous 
    Public Function GetWeather() As String
        Dim weather As String = ""

        'predict the weather

        Return weather
    End Function

    'get weather: Asynchronous 
    Public Sub GetWeatherAsync()
        'get the weather
    End Sub

    Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
End Class

Public Class GetWeatherCompletedEventArgs
    Inherits AsyncCompletedEventArgs
    Public Sub New(ByVal [error] As Exception, ByVal canceled As Boolean, ByVal userState As Object, ByVal weather As String)
        MyBase.New([error], canceled, userState)
        _weather = weather
    End Sub

    Public ReadOnly Property Weather() As String
        Get
            Return _weather
        End Get
    End Property
    Private _weather As String
End Class

Public Delegate Sub GetWeatherCompletedEventHandler(ByVal sender As Object, ByVal e As GetWeatherCompletedEventArgs)

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

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

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();
}
Public Class WeatherComponent2
    Inherits Component
    Public Function GetWeather() As String
        Return fetchWeatherFromServer()
    End Function

    Private requestingContext As DispatcherSynchronizationContext = Nothing

    Public Sub GetWeatherAsync()
        If requestingContext IsNot Nothing Then
            Throw New InvalidOperationException("This component can only handle 1 async request at a time")
        End If

        requestingContext = CType(DispatcherSynchronizationContext.Current, DispatcherSynchronizationContext)

        Dim fetcher As New NoArgDelegate(AddressOf Me.fetchWeatherFromServer)

        ' Launch thread
        fetcher.BeginInvoke(Nothing, Nothing)
    End Sub

    Private Sub [RaiseEvent](ByVal e As GetWeatherCompletedEventArgs)
        RaiseEvent GetWeatherCompleted(Me, e)
    End Sub

    Private Function fetchWeatherFromServer() As String
        ' do stuff
        Dim weather As String = ""

        Dim e As New GetWeatherCompletedEventArgs(Nothing, False, Nothing, weather)

        Dim callback As New SendOrPostCallback(AddressOf DoEvent)
        requestingContext.Post(callback, e)
        requestingContext = Nothing

        Return e.Weather
    End Function

    Private Sub DoEvent(ByVal e As Object)
        'do stuff
    End Sub

    Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
    Public Delegate Function NoArgDelegate() As String
End Class

入れ子になったポンプ

UI スレッドを完全にロックすることができない場合があります。 MessageBox クラスの Show メソッドについて考えてみましょう。 ユーザーが [OK] ボタンをクリックするまで Show は戻りません。 ただし、対話型にするためにメッセージ ループが必要なウィンドウが作成されます。 ユーザーが [OK] をクリックするまで待機している間に、元のアプリケーション ウインドウではユーザー入力に応答しません。 ただし、描画メッセージの処理は続行されます。 元のウィンドウは、隠れたときと、見えるようになったときに再描画されます。

[OK] ボタンのある MessageBox を示すスクリーンショット

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

この場合、MessageBox.Show の呼び出し時にプログラム コンテキストが PushFrame に保持され、新しいメッセージ ループが開始され、背景ウィンドウが再描画され、メッセージ ボックス ウィンドウへの入力が処理されます。 ユーザーが [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 が複数のスレッドからアクセスされることはないと想定して作業しているためです。 この場合、その 1 つのスレッドで予期しないタイミングで環境が変化し、本来は DispatcherObject 相互排他メカニズムで解決されるはずの悪影響を生じる可能性があります。 次の擬似コードを考えてみましょう。

スレッド処理の再入を示す図。

ほとんどの場合は適切ですが、WPF には、このような予期しない再入によって問題を発生する場合があります。 そのため、特定のキー時刻で、WPF によって DisableProcessing が呼び出されます。これにより、通常の CLR ロックではなく、WPF 再入可能なロックを使用するように、そのスレッドのロック命令が変更されます。

では、CLR チームがこの動作を選択したのはなぜでしょうか。 COM STA オブジェクトと終了処理スレッドに対応する必要がありました。 オブジェクトのガベージ コレクションが実行されると、その Finalize メソッドは、UI スレッドではなく、専用のファイナライザー スレッドで実行されます。 ここに問題があります。これは、UI スレッドで作成された COM STA オブジェクトは、UI スレッドでのみ破棄できるためです。 CLR では、(この場合は Win32 の SendMessage を使用して) BeginInvoke に相当する処理が実行されます。 ただし、UI スレッドがビジーの場合、ファイナライザー スレッドは停止し、COM STA オブジェクトを破棄できないため、重大なメモリ リークが発生します。 そのため、CLR チームは、ロックを適切に機能させるために難しい判断を下しました。

WPF のタスクは、メモリ リークを再発生させることなく、予期しない再入を回避することです。そのため、ここでは、あらゆる場所で再入をブロックしていません。

関連項目