Trådningsmodell

Windows Presentation Foundation (WPF) är utformat för att rädda utvecklare från svårigheterna med trådning. Därför skriver de flesta WPF-utvecklare inte ett gränssnitt som använder mer än en tråd. Eftersom flertrådade program är komplexa och svåra att felsöka bör de undvikas när det finns enkla trådade lösningar.

Oavsett hur välkonstruerat det är kan dock inget gränssnittsramverk tillhandahålla en enkeltrådad lösning för alla typer av problem. WPF kommer nära, men det finns fortfarande situationer där flera trådar förbättrar svarstiden för användargränssnittet (UI) eller programprestanda. Efter att ha diskuterat lite bakgrundsmaterial utforskar den här artikeln några av dessa situationer och avslutas sedan med en diskussion om några detaljer på lägre nivå.

Anmärkning

I det här avsnittet beskrivs trådning med hjälp av metoden InvokeAsync för asynkrona anrop. Metoden InvokeAsync tar en Action eller Func<TResult> som en parameter och returnerar en DispatcherOperation eller DispatcherOperation<TResult>, som har en Task egenskap. Du kan använda nyckelordet await med antingen DispatcherOperation eller den associerade Task. Om du behöver vänta synkront på Task som returneras av en DispatcherOperation eller DispatcherOperation<TResult>, kan du anropa DispatcherOperationWait-tilläggsmetoden. Anrop Task.Wait resulterar i ett dödläge. Mer information om hur du använder en Task för att utföra asynkrona åtgärder finns i Aktivitetsbaserad asynkron programmering.

Om du vill göra ett synkront anrop använder du Invoke-metoden, som också har överlagringar som tar en delegat, Action eller Func<TResult>-parameter.

Översikt och avsändaren

Vanligtvis börjar WPF-program med två trådar: en för hantering av rendering och en annan för att hantera användargränssnittet. Återgivningstråden körs effektivt dold i bakgrunden medan användargränssnittstråden tar emot indata, hanterar händelser, målar skärmen och kör programkod. De flesta program använder en enda användargränssnittstråd, men i vissa situationer är det bäst att använda flera. Vi diskuterar detta med ett exempel senare.

UI-tråden köar arbetsobjekt i ett objekt som kallas Dispatcher. Väljer Dispatcher arbetsobjekt på prioritetsbasis och kör var och en till slutförande. Varje användargränssnittstråd måste ha minst en Dispatcher, och var och en Dispatcher kan köra arbetsobjekt i exakt en tråd.

Tricket med att skapa dynamiska, användarvänliga program är att maximera Dispatcher dataflödet genom att hålla arbetsobjekten små. På så sätt hamnar aldrig objekt i kön och blir inaktuella medan de väntar på att behandlas av Dispatcher. Eventuell fördröjning mellan indata och svar kan hindra en användare.

Hur ska WPF-program då hantera stora åtgärder? Vad händer om koden omfattar en stor beräkning eller behöver köra frågor mot en databas på någon fjärrserver? Vanligtvis är svaret att hantera den stora åtgärden i en separat tråd, och lämna UI-tråden fri för att hantera objekt i Dispatcher kön. När den stora åtgärden är klar kan den rapportera resultatet tillbaka till användargränssnittstråden för visning.

Tidigare tillåter Windows att gränssnittselement endast kan nås av tråden som skapade dem. Det innebär att en bakgrundstråd som ansvarar för vissa långvariga aktiviteter inte kan uppdatera en textruta när den är klar. Windows gör detta för att säkerställa integriteten för gränssnittskomponenter. En listruta kan verka konstig om dess innehåll uppdateras av en bakgrundstråd medan den renderas.

WPF har en inbyggd mekanism för ömsesidig uteslutning som tillämpar den här samordningen. De flesta klasser i WPF härleds från DispatcherObject. Under konstruktion lagrar en DispatcherObject en referens till den Dispatcher länkad till den tråd som för närvarande körs. I själva verket är DispatcherObject associerat med tråden som skapar den. Under programkörning kan en DispatcherObject anropa sin offentliga VerifyAccess metod. VerifyAccess undersöker den Dispatcher som är associerad med den aktuella tråden och jämför den med den referens som lagrades under Dispatcher konstruktionen. Om de inte matchar, VerifyAccess utlöser ett undantag. VerifyAccess är avsedd att anropas i början av varje metod som tillhör en DispatcherObject.

