スレッド モデル

Windows Presentation Foundation (WPF) は、開発者がスレッド処理の問題を回避できるように設計されています。 これにより、ほとんどの WPF 開発者が複数のスレッドを使用するインターフェイスを記述する必要がなくなります。 マルチスレッド プログラムは複雑でデバッグが困難なため、シングルスレッド ソリューションが存在する場合は回避することが推奨されます。

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

Note

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

同期呼び出しを行うには、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 に属するすべてのメソッドの最初に呼び出されることが想定されています。

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

Dispatcher によって、キュー内の要素が優先度順に並べ替えられます。 要素を Dispatcher キューに追加するときに指定できるレベルは 10 個あります。 これらの優先度は、DispatcherPriority 列挙体に維持されます。

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

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

このセクションの概念を示すサンプル アプリは、C# または Visual Basic 用に GitHub からダウンロードできます。

次の例を考えてみましょう。

Screenshot that shows threading of prime numbers.

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

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

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

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

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

Screenshot that shows the dispatcher queue.

Microsoft Word では、このメカニズムを使用してスペル チェックが実行されます。 スペル チェックは、UI スレッドのアイドル時間を使用してバックグラウンドで行われます。 コードを見てみましょう。

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

重要

この記事に示す XAML は、C# プロジェクトの XAML です。 Visual Basic の XAML は、XAML のバッキング クラスを宣言するときに若干異なります。

<Window x:Class="SDKSamples.PrimeNumber"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Prime Numbers" Width="360" Height="100">
    <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
        <Button Content="Start"  
                Click="StartStopButton_Click"
                Name="StartStopButton"
                Margin="5,0,5,0" Padding="10,0" />
        
        <TextBlock Margin="10,0,0,0">Biggest Prime Found:</TextBlock>
        <TextBlock Name="bigPrime" Margin="4,0,0,0">3</TextBlock>
    </StackPanel>
</Window>

コードビハインドの例を次に示します。

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

namespace SDKSamples
{
    public partial class PrimeNumber : Window
    {
        // Current number to check
        private long _num = 3;
        private bool _runCalculation = false;

        public PrimeNumber() =>
            InitializeComponent();

        private void StartStopButton_Click(object sender, RoutedEventArgs e)
        {
            _runCalculation = !_runCalculation;

            if (_runCalculation)
            {
                StartStopButton.Content = "Stop";
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
            }
            else
                StartStopButton.Content = "Resume";
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            _isPrime = true;

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

            // If a prime number, update the UI text
            if (_isPrime)
                bigPrime.Text = _num.ToString();

            _num += 2;
            
            // Requeue this method on the dispatcher
            if (_runCalculation)
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
        }

        private bool _isPrime = false;
    }
}
Imports System.Windows.Threading

Public Class PrimeNumber
    ' Current number to check
    Private _num As Long = 3
    Private _runCalculation As Boolean = False

    Private Sub StartStopButton_Click(sender As Object, e As RoutedEventArgs)
        _runCalculation = Not _runCalculation

        If _runCalculation Then
            StartStopButton.Content = "Stop"
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        Else
            StartStopButton.Content = "Resume"
        End If

    End Sub

    Public Sub CheckNextNumber()
        ' Reset flag.
        _isPrime = True

        For i As Long = 3 To Math.Sqrt(_num)
            If (_num Mod i = 0) Then

                ' Set Not a prime flag to true.
                _isPrime = False
                Exit For
            End If
        Next

        ' If a prime number, update the UI text
        If _isPrime Then
            bigPrime.Text = _num.ToString()
        End If

        _num += 2

        ' Requeue this method on the dispatcher
        If (_runCalculation) Then
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        End If
    End Sub

    Private _isPrime As Boolean
End Class

StartStopButton_Click ハンドラーは Button のテキストを更新するだけでなく、Dispatcher キューにデリゲートを追加して最初の素数チェックのスケジュールを設定する役割を持っています。 このイベント ハンドラーの処理が完了すると、Dispatcher によってこのデリゲートが選択され、実行されます。

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

