スレッド モデル
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> を渡すことにより、同期呼び出しを行うことができます。
概要とディスパッチャー
通常は UI です。 UI スレッドで入力を受け取り、イベントを処理し、画面を描画し、アプリケーション コードを実行する間、レンダリング スレッドは表示されずにバックグラウンドで効果的に実行されます。 ほとんどのアプリケーションでは 1 つの UI スレッドを使用しますが、複数を使用することが最適な状況もあります。 これについては後で例を使って説明します。
UI スレッドにより、Dispatcher というオブジェクト内の作業項目がキューに格納されます。 Dispatcher は作業項目を優先順位に従って選択し、それぞれを最後まで実行します。 すべての UI スレッドには少なくとも 1 つの Dispatcher が必要であり、各 Dispatcher では 1 つのスレッドで作業項目を実行できます。
応答性の高いユーザー フレンドリなアプリケーションを構築する秘訣は、作業項目を小さく保って Dispatcher のスループットを最大化することです。 このようにすると、処理の待機中に Dispatcher キューに格納されている項目が古くなることはなくなります。 入力と応答の間に知覚可能な遅延があると、ユーザーに不満が生じる可能性があります。
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 クラスには、作業項目を登録するための 2 つのメソッド Invoke と BeginInvoke が用意されています。 どちらのメソッドでも、デリゲートの実行がスケジュールされます。 Invoke は同期呼び出しです。つまり、UI スレッドでデリゲートの実行が実際に完了するまで戻りません。 BeginInvoke は非同期であり、すぐに戻ります。
Dispatcher によって、キュー内の要素が優先度順に並べ替えられます。 要素を Dispatcher キューに追加するときに指定できるレベルは 10 個あります。 これらの優先度は、DispatcherPriority 列挙体に維持されます。 DispatcherPriority レベルの詳細については、Windows SDK のドキュメントを参照してください。
動作中のスレッド:サンプル
実行時間の長い計算を使用するシングルスレッド アプリケーション
ほとんどのグラフィカル ユーザー インターフェイス (GUI) では、ユーザーの操作に応じて生成されるイベントを待機する間、アイドルの状態で大部分の時間が費やされます。 このアイドル時間は、慎重にプログラミングすることで、UI の応答性に影響を与えることなく、建設的に利用できます。 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
このメソッドでは、次の奇数が素数かどうかがチェックされます。 素数の場合、メソッドによって bigPrime
TextBlock が直接更新され、その検出が反映されます。 これを実行できるのは、コンポーネントの作成に使用されたものと同じスレッドで計算が行われているためです。 計算に別のスレッドを使用することを選択した場合、より複雑な同期メカニズムを使用して、UI スレッドで更新を実行する必要があります。 この状況を次に示します。
このサンプルの完全なソース コードについては、実行時間が長い計算があるシングルスレッド アプリケーションのサンプルに関するページを参照してください。
バックグラウンド スレッドを使用したブロック操作の処理
グラフィカル アプリケーションでのブロック操作の処理は困難な場合があります。 イベント ハンドラーからはブロック メソッドを呼び出したくありません。これは、アプリケーションがフリーズしたように見えるためです。 別のスレッドを使用してこれらの操作を処理できますが、ワーカー スレッドからは GUI を直接変更できないため、完了したら、UI スレッドと同期する必要があります。 Invoke または BeginInvoke を使用して、デリゲートを UI スレッドの Dispatcher に挿入できます。 最終的に、これらのデリゲートは、UI 要素を変更するアクセス許可を使用して実行されます。
この例では、天気予報を取得するリモート プロシージャ コールを模倣しています。 別のワーカー スレッドを使用してこの呼び出しを実行し、完了したら UI スレッドの Dispatcher で更新メソッドをスケジュールします。
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] をクリックするまで待機している間に、元のアプリケーション ウインドウではユーザー入力に応答しません。 ただし、描画メッセージの処理は続行されます。 元のウィンドウは、隠れたときと、見えるようになったときに再描画されます。
何らかのスレッドがメッセージ ボックス ウィンドウを担当する必要があります。 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 のタスクは、メモリ リークを再発生させることなく、予期しない再入を回避することです。そのため、ここでは、あらゆる場所で再入をブロックしていません。