Hur interagerar bakgrundstrådar med användaren om bara en tråd kan ändra användargränssnittet? En bakgrundstråd kan be användargränssnittstråden att utföra en åtgärd för dess räkning. Det gör den genom att registrera ett arbetsobjekt på Dispatcher-användargränssnittstråden. Klassen Dispatcher innehåller metoder för att registrera arbetsobjekt: Dispatcher.InvokeAsync, Dispatcher.BeginInvokeoch Dispatcher.Invoke. Dessa metoder schemalägger en delegerad för utförande. Invoke är ett synkront anrop – det vill säga att den inte returnerar förrän användargränssnittstråden faktiskt har slutfört exekveringen av delegaten. InvokeAsync och BeginInvoke är asynkrona och returnerar omedelbart.

Dispatcher sorterar elementen i kön efter prioritet. Det finns tio nivåer som kan anges när du lägger till ett element i Dispatcher kön. Dessa prioriteringar bibehålls i DispatcherPriority uppräkningen.

Entrådad app med en tidskrävande beräkning

De flesta grafiska användargränssnitt (GUIs) tillbringar en stor del av sin tid i viloläge i väntan på händelser som genereras som svar på användarinteraktioner. Med noggrann programmering kan den här inaktiva tiden användas konstruktivt, utan att påverka användargränssnittets svarstider. WPF-trådningsmodellen tillåter inte att indata avbryter en åtgärd som utförs i användargränssnittstråden. Det innebär att du måste vara säker på att gå tillbaka till Dispatcher regelbundet för att bearbeta väntande indatahändelser innan de blir inaktuella.

En exempelapp som visar begreppen i det här avsnittet kan laddas ned från GitHub för C # eller Visual Basic.

Tänk på följande exempel:

Skärmbild som visar trådning av primtal.

Det här enkla programmet räknas upp från tre och söker efter primtal. När användaren klickar på startknappen börjar sökningen. När programmet hittar en prime uppdateras användargränssnittet med dess identifiering. När som helst kan användaren stoppa sökningen.

Även om det är tillräckligt enkelt kan sökningen av primtal fortsätta för alltid, vilket medför vissa svårigheter. Om vi hanterade hela sökningen inuti knappens klickhändelsehanterare skulle vi aldrig ge användargränssnittstråden en chans att hantera andra händelser. Användargränssnittet skulle inte kunna svara på indata eller bearbeta meddelanden. Den skulle aldrig måla om och svara aldrig på knappklick.

Vi skulle kunna utföra primärnummersökningen i en separat tråd, men då skulle vi behöva hantera synkroniseringsproblem. Med en enkeltrådad metod kan vi direkt uppdatera etiketten som visar det största primtal som hittats.

Om vi delar upp beräkningsuppgiften i hanterbara segment kan vi regelbundet återgå till Dispatcher och bearbeta händelser. Vi kan ge WPF en möjlighet att måla om och bearbeta indata.

Det bästa sättet att dela upp bearbetningstiden mellan beräkning och händelsehantering är att hantera beräkningen från Dispatcher. Genom att använda InvokeAsync-metoden kan vi schemalägga primtalskontroller i samma kö som användargränssnittshändelser hämtas från. I vårt exempel schemalägger vi bara en enda primtalskontroll i taget. När den primära nummerkontrollen är klar schemalägger vi nästa kontroll omedelbart. Den här kontrollen fortsätter först när väntande användargränssnittshändelser har hanterats.

Skärmbild som visar dispatcher-kön.

Microsoft Word utför stavningskontroll med den här mekanismen. Stavningskontroll görs i bakgrunden medan användargränssnittstråden är inaktiv. Låt oss ta en titt på koden.

I följande exempel visas XAML som skapar användargränssnittet.

Viktigt!

XAML som visas i den här artikeln kommer från ett C#-projekt. Visual Basic XAML skiljer sig något när du deklarerar bakgrundsklassen för XAML.

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

I följande exempel visas koden bakom.

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

Förutom att uppdatera texten på ButtonStartStopButton_Click ansvarar hanteraren för att schemalägga den första huvudnummerkontrollen genom att lägga till ett ombud i Dispatcher kön. Någon gång efter att den här händelsehanteraren har slutfört sitt arbete, kommer Dispatcher att välja delegaten för körning.

Som vi nämnde tidigare är InvokeAsync den Dispatcher medlem som används för att schemalägga en delegat för körning. I det här fallet väljer vi prioriteten SystemIdle . Dispatcher kommer att utföra denna delegering endast när det inte finns några viktiga händelser att bearbeta. UI-svarstider är viktigare än nummerkontroll. Vi skickar också ett nytt ombud som representerar nummerkontrollrutinen.

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