public void CheckNextNumber()
{
    // Reset flag.
    _isPrime = true;

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

    // If a prime number, update the UI text
    if (_isPrime)
        bigPrime.Text = _num.ToString();

    _num += 2;
    
    // Requeue this method on the dispatcher
    if (_runCalculation)
        StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}

private bool _isPrime = false;
Public Sub CheckNextNumber()
    ' Reset flag.
    _isPrime = True

    For i As Long = 3 To Math.Sqrt(_num)
        If (_num Mod i = 0) Then

            ' Set Not a prime flag to true.
            _isPrime = False
            Exit For
        End If
    Next

    ' If a prime number, update the UI text
    If _isPrime Then
        bigPrime.Text = _num.ToString()
    End If

    _num += 2

    ' Requeue this method on the dispatcher
    If (_runCalculation) Then
        StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
    End If
End Sub

Private _isPrime As Boolean

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

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

一部の WPF アプリケーションには、複数の最上位ウィンドウが必要です。 1 つのスレッドとディスパッチャーの組み合わせで複数のウィンドウを管理することは全く問題ありませんが、複数のスレッドの方が適切にジョブを実行できる場合があります。 ウィンドウのうちの 1 つがスレッドを独占する可能性がある場合に、これは特に当てはまります。

Windows エクスプローラーはこの方法で動作します。 新しいエクスプローラー ウィンドウはそれぞれ元のプロセスに属しますが、独立したスレッドのコントロール下で作成されます。 ネットワーク リソースを探す場合など、エクスプローラーが応答しなくなると、他のエクスプローラー ウィンドウが引き続き応答し、使用できます。

この概念は次の例で示すことができます。

A screenshot of a WPF window that's duplicated four times. Three of the windows indicate that they're using the same thread, while the other two are on different threads.

このイメージの上位 3 つのウィンドウは同じスレッド識別子 1 を共有します。 他の 2 つのウィンドウには 9 と 4 という異なるスレッド識別子があります。 各ウィンドウの右上にはマゼンタ色の回転する !! 記号があります。

この例には、回転する ‼️ 記号、[一時停止] ボタン、現在のスレッドまたは新しいスレッドの下に新しいウィンドウを作成する他の 2 つのボタンを含むウィンドウが含まれています。 スレッドを 5 秒間一時停止する [一時停止] ボタンが押されるまで、‼️ 記号は常に回転します。 ウィンドウの下部にスレッド識別子が表示されます。

[一時停止] ボタンを押すと、同じスレッドのすべてのウィンドウが応答しなくなります。 別のスレッドのウィンドウは引き続き正常に動作します。

次の例は、ウィンドウへの XAML です。

<Window x:Class="SDKSamples.MultiWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Thread Hosted Window" Width="360" Height="180" SizeToContent="Height" ResizeMode="NoResize" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBlock HorizontalAlignment="Right" Margin="30,0" Text="‼️" FontSize="50" FontWeight="ExtraBold"
                   Foreground="Magenta" RenderTransformOrigin="0.5,0.5" Name="RotatedTextBlock">
            <TextBlock.RenderTransform>
                <RotateTransform Angle="0" />
            </TextBlock.RenderTransform>
            <TextBlock.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="RotatedTextBlock"
                                Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
                                From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </TextBlock.Triggers>
        </TextBlock>

        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
            <Button Content="Pause" Click="PauseButton_Click" Margin="5,0" Padding="10,0" />
            <TextBlock Margin="5,0,0,0" Text="<-- Pause for 5 seconds" />
        </StackPanel>

        <StackPanel Grid.Row="1" Margin="10">
            <Button Content="Create 'Same Thread' Window" Click="SameThreadWindow_Click" />
            <Button Content="Create 'New Thread' Window" Click="NewThreadWindow_Click" Margin="0,10,0,0" />
        </StackPanel>

        <StatusBar Grid.Row="2" VerticalAlignment="Bottom">
            <StatusBarItem Content="Thread ID" Name="ThreadStatusItem" />
        </StatusBar>

    </Grid>
