スレッド モデル

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

しかし、どのように適切に設計されていたとしても、UI フレームワークでは、すべての種類の問題に対してシングル スレッド ソリューションを提供することはできません。WPF でも対策は講じられていますが、マルチスレッド化によってuser interface (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 のドキュメントを参照してください。

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

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

ほとんどのgraphical user interfaces (GUIs) は、ユーザーの操作に応答してイベントが生成されるまで、アイドル状態で待機しています。注意してプログラミングすることによって、このアイドル時間は、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>
<Window x:Class="SDKSamples.MainWindow"
    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>

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

Imports System
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
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;
    }
}

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

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
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 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
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;

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

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

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

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

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

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


Imports System
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
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 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 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 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 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 への呼び出しをスケジュールします。天気を表す文字列を、このスケジュールされたメソッド呼び出しに渡します。

  • UI の更新

            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 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>

Imports System
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("https://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
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 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 NewWindowHandler(object sender, RoutedEventArgs e)
{       
    Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
    newWindowThread.SetApartmentState(ApartmentState.STA);
    newWindowThread.IsBackground = true;
    newWindowThread.Start();
}

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

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

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

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

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

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

    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)
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 Formsまたは ASP.NET プログラムでは使用できません。

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

    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
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 がイベントを処理済みとしてマークしない場合、イベントは古くなっていてもツリーの上位に渡されます。

再入およびロック

common language runtime (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 のタスクは、メモリ リークを再発生させずに予期しない再入を回避することであり、再入のブロックをすべての場所で行うわけではないのはそのためです。

参照

その他の技術情報

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