Den här metoden kontrollerar om nästa udda tal är prime. Om det är ett primtal uppdaterar metoden direkt bigPrimeTextBlock så att det återspeglar dess upptäckt. Vi kan göra detta eftersom beräkningen sker i samma tråd som användes för att skapa kontrollen. Om vi hade valt att använda en separat tråd för beräkningen skulle vi behöva använda en mer komplicerad synkroniseringsmekanism och köra uppdateringen i användargränssnittstråden. Vi ska demonstrera den här situationen härnäst.

Flera fönster, flera trådar

Vissa WPF-program kräver flera fönster på den översta nivån. Det är helt acceptabelt att en tråd/dispatcher-kombination hanterar flera fönster, men ibland gör flera trådar ett bättre jobb. Detta gäller särskilt om det finns någon chans att ett av fönstren kommer att monopolisera tråden.

Utforskaren i Windows fungerar på det här sättet. Varje nytt Utforskarfönster tillhör den ursprungliga processen, men det skapas under kontroll av en oberoende tråd. När Explorer inte svarar, till exempel när du letar efter nätverksresurser, fortsätter andra Explorer-fönster att vara dynamiska och användbara.

Vi kan demonstrera det här konceptet med följande exempel.

En skärmbild av ett WPF-fönster som dupliceras fyra gånger. Tre av fönstren anger att de använder samma tråd, medan de andra två är på olika trådar.

De tre översta fönstren i den här bilden delar samma trådidentifierare: 1. De två andra fönstren har olika trådidentifierare: Nio och 4. Det finns en magentafärgad roterande ! !️ glyph i det övre högra hörnet av varje fönster.

Det här exemplet innehåller ett fönster med en roterande ‼️ glyf, en pausknapp och två andra knappar som skapar ett nytt fönster under den aktuella tråden eller i en ny tråd. Glyfen ‼️ roterar hela tiden tills pausknappen trycks in, vilket pausar tråden i fem sekunder. Längst ned i fönstret visas trådidentifieraren.

När Pause-knappen trycks in blir alla fönster under samma tråd icke-responsiva. Alla fönster under en annan tråd fortsätter att fungera normalt.

Följande exempel är XAML för fönstret:

<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>

I följande exempel visas koden bakom.

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

Följande är några av de uppgifter som ska noteras:

  • Uppgiften Task.Delay(TimeSpan) används för att göra så att den aktuella tråden pausas i fem sekunder när knappen Pausa trycks ned.

    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
    
  • Händelsehanteraren SameThreadWindow_Click visar omedelbart ett nytt fönster under den aktuella tråden. Händelsehanteraren NewThreadWindow_Click skapar en ny tråd som börjar köra ThreadStartingPoint metoden, som i sin tur visar ett nytt fönster, enligt beskrivningen i nästa punkt.

    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
    
  • Metoden ThreadStartingPoint är startpunkten för den nya tråden. Det nya fönstret skapas under kontroll av den här tråden. WPF skapar automatiskt en ny System.Windows.Threading.Dispatcher för att hantera den nya tråden. Allt vi behöver göra för att fönstret ska fungera är att starta 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
    

En exempelapp som visar begreppen i det här avsnittet kan laddas ned från GitHub för C # eller Visual Basic.

Hantera en blockeringsåtgärd med Task.Run

Det kan vara svårt att hantera blockeringsåtgärder i ett grafiskt program. Vi vill inte anropa blockeringsmetoder från händelsehanterare eftersom programmet verkar frysa. I föregående exempel skapades nya fönster i sin egen tråd, så att varje fönster kan köras oberoende av varandra. Även om vi kan skapa en ny tråd med System.Windows.Threading.Dispatcherblir det svårt att synkronisera den nya tråden med huvudgränssnittstråden när arbetet har slutförts. Eftersom den nya tråden inte kan ändra användargränssnittet direkt måste vi använda Dispatcher.InvokeAsync, Dispatcher.BeginInvokeeller Dispatcher.Invoke, för att infoga ombud i Dispatcher användargränssnittstråden. Slutligen körs dessa ombud med behörighet att ändra användargränssnittselement.

Det finns ett enklare sätt att köra koden på en ny tråd när du synkroniserar resultatet, det aktivitetsbaserade asynkrona mönstret (TAP). Den baseras på typerna Task och Task<TResult> i System.Threading.Tasks namnområdet, som används för att representera asynkrona åtgärder. TAP använder en enda metod för att representera initieringen och slutförandet av en asynkron åtgärd. Det finns några fördelar med det här mönstret:

  • Anroparen för en Task kan välja att köra koden asynkront eller synkront.
  • Förloppet kan rapporteras från Task.
  • Den anropande koden kan pausa körningen och vänta på resultatet av operationen.