</Window>

コードビハインドの例を次に示します。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace SDKSamples
{
    public partial class MultiWindow : Window
    {
        public MultiWindow() =>
            InitializeComponent();

        private void Window_Loaded(object sender, RoutedEventArgs e) =>
            ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}";

        private void PauseButton_Click(object sender, RoutedEventArgs e) =>
            Task.Delay(TimeSpan.FromSeconds(5)).Wait();

        private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
            new MultiWindow().Show();

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

        private void ThreadStartingPoint()
        {
            new MultiWindow().Show();

            System.Windows.Threading.Dispatcher.Run();
        }
    }
}
Imports System.Threading

Public Class MultiWindow
    Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
        ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}"
    End Sub

    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub

    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub

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

    Private Sub ThreadStartingPoint()
        Dim window As New MultiWindow()
        window.Show()

        System.Windows.Threading.Dispatcher.Run()
    End Sub
End Class

以下の詳細について確認する必要があります。

  • Task.Delay(TimeSpan) タスクは、[一時停止] ボタンが押されたときに現在のスレッドを 5 秒間一時停止 するために使用されます。

    private void PauseButton_Click(object sender, RoutedEventArgs e) =>
        Task.Delay(TimeSpan.FromSeconds(5)).Wait();
    
    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub
    
  • SameThreadWindow_Click イベント ハンドラーは、現在のスレッドで新しいウィンドウを表示します。 NewThreadWindow_Click イベント ハンドラーは、ThreadStartingPoint メソッドの実行を開始する新しいスレッドを作成します。次の箇条書きで説明するように、新しいウィンドウが表示されます。

    private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
        new MultiWindow().Show();
    
    private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
    {
        Thread newWindowThread = new Thread(ThreadStartingPoint);
        newWindowThread.SetApartmentState(ApartmentState.STA);
        newWindowThread.IsBackground = true;
        newWindowThread.Start();
    }
    
    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub
    
    Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint)
        newWindowThread.SetApartmentState(ApartmentState.STA)
        newWindowThread.IsBackground = True
        newWindowThread.Start()
    End Sub
    
  • ThreadStartingPoint メソッドは新しいスレッドの始点です。 このスレッドのコントロール下で新しいウィンドウを作成します。 WPF によって自動的に新しい System.Windows.Threading.Dispatcher が作成され、新しいスレッドが管理されます。 ウィンドウを機能させるために必要なことは、System.Windows.Threading.Dispatcher の開始のみです。

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

このセクションの概念を示すサンプル アプリは、C# または Visual Basic 用に GitHub からダウンロードできます。

Task.Run を使用してブロック操作を処理する

グラフィカル アプリケーションでのブロック操作の処理は困難な場合があります。 イベント ハンドラーからはブロック メソッドを呼び出したくありません。これは、アプリケーションがフリーズしたように見えるためです。 前の例では、独自のスレッドに新しいウィンドウを作成し、各ウィンドウが互いに独立して実行されるようにしました。 System.Windows.Threading.Dispatcher を使用して新しいスレッドを作成できますが、作業が完了した後、新しいスレッドをメイン UI スレッドと同期することが困難になります。 新しいスレッドは UI を直接変更できないため、UI スレッドに委任 Dispatcher を挿入するために、Dispatcher.InvokeAsyncDispatcher.BeginInvoke または Dispatcher.Invoke を使用する必要があります。 最終的に、これらのデリゲートは、UI 要素を変更するアクセス許可を使用して実行されます。

結果を同期しながら、新しいスレッドでコードを実行する簡単な方法として、タスク ベースの非同期パターン (TAP) があります。 これは、非同期操作を表すために使用される、System.Threading.Tasks 名前空間内の Task および Task<TResult> 型に基づいています。 TAP では、非同期操作の開始と終了を表すために単一のメソッドが使用されます。 このパターンにはいくつかの利点があります。

  • Task の呼び出し元は、コードを非同期的または同期的に実行することを選択できます。
  • 進行状況は、Task から報告できます。
  • 呼び出し元のコードは、実行を中断し、操作の結果を待機できます。

