Threading-Modell
Windows Presentation Foundation (WPF) dient dazu, Entwicklern die Schwierigkeiten des Threadings zu ersparen. Die meisten WPF-Entwickler müssen daher keine Schnittstellen mit mehr als einem Thread schreiben. Da Multithreadprogramme komplex und schwierig zu debuggen sind, sollten sie vermieden werden, wenn Singlethreadlösungen zur Verfügung stehen.
Doch so gut die Architektur auch sein mag, ist kein UInframework in der Lage, eine Singlethreadlösung für jedes Problem bereitzustellen. Obwohl WPF diesem Ideal nahe kommt, gibt es immer noch Situationen, in denen die Reaktionsgeschwindigkeit der user interface (UI) oder die Anwendungsleistung durch mehrere Threads verbessert wird. Nach der Erläuterung von Hintergrundmaterial werden in diesem Beitrag einige dieser Situationen untersucht, bevor zum Schluss tiefergehende Details erörtert werden.
Dieses Thema enthält folgende Abschnitte.
- Übersicht und Verteiler
- Threads in Aktion: Beispiele
- Technische Details und Stolpersteine
- Verwandte Abschnitte
Übersicht und Verteiler
In der Regel beginnen WPF-Anwendungen mit zwei Threads: einem für die Behandlung des Renderings und einem weiteren zum Verwalten der UI. Der Renderingthread wird effektiv verborgen im Hintergrund ausgeführt, während der UInthread Eingaben empfängt, Ereignisse behandelt, den Bildschirm zeichnet und Anwendungscode ausführt. Die meisten Anwendungen verwenden nur einen UInthread, obwohl in manchen Situationen die Verwendung mehrerer Threads vorteilhaft ist. Dies wird an späterer Stelle anhand eines Beispiels erörtert.
Der UInthread reiht Arbeitsaufgaben innerhalb eines als Dispatcher bezeichneten Objekts in eine Warteschlange ein. Der Dispatcher wählt Arbeitsaufgaben auf Grundlage einer Priorität aus und führt jede Aufgabe bis zu ihrem Abschluss aus. Jeder UInthread muss über mindestens einen Dispatcher verfügen, und jeder Dispatcher kann Arbeitsaufgaben in genau einem Thread ausführen.
Der Schlüssel zum Erstellen reaktionsschneller, benutzerfreundlicher Anwendungen besteht darin, den Dispatcher-Durchsatz zu optimieren, indem die Größe der Arbeitsaufgaben gering gehalten wird. Auf diese Weise veralten die Elemente in der Dispatcher-Warteschlange nicht, wo sie auf die Verarbeitung warten. Jede spürbare Verzögerung zwischen Eingabe und Antwort kann einen Benutzer frustrieren.
Wie aber sollen WPF-Anwendungen dann umfangreiche Vorgänge behandeln? Was, wenn Ihr Code eine aufwändige Berechnung enthält oder eine Datenbankabfrage auf einem Remoteserver durchgeführt werden muss? Normalerweise wird in einem solchen Fall der umfangreiche Vorgang in einen separaten Thread ausgelagert, sodass der UInthread für Elemente in der Dispatcher-Warteschlange zur Verfügung steht. Nachdem der umfangreiche Vorgang abgeschlossen ist, kann er sein Ergebnis zur Anzeige an den UInthread zurückübermitteln.
Traditionell ermöglicht Windows nur dem Thread den Zugriff auf die UInelemente, der sie erstellt hat. Dies bedeutet, dass ein für eine zeitintensive Aufgabe zuständiger Hintergrundthread ein Textfeld bei seiner Beendigung nicht aktualisieren kann. Dadurch stellt Windows die Integrität von UInkomponenten sicher. Ein Listenfeld könnte merkwürdig aussehen, wenn sein Inhalt während des Zeichnens von einem Hintergrundthread aktualisiert würde.
WPF verfügt über einen integrierten gegenseitigen Ausschlussmechanismus, der diese Koordination erzwingt. Die meisten Klassen in WPF sind von DispatcherObject abgeleitet. Beim Erstellen speichert ein DispatcherObject einen Verweis auf den Dispatcher, der mit dem aktuell ausgeführten Thread verknüpft ist. Tatsächlich ist das DispatcherObject dem Thread zugeordnet, von dem es erstellt wird. Während der Programmausführung kann ein DispatcherObject seine öffentliche VerifyAccess-Methode aufrufen. VerifyAccess überprüft den Dispatcher, der dem aktuellen Thread zugeordnet ist, und vergleicht ihn mit dem während der Erstellung gespeicherten Dispatcher-Verweis. Stimmen sie nicht überein, löst VerifyAccess eine Ausnahme aus. VerifyAccess soll zu Beginn jeder Methode aufgerufen werden, die zu einem DispatcherObject gehört.
Wenn nur ein Thread die UI ändern kann, wie interagieren Hintergrundthreads dann mit Benutzern? Ein Hintergrundthread kann den UInthread auffordern, einen Vorgang in seinem Auftrag auszuführen. Zu diesem Zweck registriert er eine Arbeitsaufgabe im Dispatcher des UInthreads. Die Dispatcher-Klasse stellt zwei Methoden zum Registrieren von Arbeitsaufgaben bereit: Invoke und BeginInvoke. Beide Methoden planen einen Delegaten für die Ausführung. Invoke ist ein synchroner Aufruf, d. h. er gibt erst dann ein Ergebnis zurück, wenn der UInthread die Ausführung des Delegaten tatsächlich beendet. BeginInvoke ist asynchron und gibt sein Ergebnis sofort zurück.
Der Dispatcher ordnet die Elemente in seiner Warteschlange nach Priorität an. Es gibt zehn Ebenen, die angegeben werden können, wenn der Dispatcher-Warteschlange ein Element hinzugefügt wird. Diese Prioritäten werden in der DispatcherPriority-Enumeration beibehalten. Ausführliche Informationen über DispatcherPriority-Ebenen finden Sie in der Windows SDK-Dokumentation.
Threads in Aktion: Beispiele
Singlethread-Anwendung mit einer Berechnung mit langer Laufzeit
Die meisten graphical user interfaces (GUIs) verbringen einen großen Teil ihrer Zeit im Leerlauf, während sie auf Ereignisse warten, die als Reaktion auf Benutzerinteraktionen generiert werden. Bei sorgfältiger Programmierung kann diese Leerlaufzeit konstruktiv genutzt werden, ohne die Reaktionszeit der UI zu beeinflussen. Das WPF-Threadmodell lässt nicht zu, dass Eingaben einen Vorgang im UInthread unterbrechen. Daher müssen Sie regelmäßig zum Dispatcher zurückkehren, um ausstehende Eingabeereignisse zu verarbeiten, bevor sie veralteten.
Betrachten Sie das folgende Beispiel:
Diese einfache Anwendung zählt von drei an aufwärts und sucht dabei nach Primzahlen. Wenn der Benutzer auf die Schaltfläche Start klickt, beginnt die Suche. Wenn das Programm eine Primzahl findet, wird die Benutzeroberfläche entsprechend aktualisiert. Der Benutzer kann die Suche jederzeit beenden.
Obwohl es sich bei der Primzahlensuche um eine einfache Aufgabe handelt, könnte sie endlos weitergehen, was einige Schwierigkeiten mit sich bringt. Wenn die gesamte Suche innerhalb des Handlers für das Click-Ereignis der Schaltfläche verarbeitet wird, hat der UInthread nie die Möglichkeit, andere Ereignisse zu behandeln. Die UI wäre nicht in der Lage, auf Eingaben zu reagieren oder Meldungen zu verarbeiten. Sie würde nie aktualisiert werden und nie auf Schaltflächenklicks reagieren.
Die Primzahlensuche könnte in einem separaten Thread ausgeführt werden, doch dann müssten Synchronisierungsprobleme abgefangen werden. Mit einem Singlethread-Ansatz kann die Bezeichnung, die die größte gefundene Primzahl auflistet, direkt aktualisiert werden.
Wenn die Berechnungsaufgabe in einfach zu handhabende Abschnitte aufgeteilt wird, können Sie regelmäßig zum Dispatcher zurückkehren und Ereignisse verarbeitet. WPF erhält die Möglichkeit, Eingaben neu zu zeichnen und zu verarbeiten.
Die beste Art, Verarbeitungszeit zwischen der Berechnung und der Ereignisbehandlung aufzuteilen, besteht darin, die Berechnung vom Dispatcher aus zu verwalten. Mit der BeginInvoke-Methode lassen sich Primzahlüberprüfungen in der gleichen Warteschlange planen, aus der auch UInereignisse stammen. In diesem Beispiel wird jeweils nur eine Primzahlüberprüfung geplant. Nachdem die Primzahlüberprüfung abgeschlossen ist, wird sofort die nächste Überprüfung geplant. Diese Überprüfung wird erst fortgesetzt, nachdem ausstehende UInereignisse behandelt wurden.
Mit diesem Mechanismus führt Microsoft Word die Rechtschreibprüfung aus. Die Rechtschreibprüfung wird im Hintergrund in der Leerlaufzeit des UInthreads ausgeführt. Sehen Sie sich einmal den Code an.
Im folgenden Beispiel wird der XAML-Code gezeigt, mit dem die Benutzeroberfläche erstellt wird.
<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>
Das folgende Beispiel zeigt den Code-Behind.
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;
}
}
Im folgenden Beispiel wird der Ereignishandler für die Button dargestellt.
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));
}
}
Außer für die Textaktualisierung von Button ist dieser Handler für die Planung der ersten Primzahlenüberprüfung verantwortlich, indem er der Dispatcher-Warteschlange einen Delegaten hinzufügt. Nachdem dieser Ereignishandler seine Arbeit abgeschlossen hat, wählt der Dispatcher diesen Delegaten zur Ausführung aus.
Wie bereits erwähnt, ist BeginInvoke der Dispatcher-Member, mit dem ein Delegat zur Ausführung geplant wird. In diesem Fall wird die SystemIdle-Priorität ausgewählt. Der Dispatcher führt diesen Delegaten nur aus, wenn keine wichtigen zu verarbeitenden Ereignisse vorhanden sind. Die Reaktionsgeschwindigkeit der UI ist wichtiger als die Zahlenüberprüfung. Außerdem wird ein neuer Delegat übergeben, der die Routine für die Zahlenüberprüfung darstellt.
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;
Diese Methode überprüft, ob es sich bei der nächsten ungeraden Zahl um eine Primzahl handelt. Ist das der Fall, aktualisiert die Methode direkt den bigPrime TextBlock, um auf die erkannte Primzahl hinzuweisen. Dies ist möglich, weil die Berechnung im selben Thread stattfindet, mit dem die Komponente erstellt wurde. Wäre ein anderer Thread für die Berechnung ausgewählt worden, wäre ein komplizierterer Synchronisierungsmechanismus notwendig, und die Aktualisierung müsste im UInthread ausgeführt werden. Diese Situation wird im Folgenden dargestellt.
Den vollständigen Quellcode für dieses Beispiel finden Sie unter Beispiel für eine Singlethread-Anwendung mit Berechnung mit langer Laufzeit
Behandeln eines Blockierungsvorgangs mithilfe eines Hintergrundthreads
Die Behandlung von Blockierungsvorgängen in einer grafischen Anwendung kann schwierig sein. Blockierungmethoden sollen nicht von Ereignishandlern aufgerufen werden, da die Anwendung nicht mehr zu reagieren scheint. Diese Vorgänge können mithilfe eines separaten Threads behandelt werden, aber anschließend muss eine Synchronisierung mit dem UInthread erfolgen, da die GUI nicht direkt vom Arbeitsthread aus geändert werden kann. Mithilfe von Invoke oder BeginInvoke können Delegaten in den Dispatcher des UInthreads eingefügt werden. Letztlich werden diese Delegaten mit der Berechtigung ausgeführt, UInelemente zu ändern.
In diesem Beispiel soll ein Remoteprozeduraufruf eine Wettervorhersage abrufen. Dieser Aufruf wird mithilfe eines separaten Arbeitsthreads ausgeführt, und anschließend wird eine Aktualisierungsmethode im Dispatcher des UI-Threads geplant.
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);
}
}
}
Im Folgenden finden Sie einige Details, die beachtet werden müssen.
Erstellen des Schaltflächenhandlers
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); }
Wenn auf die Schaltfläche geklickt wird, wird die Uhrzeichnung angezeigt und animiert. Die Schaltfläche wird deaktiviert. Die FetchWeatherFromServer-Methode wird in einem neuen Thread aufgerufen, und nach der Rückkehr kann der Dispatcher Ereignisse verarbeiten, während auf den Abruf der Wettervorhersage gewartet wird.
Abrufen der Wettervorhersage
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); }
Um dieses Beispiel möglichst einfach zu halten, enthält es keinen Netzwerkcode. Stattdessen wird die Verzögerung beim Netzwerkzugriff simuliert, indem der neue Thread für vier Sekunden in den Ruhezustand versetzt wird. In dieser Zeit wird der ursprüngliche UInthread weiter ausgeführt, und er reagiert auf Ereignisse. Um dies zu verdeutlichen, wird eine Animation weiter ausgeführt, und die Schaltflächen zum Minimieren und Maximieren funktionieren ebenfalls nach wie vor.
Wenn die Verzögerung beendet wird und die Wettervorhersagen zufällig ausgewählt wurden, ist es Zeit, das Ergebnis an den UInthread zu melden. Dazu wird ein Aufruf von UpdateUserInterface im UInthread mit dem Dispatcher dieses Threads geplant. Es wird eine Zeichenfolge übergeben, die das Wetter für diesen geplanten Methodenaufruf beschreibt.
Aktualisieren der 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; }
Wenn der Dispatcher im UInthread Zeit hat, führt er den geplanten Aufruf von UpdateUserInterface aus. Diese Methode beendet die Uhranimation und wählt ein Bild aus, um das Wetter zu beschreiben. Dieses Bild wird angezeigt, und die Schaltfläche "fetch forecast" wird wiederhergestellt.
Mehrere Fenster, mehrere Threads
Einige WPF-Anwendungen erfordern mehrere Fenster der obersten Ebene. Es spricht absolut nichts dagegen, dass eine Kombination aus Thread und Dispatcher mehrere Fenster verwaltet, mitunter eignen sich mehrere Threads jedoch besser. Dies gilt umso mehr, wenn die Möglichkeit besteht, dass eines der Fenster den Thread für sich allein beansprucht.
Windows-Explorer arbeitet auf diese Weise. Jedes neue Explorer-Fenster gehört zum ursprünglichen Prozess, seine Erstellung wird jedoch von einem unabhängigen Thread gesteuert.
Webseiten können mithilfe eines WPF-Frame-Steuerelements angezeigt werden. Ein einfacher Internet Explorer-Ersatz kann leicht erstellt werden. Am Anfang steht eine wichtige Funktion: die Fähigkeit, ein neues Explorer-Fenster zu öffnen. Wenn der Benutzer auf die Schaltfläche "Neues Fenster" klickt, wird eine Kopie des Fensters in einem separaten Thread geöffnet. Auf diese Weise sperren Vorgänge mit langer Laufzeit oder Blockierungsvorgänge in einem der Fenster nicht alle anderen Fenster.
In Wirklichkeit besitzt das Webbrowsermodell ein eigenes kompliziertes Threadmodell. Es ist deshalb ausgewählt worden, weil die meisten Leser damit vertraut sein sollten.
Im folgenden Beispiel wird der Code gezeigt.
<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();
}
}
}
Die folgenden Threadsegmente dieses Codes sind in diesem Kontext die interessantesten:
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();
}
Diese Methode wird aufgerufen, wenn auf die Schaltfläche "Neues Fenster" geklickt wird. Durch sie wird ein neuer Thread erstellt und asynchron gestartet.
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();
}
Diese Methode ist der Ausgangspunkt für den neuen Thread. Unter dem Steuerelement dieses Threads wird ein neues Fenster erstellt. WPF erstellt automatisch einen neuen Dispatcher, um den neuen Thread zu verwalten. Damit das Fenster funktionsfähig ist, muss lediglich der Dispatcher gestartet werden.
Technische Details und Stolpersteine
Schreiben von Komponenten mithilfe von Threading
Im Microsoft .NET Framework-Entwicklerhandbuch wird ein Muster beschrieben, anhand dessen eine Komponente für ihre Clients asynchrones Verhalten verfügbar machen kann (siehe Übersicht über ereignisbasierte asynchrone Muster). Angenommen, die FetchWeatherFromServer-Methode soll in eine wiederverwendbare, nicht grafische Komponente gepackt werden. Nach dem Microsoft .NET Framework-Standardmuster würde dies ungefähr wie folgt aussehen.
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 würde eine der zuvor beschriebenen Techniken verwenden, z. B. das Erstellen eines Hintergrundthreads, um die Arbeit asynchron auszuführen und den aufrufenden Thread nicht zu blockieren.
Einer der interessantesten Teile dieses Musters ist der Aufruf der MethodNameCompleted-Methode im selben Thread, der zu Beginn die MethodNameAsync-Methode aufgerufen hat. Dies kann sehr leicht mit WPF durch das Speichern von CurrentDispatcher erreicht werden, aber dann kann die nicht grafische Komponente nur in WPF-Anwendungen verwendet werden, nicht in Windows Forms-Programmen oder in ASP.NET-Programmen.
Für diese Anforderung wurde die DispatcherSynchronizationContext-Klasse konzipiert. Stellen Sie sich diese Klasse als vereinfachte Version von Dispatcher vor, die auch mit anderen UI-Frameworks funktioniert.
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();
}
Geschachteltes Verschieben
Manchmal ist es nicht möglich, den UInthread vollständig zu sperren. Sehen Sie sich die Show-Methode der MessageBox-Klasse an. Show wird erst zurückgegeben, wenn der Benutzer auf die Schaltfläche "OK" klickt. Allerdings wird ein Fenster erstellt, das eine Meldungsschleife enthalten muss, um interaktiv zu sein. Während gewartet wird, dass der Benutzer auf "OK" klickt, reagiert das ursprüngliche Anwendungsfenster nicht auf Benutzereingaben. Es verarbeitet jedoch weiterhin Zeichenmeldungen. Wenn das ursprüngliche Fenster verdeckt und dann angezeigt wird, wird es aktualisiert.
Es muss einen Thread geben, der das Meldungsfenster steuert. WPF könnte nur für das Meldungsfenster einen neuen Thread erstellen, dieser Thread wäre jedoch nicht in der Lage, die deaktivierten Elemente im ursprünglichen Fenster zu zeichnen (der oben erwähnte gegenseitige Ausschlussmechanismus). Stattdessen verwendet WPF ein geschachteltes System zur Meldungsverarbeitung. Die Dispatcher-Klasse enthält eine spezielle Methode mit dem Namen PushFrame, die den aktuellen Ausführungspunkt einer Anwendung speichert und dann eine neue Meldungsschleife beginnt. Nach Beendigung der geschachtelten Meldungsschleife wird die Ausführung nach dem ursprünglichen PushFrame-Aufruf fortgesetzt.
In diesem Fall behält PushFrame den Programmkontext beim Aufruf von MessageBox.Show bei und beginnt eine neue Meldungsschleife, um das Hintergrundfenster neu zu zeichnen und Eingaben für das Meldungsfenster zu behandeln. Wenn der Benutzer auf OK klickt und das Popupfenster schließt, wird die geschachtelte Schleife beendet, und das Steuerelement wird nach dem Aufruf von Show fortgesetzt.
Veraltete Routingereignisse
Das Routingereignissystem in WPF benachrichtigt ganze Strukturen, wenn Ereignisse ausgelöst werden.
<Canvas MouseLeftButtonDown="handler1"
Width="100"
Height="100"
>
<Ellipse Width="50"
Height="50"
Fill="Blue"
Canvas.Left="30"
Canvas.Top="50"
MouseLeftButtonDown="handler2"
/>
</Canvas>
Wenn die linke Maustaste über der Ellipse gedrückt wird, wird handler2 ausgeführt. Nachdem handler2 beendet ist, wird das Ereignis an das Canvas-Objekt übergeben, das es mithilfe von handler1 verarbeitet. Dies geschieht nur, wenn handler2 das Ereignisobjekt nicht explizit als behandelt markiert.
Es kann sein, dass handler2 sehr viel Zeit für die Verarbeitung dieses Ereignisses benötigt. handler2 verwendet möglicherweise PushFrame, um eine geschachtelte Meldungsschleife zu beginnen, die stundenlang nicht zurückgegeben wird. Wenn handler2 das Ereignis nicht als behandelt markiert, nachdem die Meldungsschleife vollständig ist, wird das Ereignis in der Struktur nach oben weitergereicht, obwohl es sehr alt ist.
Reentranz und Sperrung
Der Sperrmechanismus der common language runtime (CLR) verhält sich nicht genau so wie anzunehmen wäre: Bei der Anforderung einer Sperre sollte ein Thread eigentlich alle Aktivitäten vollständig einstellen. Tatsächlich aber empfängt der Thread weiterhin Meldungen mit hoher Priorität und verarbeitet sie. Dadurch können zwar Deadlocks vermieden und eine minimale Reaktionsfähigkeit von Schnittstellen aufrechterhalten werden, es besteht aber auch die Möglichkeit, dass sich fast unmerkliche Fehler einschleichen. Obwohl dieses Verhalten meist keine Rolle spielt, kann es unter seltenen Umständen nützlich sein, dies zu wissen (in diesen Fällen sind in der Regel Win32-Fenstermeldungen oder COM-STA-Komponenten beteiligt).
Die meisten Schnittstellen werden nicht im Hinblick auf Threadsicherheit erstellt, da Entwickler bei ihrer Arbeit davon ausgehen, dass nie mehr als ein Thread auf eine UI zugreift. In diesem Fall führt dieser einzelne Thread vielleicht zu unerwarteten Zeitpunkten Änderungen an der Umgebung aus und verursacht dadurch die unschönen Effekte, die vom gegenseitigen Ausschlussmechanismus von DispatcherObject behoben werden sollen. Sehen Sie sich den folgenden Pseudocode an:
Meistens ist dies genau richtig, aber manchmal kann es in WPF vorkommen, dass solche unerwartete Reentranz Probleme verursacht. Es kann also u. U. dazu kommen, dass DisableProcessing von WPF aufgerufen wird, wodurch die Sperranweisung für diesen Thread so geändert wird, dass anstelle der üblichen CLR-Sperre die WPF-Sperre ohne Reentranz verwendet wird.
Warum also hat das CLR-Team dieses Verhalten gewählt? Der Grund dafür liegt in den COM-STA-Objekten und im Finalisierungsthread. Wenn ein Objekt an die Garbage Collection übergeben wird, wird seine Finalize-Methode im dedizierten Finalizerthread ausgeführt, nicht im UInthread. Und hierin liegt das Problem: Ein COM-STA-Objekt, das im UInthread erstellt wurde, kann nur im UInthread verworfen werden. Das Verhalten der CLR entspricht dem von BeginInvoke (in diesem Fall mit SendMessage von Win32). Wenn der UInthread aber ausgelastet ist, wird der Finalizerthread verzögert, und das COM-STA-Objekt kann nicht verworfen werden, sodass ein ernsthafter Speicherverlust entsteht. Deshalb hat das CLR-Team die Notbremse gezogen und die Sperren auf diese Weise eingesetzt.
Die Aufgabe für WPF besteht darin, die unerwartete Reentranz zu verhindern, ohne den Speicherverlust wieder zuzulassen. Aus diesem Grund wird die Reentranz nicht überall blockiert.
Siehe auch
Weitere Ressourcen
Beispiel für eine Singlethread-Anwendung mit Berechnung mit langer Laufzeit