Task.Run-exempel

I det här exemplet efterliknar vi ett fjärrproceduranrop som hämtar en väderprognos. När knappen klickas uppdateras användargränssnittet för att indikera att datahämtningen pågår, medan en uppgift startas för att efterlikna hämtningen av väderprognosen. När aktiviteten startas pausas knapphändelsehanterarkoden tills aktiviteten är klar. När aktiviteten är klar fortsätter händelsehanterarkoden att köras. Koden pausas och blockerar inte resten av användargränssnittstråden. Synkroniseringskontexten för WPF hanterar pausning av koden, vilket gör att WPF kan fortsätta att köras.

Ett diagram som visar arbetsflödet för exempelappen.

Ett diagram som visar exempelappens arbetsflöde. Appen har en enda knapp med texten "Hämta prognos". Det finns en pil som pekar på nästa fas i appen när knappen har tryckts ned, vilket är en klockbild placerad i mitten av appen som anger att appen är upptagen med att hämta data. Efter en tid returnerar appen antingen en bild av solen eller regnmoln, beroende på resultatet av data.

En exempelapp som visar begreppen i det här avsnittet kan laddas ned från GitHub för C # eller Visual Basic. XAML för det här exemplet är ganska stort och tillhandahålls inte i den här artikeln. Använd de tidigare GitHub-länkarna för att bläddra i XAML. XAML använder en enda knapp för att hämta vädret.

Överväg koden bakom 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

Följande är några av de detaljer som ska noteras.

  • Knapphändelsehanteraren

    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
    

    Observera att händelsehanteraren deklarerades med async (eller Async med Visual Basic). Med en "async"-metod kan koden avbrytas när en väntande metod, till exempel FetchWeatherFromServerAsync, anropas. Detta anges med nyckelordet await (eller Await med Visual Basic). Tills den FetchWeatherFromServerAsync är klar pausas knappens hanteringskod och kontrollen returneras till anroparen. Detta liknar en synkron metod förutom att en synkron metod väntar på att varje åtgärd i metoden ska slutföras varefter kontrollen returneras till anroparen.

    Väntande metoder använder trådkontexten för den aktuella metoden, som med knapphanteraren är användargränssnittstråden. Det innebär att anrop await FetchWeatherFromServerAsync(); (eller Await FetchWeatherFromServerAsync() med Visual Basic) gör att koden FetchWeatherFromServerAsync i körs på användargränssnittstråden, men inte körs på avsändaren har tid att köra den, ungefär som den entrådade appen med ett tidskrävande beräkningsexempel . Observera dock att await Task.Run används. Detta skapar en ny tråd i trådpoolen för den avsedda aktiviteten i stället för den aktuella tråden. Så FetchWeatherFromServerAsync körs på sin egen tråd.

  • Hämta vädret

    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
    

    För att hålla det enkelt har vi faktiskt ingen nätverkskod i det här exemplet. I stället simulerar vi fördröjningen av nätverksåtkomsten genom att försätta vår nya tråd i viloläge i fyra sekunder. Under den här tiden körs den ursprungliga användargränssnittstråden fortfarande och svarar på användargränssnittshändelser medan knappens händelsehanterare pausas tills den nya tråden har slutförts. För att visa detta har vi lämnat en animering igång och du kan ändra storlek på fönstret. Om användargränssnittstråden pausades eller försenades skulle animeringen inte visas och du kunde inte interagera med fönstret.

    När Task.Delay är klar, och väderprognosen slumpmässigt har valts, returneras väderstatusen till anroparen.

  • Uppdatera användargränssnittet

    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
    

    När uppgiften är klar och användargränssnittstråden har tid återupptas anroparen för Task.Runknappens händelsehanterare. Resten av metoden stoppar klockanimeringen och väljer en bild för att beskriva vädret. Den visar den här bilden och aktiverar knappen "hämta prognos".

En exempelapp som visar begreppen i det här avsnittet kan laddas ned från GitHub för C # eller Visual Basic.

Teknisk information och utmaningar

I följande avsnitt beskrivs några av de detaljer och stötestenar som du kan stöta på med multitrådning.

Strukturerad pumpning

Ibland är det inte möjligt att helt låsa användargränssnittstråden. Nu ska vi överväga Show -metoden för MessageBox klassen. Show returneras inte förrän användaren klickar på OK-knappen. Den skapar dock ett fönster som måste ha en meddelandeloop för att vara interaktiv. Medan vi väntar på att användaren ska klicka på OK svarar inte det ursprungliga programfönstret på användarindata. Det fortsätter dock att bearbeta färgmeddelanden. Det ursprungliga fönstret ritar om sig själv när det täcks och avslöjas.

