Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Windows Presentation Foundation (WPF) wurde entwickelt, um Entwicklern die Schwierigkeiten beim Threading zu sparen. Daher schreiben die meisten WPF-Entwickler keine Schnittstelle, die mehr als einen Thread verwendet. Da Multithreadprogramme komplex und schwierig zu debuggen sind, sollten sie vermieden werden, wenn Singlethread-Lösungen vorhanden sind.
Unabhängig davon, wie gut architekturiert, kann kein UI-Framework eine Singlethread-Lösung für jede Art von Problem bereitstellen. WPF kommt dem nahe, aber es gibt immer noch Situationen, in denen mehrere Threads die Reaktionsfähigkeit der Benutzeroberfläche oder die Anwendungsleistung verbessern. Nach der Erläuterung einiger Hintergrundmaterialien untersucht dieser Artikel einige dieser Situationen und endet dann mit einer Diskussion über einige Details auf niedrigerer Ebene.
Hinweis
In diesem Thema wird threading mithilfe der InvokeAsync Methode für asynchrone Aufrufe erläutert. Die InvokeAsync
Methode verwendet einen Action oder Func<TResult> als Parameter und gibt ein DispatcherOperation oder DispatcherOperation<TResult>, das über eine Task Eigenschaft verfügt. Sie können das await
-Schlüsselwort entweder mit dem DispatcherOperation oder dem Task verwenden. Wenn Sie synchrones Warten benötigen, bis Task von einem DispatcherOperation oder DispatcherOperation<TResult> zurückgegeben wird, rufen Sie die DispatcherOperationWait-Erweiterungsmethode auf. Ein Aufruf Task.Wait führt zu einer Verklemmung. Weitere Informationen über das Ausführen asynchroner Task Vorgänge finden Sie unter Aufgabenbasierte asynchrone Programmierung.
Um einen synchronen Aufruf durchzuführen, verwenden Sie die Invoke-Methode, die auch Überladungen enthält, die einen Delegaten, Action oder Func<TResult>-Parameter verwenden.
Übersicht und Verteiler
In der Regel beginnen WPF-Anwendungen mit zwei Threads: eines zum Behandeln des Renderings und eines zum Verwalten der Benutzeroberfläche. Der Renderingthread wird effektiv im Hintergrund ausgeführt, während der UI-Thread Eingaben empfängt, Ereignisse behandelt, den Bildschirm zeichnet und Anwendungscode ausführt. Die meisten Anwendungen verwenden einen einzelnen UI-Thread, obwohl es in einigen Situationen am besten ist, mehrere zu verwenden. Wir werden dies später mit einem Beispiel besprechen.
** Der UI-Thread reiht Arbeitsaufgaben innerhalb eines Objekts ein, das ein Dispatcher genannt wird. Die Dispatcher wählt Arbeitsaufgaben auf Prioritätsbasis aus und führt jede von ihnen bis zum Abschluss aus. Jeder UI-Thread muss mindestens einen Dispatcherhaben, und jede Dispatcher kann Arbeitsaufgaben in genau einem Thread ausführen.
Der Trick beim Erstellen reaktionsfähiger, benutzerfreundlicher Anwendungen besteht darin, den Dispatcher Durchsatz zu maximieren, indem die Arbeitsaufgaben klein gehalten werden. Auf diese Weise veralten Elemente nie, während sie in der Dispatcher Warteschlange auf ihre Verarbeitung warten. Jede erkennbare Verzögerung zwischen Eingabe und Reaktion kann einen Benutzer frustrieren.
Wie sollen dann WPF-Anwendungen große Vorgänge verarbeiten? Was geschieht, wenn Ihr Code eine große Berechnung umfasst oder eine Datenbank auf einem Remoteserver abfragen muss? Normalerweise besteht die Antwort darin, den großen Vorgang in einem separaten Thread zu verarbeiten und den UI-Thread frei zu lassen, um Elemente in der Dispatcher-Warteschlange zu bearbeiten. Wenn der große Vorgang abgeschlossen ist, kann er das Ergebnis zurück an den UI-Thread für die Anzeige melden.
In der Vergangenheit ermöglicht Windows den Zugriff auf UI-Elemente nur durch den Thread, der sie erstellt hat. Dies bedeutet, dass ein Hintergrundthread, der für einige lange ausgeführte Aufgaben verantwortlich ist, ein Textfeld nicht aktualisieren kann, wenn er abgeschlossen ist. Windows gewährleistet dies, um die Integrität von UI-Komponenten sicherzustellen. Ein Listenfeld könnte seltsam aussehen, wenn der Inhalt während der Zeichnung von einem Hintergrundthread aktualisiert wurde.
WPF verfügt über einen integrierten gegenseitigen Ausschlussmechanismus, der diese Koordinierung erzwingt. Die meisten Klassen in WPF erben von DispatcherObject. Bei der Konstruktion speichert ein DispatcherObject einen Verweis auf den Dispatcher, der mit dem aktuell laufenden Thread verknüpft ist. Tatsächlich ist die DispatcherObject mit dem Thread verknüpft, der sie erstellt. Während der Programmausführung kann ein DispatcherObject seine öffentliche VerifyAccess Methode aufrufen. VerifyAccess untersucht den Dispatcher aktuellen Thread und vergleicht ihn mit dem während der Dispatcher Konstruktion gespeicherten Verweis. Wenn sie nicht übereinstimmen, löst VerifyAccess eine Ausnahme aus. Am Anfang jeder Methode, die zu einem VerifyAccess gehört, soll DispatcherObject aufgerufen werden.
Wenn nur ein Thread die Benutzeroberfläche ändern kann, wie interagieren Hintergrundthreads mit dem Benutzer? Ein Hintergrundthread kann den UI-Thread bitten, einen Vorgang in seinem Auftrag auszuführen. Dazu registrieren Sie eine Arbeitsaufgabe für den Dispatcher UI-Thread. Die Dispatcher Klasse stellt die Methoden zum Registrieren von Arbeitsaufgaben bereit: Dispatcher.InvokeAsync, , Dispatcher.BeginInvokeund Dispatcher.Invoke. Diese Methoden planen einen Delegaten für die Ausführung.
Invoke
ist ein synchroner Aufruf, das heißt, er kehrt erst zurück, wenn der UI-Thread die Ausführung des Delegaten abgeschlossen hat.
InvokeAsync
und BeginInvoke
sind asynchron und geben sofort zurück.
Die Dispatcher Elemente in der Warteschlange werden nach Priorität sortiert. Es gibt zehn Ebenen, die beim Hinzufügen eines Elements zur Dispatcher Warteschlange angegeben werden können. Diese Prioritäten werden in der DispatcherPriority Enumeration beibehalten.
Einfädelige App mit lang andauernder Berechnung
Die meisten grafischen Benutzeroberflächen (GUIs) verbringen einen großen Teil ihrer Zeit im Leerlauf, während sie auf Ereignisse warten, die als Reaktion auf Benutzerinteraktionen generiert werden. Mit sorgfältiger Programmierung kann diese Leerlaufzeit konstruktiv verwendet werden, ohne die Reaktionsfähigkeit der Benutzeroberfläche zu beeinträchtigen. Das WPF-Threadingmodell lässt keine Eingabe zu, um einen Vorgang im UI-Thread zu unterbrechen. Dies bedeutet, dass Sie in regelmäßigen Abständen unbedingt zum Dispatcher zurückkehren müssen, um ausstehende Eingabeereignisse zu verarbeiten, bevor sie veraltet werden.
Eine Beispiel-App, die die Konzepte dieses Abschnitts veranschaulicht, kann von GitHub für C# oder Visual Basic heruntergeladen werden.
Betrachten Sie das folgenden Beispiel:
Diese einfache Anwendung zählt von drei nach oben und sucht nach Primzahlen. Wenn der Benutzer auf die Schaltfläche "Start " klickt, beginnt die Suche. Wenn das Programm eine Primzahl findet, aktualisiert es die Benutzeroberfläche mit seiner Ermittlung. Der Benutzer kann die Suche jederzeit beenden.
Obwohl einfach genug, könnte die Primzahlsuche für immer weitergehen, was einige Schwierigkeiten darstellt. Wenn wir die gesamte Suche innerhalb des Click-Ereignishandlers der Schaltfläche behandeln würden, könnten wir dem UI-Thread niemals die Möglichkeit geben, andere Ereignisse zu verarbeiten. Die Benutzeroberfläche kann nicht auf Eingabe- oder Verarbeitungsmeldungen reagieren. Es würde nie neu zeichnen und nie auf Klicks auf Schaltflächen reagieren.
Wir könnten die Primzahlsuche in einem separaten Thread durchführen, aber dann müssten wir mit Synchronisierungsproblemen umgehen. Mit einem einzelnen Thread-Ansatz können wir das Label direkt aktualisieren, das die größte gefundene Primzahl auflistet.
Wenn wir die Berechnungsaufgabe in verwaltbare Blöcke aufteilen, können wir regelmäßig zu den Dispatcher Ereignissen und Prozessen zurückkehren. Wir können WPF die Möglichkeit geben, neu zu zeichnen und Eingaben zu verarbeiten.
Der beste Weg, die Verarbeitungszeit zwischen Berechnung und Ereignisbehandlung aufzuteilen, besteht darin, die Berechnung vom Dispatcher aus zu steuern. Unter Verwendung der InvokeAsync-Methode können wir Primzahlprüfungen in derselben Warteschlange planen, aus der UI-Ereignisse abgerufen werden. In unserem Beispiel planen wir jeweils nur eine einzige Primzahlüberprüfung. Nach Abschluss der Primzahlüberprüfung planen wir die nächste Prüfung sofort. Diese Überprüfung wird erst fortgesetzt, nachdem ausstehende UI-Ereignisse behandelt wurden.
Microsoft Word führt die Rechtschreibprüfung mithilfe dieses Mechanismus durch. Die Rechtschreibprüfung erfolgt im Hintergrund mithilfe der Leerlaufzeit des UI-Threads. Sehen wir uns den Code an.
Das folgende Beispiel zeigt den XAML-Code, der die Benutzeroberfläche erstellt.
Von Bedeutung
Der in diesem Artikel gezeigte XAML stammt aus einem C#-Projekt. Visual Basic XAML unterscheidet sich geringfügig beim Deklarieren der Hintergrundklasse für den XAML-Code.
<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>
Das folgende Beispiel zeigt das Code-Behind.
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
Neben der Aktualisierung des Texts auf dem Button ist der StartStopButton_Click
Handler für die Planung der ersten Primzahl-Prüfung zuständig, indem er einen Delegierten zur Dispatcher Warteschlange hinzufügt. Irgendwann nachdem dieser Ereignishandler seine Arbeit abgeschlossen hat, wird der Delegat für die Ausführung durch Dispatcher ausgewählt.
Wie bereits erwähnt, ist InvokeAsync das Dispatcher-Mitglied, das verwendet wird, um einen Delegaten zur Ausführung zu planen. In diesem Fall wählen wir die SystemIdle Priorität aus. Der Delegat Dispatcher wird nur ausgeführt, wenn keine wichtigen Ereignisse zur Verarbeitung vorhanden sind. Die Reaktionsfähigkeit der Benutzeroberfläche ist wichtiger als die Zahlenüberprüfung. Außerdem wird eine neue Stellvertretung übergeben, die die Routine für die Nummernüberprüfung darstellt.
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
Mit dieser Methode wird überprüft, ob die nächste ungerade Zahl primiert ist. Wenn es eine Primzahl ist, aktualisiert die Methode bigPrime
TextBlock direkt, um deren Entdeckung widerzuspiegeln. Dies ist möglich, da die Berechnung im selben Thread erfolgt, der zum Erstellen des Steuerelements verwendet wurde. Wenn wir einen separaten Thread für die Berechnung verwenden wollten, müssten wir einen komplizierteren Synchronisierungsmechanismus verwenden und das Update im UI-Thread ausführen. Wir zeigen diese Situation als Nächstes.
Mehrere Fenster, mehrere Threads
Für einige WPF-Anwendungen sind mehrere Fenster der obersten Ebene erforderlich. Es ist vollkommen akzeptabel, dass eine Thread-/Dispatcher-Kombination mehrere Fenster verwaltet, aber manchmal führen mehrere Threads einen besseren Auftrag aus. Dies gilt insbesondere, wenn es die Möglichkeit gibt, dass eines der Fenster den Thread monopolisiert.
Windows Explorer funktioniert auf diese Weise. Jedes neue Explorer-Fenster gehört zum ursprünglichen Prozess, wird jedoch unter der Kontrolle eines unabhängigen Threads erstellt. Wenn Explorer nicht mehr reagiert, z. B. bei der Suche nach Netzwerkressourcen, sind andere Explorer-Fenster weiterhin reaktionsfähig und verwendbar.
Wir können dieses Konzept mit dem folgenden Beispiel veranschaulichen.
Die drei oberen Fenster dieses Bilds haben denselben Threadbezeichner: 1. Die beiden anderen Fenster weisen unterschiedliche Thread-IDs auf: Neun und 4. Eine magentafarbene rotierende ‼️ Glyphe befindet sich in der oberen rechten Ecke jedes Fensters.
Dieses Beispiel enthält ein Fenster mit einer gedrehten ‼️
Glyphe , einer Pause-Schaltfläche und zwei anderen Schaltflächen, die ein neues Fenster unter dem aktuellen Thread oder in einem neuen Thread erstellen. Die ‼️
Glyphe wird ständig gedreht, bis die Pause-Taste gedrückt wird, wodurch der Thread fünf Sekunden lang angehalten wird. Am unteren Rand des Fensters wird der Threadbezeichner angezeigt.
Wenn die Schaltfläche "Anhalten" gedrückt wird, reagieren alle Fenster, die demselben Thread zugeordnet sind, nicht mehr. Jedes Fenster unter einem anderen Thread funktioniert weiterhin normal.
Das folgende Beispiel ist der XAML-Code für das Fenster:
<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>
Das folgende Beispiel zeigt das Code-Behind.
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
Im Folgenden finden Sie einige der zu beachtenden Details:
Die Task.Delay(TimeSpan) Aufgabe wird verwendet, damit der aktuelle Thread fünf Sekunden lang angehalten wird, wenn die Schaltfläche " Anhalten " gedrückt wird.
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
Der
SameThreadWindow_Click
Ereignishandler zeigt sofort ein neues Fenster im aktuellen Thread an. DerNewThreadWindow_Click
Ereignishandler erstellt einen neuen Thread, der mit der Ausführung derThreadStartingPoint
Methode beginnt, die wiederum ein neues Fenster anzeigt, wie im nächsten Aufzählungspunkt beschrieben.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
Die
ThreadStartingPoint
Methode ist der Ausgangspunkt für den neuen Thread. Das neue Fenster wird unter der Kontrolle dieses Threads erstellt. WPF erstellt automatisch eine neue System.Windows.Threading.Dispatcher zum Verwalten des neuen Threads. Alles, was wir tun müssen, um das Fenster funktionsfähig zu machen, besteht darin, das System.Windows.Threading.DispatcherFenster zu starten.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
Eine Beispiel-App, die die Konzepte dieses Abschnitts veranschaulicht, kann von GitHub für C# oder Visual Basic heruntergeladen werden.
Behandeln eines Blockierungsvorgangs mit Task.Run
Das Behandeln von Blockierungsvorgängen in einer grafischen Anwendung kann schwierig sein. Wir möchten keine blockierenden Methoden von Ereignishandlern aufrufen, da die Anwendung scheinbar anfriert. Im vorherigen Beispiel wurden neue Fenster in ihrem eigenen Thread erstellt, sodass jedes Fenster unabhängig voneinander ausgeführt werden kann. Während wir mit System.Windows.Threading.Dispatcher einen neuen Thread erstellen können, wird es schwierig, diesen nach Abschluss der Arbeit mit dem Haupt-UI-Thread zu synchronisieren. Da der neue Thread die Benutzeroberfläche nicht direkt ändern kann, müssen wir Dispatcher.InvokeAsync, Dispatcher.BeginInvoke oder Dispatcher.Invoke verwenden, um Delegaten in den Dispatcher des UI-Threads der Benutzeroberfläche einzufügen. Schließlich werden diese Delegierten mit der Berechtigung zur Änderung von Benutzeroberflächenelementen ausgeführt.
Es gibt eine einfachere Möglichkeit, den Code in einem neuen Thread auszuführen, während die Ergebnisse synchronisiert werden, das aufgabenbasierte asynchrone Muster (TAP). Sie basiert auf den Task Und Task<TResult> Typen im System.Threading.Tasks
Namespace, die verwendet werden, um asynchrone Vorgänge darzustellen. TAP verwendet eine einzelne Methode, um die Initiierung und den Abschluss eines asynchronen Vorgangs darzustellen. Für dieses Muster gibt es einige Vorteile:
- Der Aufrufer eines
Task
Codes kann auswählen, dass der Code asynchron oder synchron ausgeführt werden soll. - Der Fortschritt lässt sich vom
Task
berichten. - Der aufrufende Code kann die Ausführung anhalten und auf das Ergebnis des Vorgangs warten.
Beispiel für Task.Run
In diesem Beispiel imitieren wir einen Remoteprozeduraufruf, der eine Wettervorhersage abruft. Wenn auf die Schaltfläche geklickt wird, wird die Benutzeroberfläche aktualisiert, um anzugeben, dass der Datenabruf ausgeführt wird, während eine Aufgabe gestartet wird, um das Abrufen der Wettervorhersage nachzuahmen. Wenn die Aufgabe gestartet wird, wird der Ereignishandlercode der Schaltfläche angehalten, bis die Aufgabe abgeschlossen ist. Nach Abschluss der Aufgabe wird der Ereignishandlercode weiterhin ausgeführt. Der Code wird ausgesetzt, und er blockiert den Rest des UI-Threads nicht. Der Synchronisierungskontext von WPF behandelt das Anhalten des Codes, wodurch WPF weiterhin ausgeführt werden kann.
Ein Diagramm, das den Workflow der Beispiel-App veranschaulicht. Die App verfügt über eine einzelne Schaltfläche mit dem Text "Fetch Forecast". Es gibt einen Pfeil, der auf die nächste Phase der App zeigt, nachdem die Schaltfläche gedrückt wurde. Dabei handelt es sich um ein Uhrbild in der Mitte der App, der angibt, dass die App mit dem Abrufen von Daten beschäftigt ist. Nach einiger Zeit gibt die App je nach Ergebnis der Daten entweder ein Bild der Sonne oder regenwolken zurück.
Eine Beispiel-App, die die Konzepte dieses Abschnitts veranschaulicht, kann von GitHub für C# oder Visual Basic heruntergeladen werden. Der XAML-Code für dieses Beispiel ist ziemlich groß und wird in diesem Artikel nicht bereitgestellt. Verwenden Sie die vorherigen GitHub-Links, um den XAML-Code zu durchsuchen. Der XAML-Code verwendet eine einzelne Schaltfläche zum Abrufen des Wetters.
Berücksichtigen Sie den Code-Behind für den 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
Im Folgenden sind einige der Details aufgeführt, die zu beachten sind.
Der Schaltflächenereignishandler
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
Beachten Sie, dass der Ereignishandler mit
async
(oderAsync
mit Visual Basic) deklariert wurde. Eine Async-Methode ermöglicht das Anhalten des Codes, wenn eine Methode, wie z. B.FetchWeatherFromServerAsync
, aufgerufen wird. Dies wird durch dasawait
Schlüsselwort (oderAwait
mit Visual Basic) festgelegt. Bis zumFetchWeatherFromServerAsync
Abschluss wird der Handlercode der Schaltfläche angehalten, und das Steuerelement wird an den Aufrufer zurückgegeben. Dies ähnelt einer synchronen Methode, außer dass eine synchrone Methode darauf wartet, dass jeder Vorgang in der Methode abgeschlossen ist, bevor die Steuerung an den Aufrufer zurückgegeben wird.Die erwarteten Methoden verwenden den Threadingkontext der aktuellen Methode, die mit dem Schaltflächenhandler den UI-Thread darstellt. Dies bedeutet, dass der Aufruf von
await FetchWeatherFromServerAsync();
(oderAwait FetchWeatherFromServerAsync()
mit Visual Basic) bewirkt, dass der Code inFetchWeatherFromServerAsync
im UI-Thread ausgeführt wird. Der Dispatcher führt diesen jedoch erst aus, wenn er Zeit dafür hat, ähnlich wie das Beispiel Singlethread-App mit lang andauernder Berechnung arbeitet. Beachten Sie jedoch, dassawait Task.Run
verwendet wird. Dadurch wird ein neuer Thread im Threadpool für die angegebene Aufgabe anstelle des aktuellen Threads erstellt. LäuftFetchWeatherFromServerAsync
somit auf einem eigenen Thread.Wetter abrufen
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
Um die Dinge einfach zu halten, haben wir in diesem Beispiel keinen Netzwerkcode. Stattdessen simulieren wir die Verzögerung des Netzwerkzugriffs, indem wir unseren neuen Thread vier Sekunden lang in den Ruhezustand versetzen. In dieser Zeit wird der ursprüngliche UI-Thread weiterhin ausgeführt und reagiert auf UI-Ereignisse, während der Ereignishandler der Schaltfläche angehalten wird, bis der neue Thread abgeschlossen ist. Um dies zu veranschaulichen, haben wir eine Animation ausgeführt, und Sie können die Größe des Fensters ändern. Wenn der UI-Thread angehalten oder verzögert wurde, wird die Animation nicht angezeigt, und Sie konnten nicht mit dem Fenster interagieren.
Wenn der
Task.Delay
Vorgang abgeschlossen ist und wir zufällig unsere Wettervorhersage ausgewählt haben, wird der Wetterstatus an den Anrufer zurückgegeben.Aktualisieren der Benutzeroberfläche
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
Wenn die Aufgabe abgeschlossen ist und der UI-Thread Zeit hat, wird der Aufrufer des
Task.Run
-Ereignishandlers der Taste wiederaufgenommen. Der Rest der Methode stoppt die Uhranimation und wählt ein Bild aus, um das Wetter zu beschreiben. Es zeigt dieses Bild an und aktiviert die Schaltfläche "Prognose abrufen".
Eine Beispiel-App, die die Konzepte dieses Abschnitts veranschaulicht, kann von GitHub für C# oder Visual Basic heruntergeladen werden.
Technische Details und Stumblingpunkte
In den folgenden Abschnitten werden einige der Details und Stumblingpunkte beschrieben, auf die Sie möglicherweise multithreading stoßen.
Geschachtelte Pumpen
Manchmal ist es nicht möglich, den UI-Thread vollständig zu sperren. Betrachten wir die Show Methode der MessageBox Klasse. Show wird erst zurückgegeben, wenn der Benutzer auf die Schaltfläche "OK" klickt. Es wird jedoch ein Fenster erstellt, das über eine Nachrichtenschleife verfügen muss, um interaktiv zu sein. Während wir warten, dass der Benutzer auf "OK" klickt, reagiert das ursprüngliche Anwendungsfenster nicht auf Benutzereingaben. Sie verarbeitet jedoch weiterhin Paint-Nachrichten. Das ursprüngliche Fenster wird neu gezeichnet, wenn es verdeckt und offenbart wird.
Ein Thread muss für das Nachrichtenfenster zuständig sein. WPF könnte nur für das Meldungsfeldfenster einen neuen Thread erstellen, aber dieser Thread konnte die deaktivierten Elemente nicht im ursprünglichen Fenster zeichnen (denken Sie sich an die frühere Diskussion über gegenseitige Ausgrenzung). Stattdessen verwendet WPF ein geschachteltes Nachrichtenverarbeitungssystem. Die Dispatcher Klasse enthält eine spezielle Methode namens PushFrame, die den aktuellen Ausführungspunkt einer Anwendung speichert und dann eine neue Nachrichtenschleife beginnt. Wenn die geschachtelte Nachrichtenschleife beendet ist, wird die Ausführung nach dem ursprünglichen PushFrame Aufruf fortgesetzt.
In diesem Fall PushFrame verwaltet der Programmkontext beim Aufruf MessageBox.Showund startet eine neue Nachrichtenschleife, um das Hintergrundfenster neu zu überschreiben und die Eingabe im Meldungsfeldfenster zu verarbeiten. Wenn der Benutzer auf "OK" klickt und das Popup-Fenster schließt, wird die geschachtelte Schleife beendet und die Kontrolle wird nach dem Aufruf Show fortgesetzt.
Abgestandene Routing-Ereignisse
Das Routingereignissystem in WPF benachrichtigt ganze Baumstrukturen, 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 die Ellipse gedrückt wird, handler2
wird ausgeführt. Nach Abschluss von handler2
wird das Ereignis an das Canvas-Objekt übergeben, das handler1
verwendet, um es zu verarbeiten. Dies geschieht nur, wenn handler2
das Ereignisobjekt nicht explizit als behandelt markiert wird.
Es ist möglich, dass handler2
die Verarbeitung dieses Ereignisses viel Zeit in Anspruch nimmt.
handler2
kann PushFrame verwenden, um eine geschachtelte Nachrichtenschleife zu beginnen, die stundenlang nicht zurückgegeben wird. Wenn handler2
das Ereignis nicht als behandelt markiert, wenn diese Meldungsschleife abgeschlossen ist, wird das Ereignis im Baum weitergereicht, obwohl es sehr alt ist.
Reentrancy und Sperren
Der Sperrmechanismus der Common Language Runtime (CLR) verhält sich nicht genau so, wie man es sich vorstellen könnte; man könnte erwarten, dass ein Thread vollständig stillgelegt wird, wenn eine Sperre angefordert wird. Tatsächlich empfängt der Thread weiterhin Nachrichten mit hoher Priorität und verarbeitet sie. Dies hilft, Deadlocks zu verhindern und Schnittstellen minimal reaktionsfähig zu machen, aber es führt die Möglichkeit für subtile Fehler ein. In den meisten Fällen müssen Sie darüber nichts wissen, aber unter seltenen Umständen (normalerweise bei Win32-Fensternachrichten oder COM-STA-Komponenten) kann es nützlich sein, dies zu wissen.
Die meisten Schnittstellen werden nicht mit Threadsicherheit erstellt, da Entwickler unter der Annahme arbeiten, dass auf eine Benutzeroberfläche niemals von mehr als einem Thread zugegriffen wird. In diesem Fall kann dieser einzelne Thread zu unerwarteten Zeiten Umweltänderungen vornehmen, was zu den schlechten Auswirkungen führt, die der DispatcherObject gegenseitige Ausschlussmechanismus lösen soll. Betrachten Sie den folgenden Pseudocode:
Meistens ist das das Richtige, aber es gibt Zeiten in WPF, in denen unerwarteter Wiedereintritt wirklich Probleme verursachen kann. Daher ruft WPF zu bestimmten Zeitpunkten DisableProcessing auf, die die Sperranweisung für diesen Thread so ändert, dass die WPF-Wiedereintrittsfreie Sperre verwendet wird, anstelle der üblichen CLR-Sperre.
Warum hat das CLR-Team dieses Verhalten gewählt? Es musste mit COM STA-Objekten und dem Finalisierungsthread zu tun sein. Wenn ein Objekt der Garbage-Collection unterliegt, wird seine Finalize
-Methode im dedizierten Finalisierungs-Thread ausgeführt, nicht auf dem UI-Thread. Und genau darin liegt das Problem, da ein COM STA-Objekt, das im UI-Thread erstellt wurde, nur im UI-Thread freigegeben werden kann. Die CLR führt das Äquivalent eines BeginInvoke (in diesem Fall unter Verwendung von Win32s SendMessage
) aus. Wenn der UI-Thread jedoch ausgelastet ist, wird der Finalizerthread angehalten, und das COM STA-Objekt kann nicht verworfen werden, wodurch ein schwerwiegender Speicherverlust entsteht. So hat das CLR-Team die schwierige Entscheidung getroffen, damit Sperren so funktionieren, wie sie es tun.
Die Aufgabe für WPF besteht darin, unerwartete Reentranz zu vermeiden, ohne den Speicherverlust wieder hervorzurufen. Deshalb blockieren wir die Reentranz nicht an allen Stellen.
Siehe auch
.NET Desktop feedback