Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Windows Presentation Foundation (WPF) is ontworpen om ontwikkelaars te redden van de problemen met threading. Als gevolg hiervan schrijven de meeste WPF-ontwikkelaars geen interface die meer dan één thread gebruikt. Omdat multithreaded programma's complex zijn en moeilijk te debuggen, moeten ze worden vermeden als er single-threaded oplossingen beschikbaar zijn.
Hoe goed ook ontworpen, geen UI-framework kan een oplossing met één thread bieden voor elk soort probleem. WPF komt dicht, maar er zijn nog steeds situaties waarin meerdere threads de reactiesnelheid van de gebruikersinterface (UI) of toepassingsprestaties verbeteren. Na het bespreken van wat achtergrondmateriaal, verkent dit artikel enkele van deze situaties en eindigt vervolgens met een bespreking van enkele details op lager niveau.
Opmerking
In dit onderwerp wordt threading besproken met behulp van de InvokeAsync methode voor asynchrone aanroepen. De InvokeAsync methode gebruikt een Action of Func<TResult> als parameter en retourneert een DispatcherOperation of DispatcherOperation<TResult>, die een Task eigenschap heeft. U kunt het await trefwoord gebruiken met de DispatcherOperation of de bijbehorende Task. Als u synchroon moet wachten op de Task die wordt geretourneerd door een DispatcherOperation of DispatcherOperation<TResult>, roep dan de DispatcherOperationWait-extensiemethode aan. Bellen Task.Wait leidt tot een deadlock. Zie Task voor meer informatie over het gebruik van asynchrone bewerkingen.
Als u een synchrone aanroep wilt maken, gebruikt u de Invoke methode, die ook overbelastingen heeft die een delegaat, Action, of Func<TResult> parameter gebruiken.
Overzicht en de dispatcher
WpF-toepassingen beginnen meestal met twee threads: een voor het verwerken van rendering en een andere voor het beheren van de gebruikersinterface. De renderingthread wordt effectief verborgen op de achtergrond uitgevoerd terwijl de UI-thread invoer ontvangt, gebeurtenissen verwerkt, het scherm schildert en toepassingscode uitvoert. De meeste toepassingen gebruiken één UI-thread, hoewel het in sommige situaties het beste is om verschillende te gebruiken. We bespreken dit later met een voorbeeld.
De UI-thread maakt wachtrijen voor werkitems binnen een object genaamd een Dispatcher. De Dispatcher selecteert werkitems op basis van prioriteit en voert elk item uit tot voltooiing. Elke UI-thread moet ten minste één Dispatcherthread hebben en elk Dispatcher kan werkitems in precies één thread uitvoeren.
De truc voor het bouwen van responsieve, gebruiksvriendelijke toepassingen is het maximaliseren van de Dispatcher doorvoer door de werkitems klein te houden. Op deze manier verlopen items nooit terwijl ze in de Dispatcher wachtrij zitten te wachten op verwerking. Elke mogelijke vertraging tussen invoer en reactie kan een gebruiker frustreren.
Hoe moeten WPF-toepassingen dan grote bewerkingen verwerken? Wat gebeurt er als uw code een grote berekening omvat of een query moet uitvoeren op een database op een externe server? Meestal is het antwoord om de grote bewerking in een afzonderlijke thread af te handelen, zodat de UI-thread vrij is om zich bezig te houden met items in de Dispatcher wachtrij. Wanneer de grote bewerking is voltooid, kan het resultaat worden teruggezet naar de UI-thread voor weergave.
In het verleden kunnen ui-elementen alleen worden geopend door de thread die ze heeft gemaakt. Dit betekent dat een achtergrondthread die verantwoordelijk is voor een langlopende taak, een tekstvak niet kan bijwerken wanneer deze is voltooid. Windows doet dit om de integriteit van UI-onderdelen te garanderen. Een keuzelijst kan er vreemd uitzien als de inhoud ervan is bijgewerkt door een achtergrondthread tijdens het schilderen.
WPF heeft een ingebouwd mechanisme voor wederzijdse uitsluiting dat deze coördinatie afdwingt. De meeste klassen in WPF zijn afgeleid van DispatcherObject. Bij de bouw slaat een DispatcherObject verwijzing op naar de Dispatcher gekoppelde aan de momenteel actieve thread. In feite verbindt de DispatcherObject zich met de thread die hem creëert. Tijdens de uitvoering van het programma kan een DispatcherObject openbare VerifyAccess methode worden aangeroepen. VerifyAccess onderzoekt de Dispatcher gekoppelde aan de huidige thread en vergelijkt deze met de Dispatcher verwijzing die tijdens de constructie is opgeslagen. Als ze niet overeenkomen, werpt VerifyAccess een uitzondering. VerifyAccess is bedoeld om te worden aangeroepen aan het begin van elke methode die deel uitmaakt van een DispatcherObject.
Als slechts één thread de gebruikersinterface kan wijzigen, hoe werken achtergrondthreads met de gebruiker? Een achtergrondthread kan de UI-thread vragen een bewerking namens de thread uit te voeren. Dit doet men door een werkitem te registreren bij de Dispatcher UI-thread. De Dispatcher klasse biedt de methoden voor het registreren van werkitems: Dispatcher.InvokeAsync, Dispatcher.BeginInvokeen Dispatcher.Invoke. Met deze methoden wordt een gemachtigde gepland voor uitvoering.
Invoke is een synchrone aanroep. Dat wil zeggen: deze wordt pas geretourneerd als de UI-thread daadwerkelijk klaar is met het uitvoeren van de gedelegeerde.
InvokeAsync en BeginInvoke zijn asynchroon en retourneren onmiddellijk.
De Dispatcher elementen in de wachtrij worden gerangschikt op prioriteit. Er zijn tien niveaus die kunnen worden opgegeven bij het toevoegen van een element aan de Dispatcher wachtrij. Deze prioriteiten worden bijgehouden in de DispatcherPriority opsomming.
App met één thread met een langlopende berekening
De meeste grafische gebruikersinterfaces (GUI's) besteden een groot deel van hun tijd aan niet-actieve tijd terwijl wordt gewacht op gebeurtenissen die worden gegenereerd als reactie op gebruikersinteracties. Met zorgvuldig programmeren kan deze niet-actieve tijd constructief worden gebruikt, zonder dat dit van invloed is op de reactiesnelheid van de gebruikersinterface. Het WPF-threadingmodel staat invoer niet toe om een bewerking in de UI-thread te onderbreken. Dit betekent dat u regelmatig moet terugkeren naar de Dispatcher procedure voor het verwerken van in behandeling zijnde invoer gebeurtenissen voordat ze verlopen.
Een voorbeeld-app waarin de concepten van deze sectie worden gedemonstreerd, kan worden gedownload van GitHub voor C# of Visual Basic.
Bekijk het volgende voorbeeld:
Deze eenvoudige toepassing telt omhoog van drie, op zoek naar priemgetallen. Wanneer de gebruiker op de knop Start klikt, wordt de zoekopdracht gestart. Wanneer het programma een prime vindt, wordt de gebruikersinterface bijgewerkt met de detectie. Op elk gewenst moment kan de gebruiker de zoekopdracht stoppen.
Hoewel eenvoudig genoeg, kan de zoekactie van het priemgetallen eeuwig doorgaan, wat problemen veroorzaakt. Als we de volledige zoekopdracht in de klikgebeurtenis-handler van de knop hebben verwerkt, geven we de UI-thread nooit de kans om andere gebeurtenissen te verwerken. De gebruikersinterface kan niet reageren op invoer- of procesberichten. Het zou nooit hertekenen en nooit reageren op knopklikken.
We kunnen de prime number search uitvoeren in een afzonderlijke thread, maar dan moeten we problemen met synchronisatie oplossen. Met een eendraads aanpak kunnen we het label dat het grootste priemgetal toont, direct bijwerken.
Als we de taak van de berekening opsplitsen in beheerbare segmenten, kunnen we periodiek terugkeren naar de Dispatcher gebeurtenissen en deze verwerken. We kunnen WPF de gelegenheid geven om opnieuw te tekenen en invoer te verwerken.
De beste manier om verwerkingstijd tussen berekeningen en gebeurtenisafhandeling te splitsen is door hier rekening mee te houden vanuit de Dispatcher. Met behulp van de InvokeAsync methode kunnen we priemgetalcontroles plannen in dezelfde wachtrij waaruit UI-gebeurtenissen worden getrokken. In ons voorbeeld plannen we slechts één priemgetalcontrole tegelijk. Nadat de controle van het priemnummer is voltooid, plannen we de volgende controle onmiddellijk. Deze controle wordt pas uitgevoerd nadat in behandeling zijnde UI-gebeurtenissen zijn verwerkt.
In Microsoft Word wordt de spellingcontrole uitgevoerd met behulp van dit mechanisme. Spellingcontrole wordt uitgevoerd op de achtergrond met behulp van de niet-actieve tijd van de UI-thread. Laten we de code eens bekijken.
In het volgende voorbeeld ziet u de XAML waarmee de gebruikersinterface wordt gemaakt.
Belangrijk
De XAML die in dit artikel wordt weergegeven, is afkomstig van een C#-project. Visual Basic XAML is iets anders wanneer je de ondersteunende klasse voor de XAML declareert.
<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>
In het volgende voorbeeld ziet u de code-achter.
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
Naast het bijwerken van de tekst op de Button, is de StartStopButton_Click handler verantwoordelijk voor het plannen van de eerste controle van priemgetallen door een gedelegeerde functie toe te voegen aan de Dispatcher wachtrij. Enige tijd nadat deze gebeurtenisafhandelaar zijn werk heeft voltooid, zal de Dispatcher de delegate selecteren voor uitvoering.
Zoals eerder vermeld, InvokeAsync is het Dispatcher lid dat wordt gebruikt om een gemachtigde te plannen voor uitvoering. In dit geval kiezen we de SystemIdle prioriteit. De Dispatcher delegate wordt alleen uitgevoerd wanneer er geen belangrijke gebeurtenissen te verwerken zijn. Reactiesnelheid van de gebruikersinterface is belangrijker dan het controleren van getallen. We geven ook een nieuwe gemachtigde door die de routine voor nummercontrole vertegenwoordigt.
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
Met deze methode wordt gecontroleerd of het volgende oneven getal priem is. Als deze prime is, werkt de methode de methode rechtstreeks bij om de bigPrimeTextBlock detectie ervan weer te geven. We kunnen dit doen omdat de berekening plaatsvindt in dezelfde thread die is gebruikt om het besturingselement te maken. Als we ervoor hadden gekozen om een afzonderlijke thread voor de berekening te gebruiken, zouden we een ingewikkelder synchronisatiemechanisme moeten gebruiken en de update in de UI-thread moeten uitvoeren. We laten deze situatie nu zien.
Meerdere vensters, meerdere threads
Voor sommige WPF-toepassingen zijn meerdere vensters op het hoogste niveau vereist. Het is perfect acceptabel voor één Thread/Dispatcher-combinatie om meerdere vensters te beheren, maar soms doen meerdere threads een betere taak. Dit is vooral waar als er een kans is dat een van de vensters de draad zal domineren.
Windows Verkenner werkt op deze manier. Elk nieuw Explorer-venster behoort tot het oorspronkelijke proces, maar wordt gemaakt onder beheer van een onafhankelijke thread. Wanneer Explorer niet meer reageert, zoals bij het zoeken naar netwerkresources, blijven andere Verkenner-vensters responsief en bruikbaar.
We kunnen dit concept demonstreren met het volgende voorbeeld.
De drie bovenste vensters van deze afbeelding delen dezelfde thread-identificatie: 1. De twee andere vensters hebben verschillende thread-id's: Nine en 4. Er is een magentakleurig draaiend ‼️ symbool in de rechterbovenhoek van elk venster.
Dit voorbeeld bevat een venster met een draaiende ‼️ glyph, een knop Onderbreken en twee andere knoppen waarmee een nieuw venster onder de huidige thread of in een nieuwe thread wordt gemaakt. De ‼️ glyph draait voortdurend totdat de knop Onderbreken wordt ingedrukt, waardoor de draad vijf seconden wordt onderbroken. Onderaan het venster wordt de thread-id weergegeven.
Wanneer de knop Onderbreken wordt ingedrukt, worden alle vensters onder dezelfde thread niet meer reagerend. Elk venster onder een andere thread blijft normaal werken.
Het volgende voorbeeld is de XAML voor het venster:
<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>
In het volgende voorbeeld ziet u de code-achter.
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
Hier volgen enkele van de details die u moet noteren:
De Task.Delay(TimeSpan) taak wordt gebruikt om ervoor te zorgen dat de huidige thread vijf seconden wordt onderbroken wanneer de knop Onderbreken wordt ingedrukt.
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 SubDe
SameThreadWindow_Clickgebeurtenis-handler toont onmiddellijk een nieuw venster in de huidige thread. DeNewThreadWindow_Clickgebeurtenis-handler maakt een nieuwe thread waarmee deThreadStartingPointmethode wordt uitgevoerd, die op zijn beurt een nieuw venster weergeeft, zoals beschreven in het volgende opsommingsteken.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 SubDe
ThreadStartingPointmethode is het startpunt voor de nieuwe thread. Het nieuwe venster wordt gemaakt onder het beheer van deze thread. WPF maakt automatisch een nieuwe System.Windows.Threading.Dispatcher om de nieuwe thread te beheren. Het enige wat we moeten doen om het venster functioneel te maken, is het starten van de System.Windows.Threading.Dispatcher.private void ThreadStartingPoint() { new MultiWindow().Show(); System.Windows.Threading.Dispatcher.Run(); }Private Sub ThreadStartingPoint() Dim window As New MultiWindow() window.Show() System.Windows.Threading.Dispatcher.Run() End Sub
Een voorbeeld-app waarin de concepten van deze sectie worden gedemonstreerd, kan worden gedownload van GitHub voor C# of Visual Basic.
Een blokkeringsbewerking afhandelen met Task.Run
Het verwerken van blokkeringsbewerkingen in een grafische toepassing kan lastig zijn. We willen blokkerende methoden van gebeurtenis-handlers niet aanroepen omdat de toepassing lijkt vast te lopen. In het vorige voorbeeld zijn nieuwe vensters in hun eigen thread gemaakt, zodat elk venster onafhankelijk van elkaar kan worden uitgevoerd. Hoewel we een nieuwe thread kunnen maken, System.Windows.Threading.Dispatcherwordt het lastig om de nieuwe thread te synchroniseren met de hoofd-UI-thread nadat het werk is voltooid. Omdat de nieuwe thread de gebruikersinterface niet rechtstreeks kan wijzigen, moeten we Dispatcher.InvokeAsync, Dispatcher.BeginInvoke, of Dispatcher.Invoke gebruiken om gedelegeerden in te voegen in de Dispatcher van de ui-thread. Uiteindelijk worden deze gemachtigden uitgevoerd met toestemming om UI-elementen te wijzigen.
Er is een eenvoudigere manier om de code uit te voeren op een nieuwe thread en tegelijkertijd de resultaten te synchroniseren: het taakgebaseerde asynchrone patroon (TAP). Deze is gebaseerd op de Task en Task<TResult> typen in de System.Threading.Tasks naamruimte, die worden gebruikt om asynchrone bewerkingen weer te geven. TAP gebruikt één methode om de start en voltooiing van een asynchrone bewerking aan te geven. Dit patroon heeft enkele voordelen:
- De aanroeper van een
Taskkan ervoor kiezen om de code asynchroon of synchroon uit te voeren. - De voortgang kan worden gerapporteerd vanuit de
Task. - De aanroepende code kan de uitvoering onderbreken en wachten op het resultaat van de bewerking.
Voorbeeld van Task.Run
In dit voorbeeld simuleren we een externe procedure-aanroep waarmee een weersvoorspelling wordt opgehaald. Wanneer op de knop wordt geklikt, wordt de gebruikersinterface bijgewerkt om aan te geven dat het ophalen van gegevens wordt uitgevoerd, terwijl een taak de weersvoorspelling nabootst. Wanneer de taak wordt gestart, wordt de eventhandler-code van de knop opgeschort totdat de taak is voltooid. Nadat de taak is voltooid, blijft de code van de gebeurtenis-handler actief. De code is onderbroken en blokkeert niet de rest van de UI-thread. De synchronisatiecontext van WPF zorgt ervoor dat de code wordt onderbroken, waardoor WPF kan worden uitgevoerd.
Een diagram waarin de werkstroom van de voorbeeld-app wordt gedemonstreerd. De app heeft één knop met de tekst 'Weerbericht ophalen'. Er is een pijl die verwijst naar de volgende fase van de app nadat de knop is ingedrukt. Dit is een klokafbeelding in het midden van de app die aangeeft dat de app bezig is met het ophalen van gegevens. Na enige tijd keert de app terug met een afbeelding van de zon of regenwolken, afhankelijk van het resultaat van de gegevens.
Een voorbeeld-app waarin de concepten van deze sectie worden gedemonstreerd, kan worden gedownload van GitHub voor C# of Visual Basic. De XAML voor dit voorbeeld is vrij groot en niet opgegeven in dit artikel. Gebruik de vorige GitHub-koppelingen om door de XAML te bladeren. De XAML gebruikt één knop om het weer op te halen.
Houd rekening met de code achter de 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
Hier volgen enkele van de details die u moet noteren.
De eventhandler van de knop
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 SubU ziet dat de gebeurtenis-handler is gedeclareerd met
async(ofAsyncmet Visual Basic). Met een 'async'-methode kunt u de code opschorten wanneer een verwachte methode, zoalsFetchWeatherFromServerAsync, wordt aangeroepen. Dit wordt aangeduid door hetawaittrefwoord (ofAwaitmet Visual Basic). Totdat deFetchWeatherFromServerAsyncis voltooid, wordt de handlercode van de knop onderbroken en wordt de controle teruggegeven aan de aanroeper. Dit is vergelijkbaar met een synchrone methode, behalve dat een synchrone methode wacht tot elke bewerking in de methode is voltooid waarna het besturingselement wordt geretourneerd aan de aanroeper.In afwachtingsmethoden wordt gebruikgemaakt van de threadingcontext van de huidige methode, die met de knophandler de UI-thread is. Dit betekent dat het aanroepen van
await FetchWeatherFromServerAsync();(ofAwait FetchWeatherFromServerAsync()met Visual Basic) ervoor zorgt dat de code inFetchWeatherFromServerAsyncop de UI-thread wordt uitgevoerd, maar het wordt niet uitgevoerd totdat de dispatcher tijd heeft om het uit te voeren, vergelijkbaar met de manier waarop het Enkelvoudige toepassing met een langdurige berekening-voorbeeld werkt. Merk echter op datawait Task.Runwordt gebruikt. Hiermee maakt u een nieuwe thread in de threadgroep voor de aangewezen taak in plaats van de huidige thread. DusFetchWeatherFromServerAsyncdraait op een eigen thread.Het weer ophalen
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 FunctionOm alles eenvoudig te houden, hebben we in dit voorbeeld geen netwerkcode. In plaats daarvan simuleren we de vertraging van de netwerktoegang door onze nieuwe thread vier seconden in de slaapstand te zetten. In deze periode wordt de oorspronkelijke UI-thread nog steeds uitgevoerd en reageert deze op UI-gebeurtenissen, terwijl de event-handler van de knop wordt gepauzeerd totdat de nieuwe thread is voltooid. Om dit te laten zien, hebben we een animatie uitgevoerd en kunt u het formaat van het venster wijzigen. Als de UI-thread is onderbroken of vertraagd, wordt de animatie niet weergegeven en kunt u niet communiceren met het venster.
Wanneer de
Task.Delayis voltooid en we onze weersvoorspelling willekeurig hebben geselecteerd, wordt de weersstatus aan de beller teruggekoppeld.De gebruikersinterface bijwerken
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 SubWanneer de taak is voltooid en de UI-thread tijd heeft, wordt de aanroeper van de gebeurtenisverwerker van de knop
Task.Runhervat. De rest van de methode stopt de klokanimatie en kiest een afbeelding om het weer te beschrijven. Deze afbeelding wordt weergegeven en de knop 'Prognose ophalen' is ingeschakeld.
Een voorbeeld-app waarin de concepten van deze sectie worden gedemonstreerd, kan worden gedownload van GitHub voor C# of Visual Basic.
Technische details en tumblingpunten
In de volgende secties worden enkele details en tumblingpunten beschreven die u kunt tegenkomen met multithreading.
In elkaar geschakelde pompen
Soms is het niet haalbaar om de UI-thread volledig te vergrendelen. Laten we eens kijken naar de Show methode van de MessageBox klasse. Show keert pas terug als de gebruiker op de knop OK klikt. Er wordt echter wel een venster gemaakt dat een berichtenlus moet hebben om interactief te kunnen zijn. Terwijl we wachten totdat de gebruiker op OK klikt, reageert het oorspronkelijke toepassingsvenster niet op gebruikersinvoer. Het blijft echter tekenberichten verwerken. Het oorspronkelijke venster wordt opnieuw getekend wanneer het wordt bedekt en onthuld.
Sommige threads moeten verantwoordelijk zijn voor het berichtvakvenster. WPF kan een nieuwe thread maken alleen voor het berichtvakvenster, maar deze thread zou de uitgeschakelde elementen in het oorspronkelijke venster niet kunnen schilderen (onthoud de eerdere discussie over wederzijdse uitsluiting). In plaats daarvan maakt WPF gebruik van een genest berichtverwerkingssysteem. De Dispatcher klasse bevat een speciale methode die wordt aangeroepen PushFrame, waarin het huidige uitvoeringspunt van een toepassing wordt opgeslagen en vervolgens een nieuwe berichtlus wordt gestart. Wanneer de geneste berichtlus is voltooid, wordt de uitvoering hervat na de oorspronkelijke PushFrame aanroep.
In dit geval behoudt PushFrame de programmacontext bij de aanroep naar MessageBox.Show, en wordt er een nieuwe berichtlus gestart om het achtergrondvenster opnieuw te tekenen en de invoer voor het berichtvakvenster af te handelen. Wanneer de gebruiker op OK klikt en het pop-upvenster wist, wordt de geneste lus afgesloten en gaat de controle door na de aanroep naar Show.
Verlopen gerouteerde gebeurtenissen
Het gerouteerde gebeurtenissysteem in WPF meldt volledige bomen wanneer gebeurtenissen worden gegenereerd.
<Canvas MouseLeftButtonDown="handler1"
Width="100"
Height="100"
>
<Ellipse Width="50"
Height="50"
Fill="Blue"
Canvas.Left="30"
Canvas.Top="50"
MouseLeftButtonDown="handler2"
/>
</Canvas>
Wanneer de linkermuisknop op de ellips wordt gedrukt, dan wordt handler2 uitgevoerd. Na handler2 voltooiing wordt de gebeurtenis doorgegeven aan het Canvas object, dat handler1 gebruikt om het te verwerken. Dit gebeurt alleen als het gebeurtenisobject door handler2 niet expliciet als verwerkt wordt gemarkeerd.
Het is mogelijk dat handler2 het veel tijd kost om deze gebeurtenis te verwerken.
handler2 zou PushFrame kunnen gebruiken om een geneste berichtlus te starten die urenlang niet retourneert. Als handler2 de gebeurtenis niet als verwerkt markeert nadat deze berichtlus is voltooid, wordt de gebeurtenis naar boven doorgegeven in de boomstructuur, ook al is deze erg oud.
Heringang en vergrendeling
Het vergrendelingsmechanisme van de Common Language Runtime (CLR) gedraagt zich niet precies zoals men zich zou kunnen voorstellen; een thread kan verwachten dat de bewerking volledig wordt beëindigd bij het aanvragen van een vergrendeling. In werkelijkheid blijft de thread berichten met hoge prioriteit ontvangen en verwerken. Dit helpt impasses te voorkomen en interfaces minimaal responsief te maken, maar het introduceert de mogelijkheid voor subtiele bugs. De overgrote meerderheid van de tijd hoeft u hier niets van te weten, maar onder zeldzame omstandigheden (meestal met Win32-vensterberichten of COM STA-onderdelen) kan dit de moeite waard zijn om te weten.
De meeste interfaces zijn niet gebouwd met threadveiligheid in het achterhoofd, omdat ontwikkelaars werken aan de veronderstelling dat een gebruikersinterface nooit wordt geopend door meer dan één thread. In dit geval kan die ene thread op onverwachte momenten omgevingswijzigingen aanbrengen, waardoor deze slechte effecten worden veroorzaakt die het DispatcherObject wederzijdse uitsluitingsmechanisme moet oplossen. Houd rekening met de volgende pseudocode:
Meestal is dat de juiste keuze, maar in WPF zijn er momenten waarop zulke onverwachte reentrancy echt voor problemen kan zorgen. Dus, op bepaalde belangrijke momenten roept WPF DisableProcessing aan, waardoor de vergrendelingsinstructie voor die thread wordt gewijzigd om een vergrendeling zonder WPF-reentranties te gebruiken, in plaats van de gebruikelijke CLR-vergrendeling.
Waarom heeft het CLR-team dit gedrag gekozen? Het moest te maken hebben met COM STA-objecten en de finalisatiethread. Wanneer een object wordt verzameld door de garbage collector, wordt de Finalize methode uitgevoerd op de toegewezen finalizer-thread, niet op de UI-thread. En daar ligt het probleem, omdat een COM STA-object dat is gemaakt op de UI-thread alleen kan worden verwijderd op de UI-thread. De CLR doet het equivalent van een BeginInvoke (in dit geval met behulp van Win32's SendMessage). Maar als de UI-thread bezet is, is de finalizer-thread vastgelopen en kan het COM STA-object niet worden verwijderd, waardoor er een ernstig geheugenlek ontstaat. Het CLR-team heeft dus de moeilijke beslissing genomen om vergrendelingen te laten werken zoals ze nu doen.
De taak voor WPF is om onverwachte reentrancy te voorkomen zonder het geheugenlek opnieuw te introduceren, wat de reden is dat we reentrancy niet overal blokkeren.
Zie ook
.NET Desktop feedback