Skärmbild som visar en MessageBox med en OK-knapp

En tråd måste vara ansvarig för meddelanderutans fönster. WPF kan skapa en ny tråd bara för meddelanderutans fönster, men den här tråden skulle inte kunna måla de inaktiverade elementen i det ursprungliga fönstret (kom ihåg den tidigare diskussionen om ömsesidig uteslutning). I stället använder WPF ett kapslat meddelandebearbetningssystem. Klassen Dispatcher innehåller en särskild metod med namnet PushFrame, som lagrar ett programs aktuella körningspunkt och sedan påbörjar en ny meddelandeloop. När den kapslade meddelandeloopen är klar återupptas körningen efter det ursprungliga PushFrame anropet.

I det här fallet upprätthåller PushFrame programkontexten vid anropet till MessageBox.Show. Den startar en ny meddelandecykel för att måla om bakgrundsfönstret och hantera indata till fönstret i meddelanderutan. När användaren klickar på OK och rensar popup-fönstret avslutas den kapslade loopen och kontrollen återupptas efter anropet till Show.

Inaktuella routade händelser

Det dirigerade händelsesystemet i WPF meddelar hela träd när händelser genereras.

<Canvas MouseLeftButtonDown="handler1"
        Width="100"
        Height="100"
        >
  <Ellipse Width="50"
            Height="50"
            Fill="Blue"
            Canvas.Left="30"
            Canvas.Top="50"
            MouseLeftButtonDown="handler2"
            />
</Canvas>

När den vänstra musknappen trycks över ellipsen handler2 körs. När handler2 den är klar skickas händelsen vidare till Canvas objektet, som använder handler1 för att bearbeta det. Detta sker endast om handler2 inte uttryckligen markerar händelseobjektet som hanterat.

Det är möjligt att det tar mycket tid att handler2 bearbeta den här händelsen. handler2 kan använda PushFrame för att starta en kapslad meddelandeloop som inte returneras på flera timmar. Om handler2 inte markerar händelsen som hanterad när den här meddelandeloopen är klar skickas händelsen upp i trädet trots att den är mycket gammal.

Återinträde och låsning

Låsningsmekanismen för CLR (Common Language Runtime) fungerar inte exakt som man kan föreställa sig. man kan förvänta sig att en tråd upphör helt när du begär ett lås. I själva verket fortsätter tråden att ta emot och bearbeta meddelanden med hög prioritet. Detta hjälper till att förhindra dödlägen och göra gränssnitten minimalt dynamiska, men det ger möjlighet till subtila buggar. Den stora majoriteten av tiden behöver du inte veta något om detta, men under sällsynta omständigheter (vanligtvis med Win32-fönstermeddelanden eller COM STA-komponenter) kan detta vara värt att veta.

De flesta gränssnitt skapas inte med trådsäkerhet i åtanke eftersom utvecklare arbetar under antagandet att ett användargränssnitt aldrig används av mer än en tråd. I det här fallet kan den enda tråden göra miljöförändringar vid oväntade tillfällen, vilket orsakar de dåliga effekter som mekanismen DispatcherObject för ömsesidig uteslutning är tänkt att lösa. Överväg följande pseudokod:

Diagram som visar trådningsreentitet.

För det mesta är det rätt sak att göra, men det finns tillfällen i WPF där sådan oväntad återinträde kan verkligen orsaka problem. Så vid vissa viktiga tidpunkter anropar WPF DisableProcessing, vilket ändrar låsinstruktionen för den tråden så att den använder WPF-låset utan återinträde, istället för det vanliga CLR-låset.

Så varför valde CLR-teamet det här beteendet? Det hade att göra med COM STA-objekt och slutförandetråden. När ett objekt samlas in av skräpsamlaren körs dess Finalize-metod på den dedikerade finaliserartråden, inte i användargränssnittstråden. Och däri ligger problemet, eftersom ett COM STA-objekt som skapades i användargränssnittstråden bara kan tas bort i användargränssnittstråden. CLR gör motsvarigheten till en BeginInvoke (i det här fallet med Win32s SendMessage). Men om användargränssnittstråden är upptagen stoppas finalizertråden och COM STA-objektet kan inte tas bort, vilket skapar en allvarlig minnesläcka. Så tog CLR-teamet det svåra beslutet för att få låsen att fungera som de gör.

Uppgiften för WPF är att undvika oväntad återaktivering utan att återinföra minnesläckan, vilket är anledningen till att vi inte blockerar återaktivering överallt.

Se även