Task.Run の例

この例では、天気予報を取得するリモート プロシージャ コールを模倣しています。 ボタンがクリックされると、UI が更新され、データ フェッチが進行中であることが示されます。一方、天気予報のフェッチを模倣するためのタスクが開始されます。 このタスクが開始されると、ボタン イベント ハンドラー コードはタスクが完了するまで中断されます。 タスクが完了した後、イベント ハンドラー コードは引き続き実行されます。 このコードは中断され、UI スレッドの残りの部分をブロックしません。 WPF の同期コンテキストが、コードの中断を処理します。これにより、WPF を引き続き実行できるようになります。

A diagram that demonstrates the workflow of the example app.

サンプル アプリのワークフローを示す図。 アプリには、"予報を取得" というテキストを含む 1 つのボタンがあります。 ボタンを押した後にアプリの次のフェーズを指す矢印があります。これは、アプリがデータを取得中であることを示す時計の画像です。 しばらくすると、データの結果に応じて、太陽または雨雲の画像でアプリが返されます。

このセクションの概念を示すサンプル アプリは、C# または Visual Basic 用に GitHub からダウンロードできます。 この例の XAML のサイズは非常に大きく、この記事では提供されていません。 前の GitHub リンクを使用して XAML を参照します。 XAML は、1 つのボタンを使用して天気をフェッチします。

XAML のコードビハインドについて考えてみましょう。

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Threading.Tasks;

namespace SDKSamples
{
    public partial class Weather : Window
    {
        public Weather() =>
            InitializeComponent();

        private async void FetchButton_Click(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);

            // Asynchronously fetch the weather forecast on a different thread and pause this code.
            string weather = await Task.Run(FetchWeatherFromServerAsync);

            // After async data returns, process it...
            // Set the weather image
            if (weather == "sunny")
                weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];

            else if (weather == "rainy")
                weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];

            //Stop clock animation
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
            ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
            
            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;
        }

        private async Task<string> FetchWeatherFromServerAsync()
        {
            // Simulate the delay from network access
            await Task.Delay(TimeSpan.FromSeconds(4));

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

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

        private void HideClockFaceStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowWeatherImageStoryboard"]).Begin(ClockImage);

        private void HideWeatherImageStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Begin(ClockImage, true);
    }
}
Imports System.Windows.Media.Animation

Public Class Weather

    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)

        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)

        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)

        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)

        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)

        End If

        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)

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

    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)

        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))

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

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

    End Function

    Private Sub HideClockFaceStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowWeatherImageStoryboard"), Storyboard).Begin(ClockImage)
    End Sub

    Private Sub HideWeatherImageStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Begin(ClockImage, True)
    End Sub
End Class

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

  • ボタン イベント ハンドラー

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    イベント ハンドラーが async (または Visual Basic の Async) を使用して宣言されていることに注意してください。 "async" メソッドを使用すると、待機中のメソッド (FetchWeatherFromServerAsync など) が呼び出されたときにコードを中断できます。 これは、await (または Visual Basic を使用した Await) キーワードによって指定されます。 FetchWeatherFromServerAsync が完了するまで、ボタンのハンドラー コードは中断され、非呼び出し元にコントロールが返されます。 これは同期メソッドに似ていますが、同期メソッドはメソッド内のすべての操作が完了するまで待機し、その後コントロールが呼び出し元に返される点が違います。

    待機中のメソッドは、現在のメソッド (ボタン ハンドラーの場合は UI スレッド) のスレッド コンテキストを利用します。 つまり、呼び出し元 await FetchWeatherFromServerAsync(); (または Visual Basic を使用した Await FetchWeatherFromServerAsync()) により、FetchWeatherFromServerAsync 内のコードは UI スレッドで実行されますが、これを実行する時間があるディスパッチャーでは実行されません。これは、実行時間の長い計算を含むシングル スレッド アプリの動作の例と同様です。 ただし、await Task.Run が使用されていることに注意してください。 これにより、現在のスレッドではなく、指定されたタスクのスレッド プールに新しいスレッドが作成されます。 そのため、FetchWeatherFromServerAsync は独自のスレッドで実行されます。

  • 天気のフェッチ

    private async Task<string> FetchWeatherFromServerAsync()
    {
        // Simulate the delay from network access
        await Task.Delay(TimeSpan.FromSeconds(4));
    
        // Tried and true method for weather forecasting - random numbers
        Random rand = new Random();
    
        if (rand.Next(2) == 0)
            return "rainy";
        
        else
            return "sunny";
    }
    
    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)
    
        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))
    
        ' Tried and true method for weather forecasting - random numbers
        Dim rand As New Random()
    
        If rand.Next(2) = 0 Then
            Return "rainy"
        Else
            Return "sunny"
        End If
    
    End Function
    

    簡略化するために、この例にはネットワーク コードを含めていません。 代わりに、ネットワーク アクセスの待機時間をシミュレートするために、新しいスレッドを 4 秒間スリープさせます。 この時点で、元の UI スレッドは引き続き実行され、UI イベントに応答します。新しいスレッドが完了するまで、ボタンのイベント ハンドラーは一時停止します。 これを示すために、アニメーションを実行したままにしました。また、ウィンドウのサイズを変更できます。 UI スレッドが一時停止または遅延した場合、アニメーションは表示されず、ウィンドウを操作できません。

    Task.Delay が完了し、天気予報をランダムに選択した場合、呼び出し元に天気の状態が返されます。

  • UI の更新

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    タスクが完了し、UI スレッドに時間があると、Task.Run の呼び出し元、ボタンのイベント ハンドラーが再開されます。 このメソッドの残りが時計のアニメーションを停止し、天気を説明する画像を選択します。 この画像が表示され、"天気予報を取得" ボタンが有効化されます。

このセクションの概念を示すサンプル アプリは、C# または Visual Basic 用に GitHub からダウンロードできます。

技術的な詳細と問題となるポイント

次のセクションでは、マルチスレッドで遭遇する可能性があるいくつかの詳細と問題となるポイントについて説明します。

入れ子になったポンプ

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

Screenshot that shows a MessageBox with an OK button

何らかのスレッドがメッセージ ボックス ウィンドウを担当する必要があります。 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 でこのイベントを処理するためにかなりの時間がかかる可能性があります。 handler2PushFrame を使用することで、何時間も返されない入れ子になったメッセージ ループを開始することがあります。 このメッセージ ループの完了時に handler2 によってイベントが処理済みとマークされない場合、イベントは、非常に古くても、ツリーの上位に渡されます。

再入とロック

ロックを要求するとスレッドによる操作が完全に停止すると予想するかもしれませんが、共通言語ランタイム (CLR) のロック メカニズムは、予想どおりには動作しません。 実際には、スレッドでは引き続き優先度の高いメッセージが受信され、処理されます。 これにより、デッドロックを防止し、インターフェイスの応答性を最小限に抑えることができますが、軽度のバグが発生する可能性があります。 ほとんどの場合、この点について理解する必要はありません。ただし、まれな状況 (通常は Win32 ウィンドウ メッセージまたは COM STA コンポーネントが関係しています) において、この点を理解しておくと役に立つ場合があります。

ほとんどのインターフェイスは、スレッド セーフを考慮して構築されていません。開発者は、UI が複数のスレッドからアクセスされることはないと想定して作業しているためです。 この場合、その 1 つのスレッドで予期しないタイミングで環境が変化し、本来は DispatcherObject 相互排他メカニズムで解決されるはずの悪影響を生じる可能性があります。 次の擬似コードを考えてみましょう。

Diagram that shows threading reentrancy.

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

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

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

関連項目