Model vláken

Windows Presentation Foundation (WPF) je navržený tak, aby vývojářům zabránil v potížích s vlákny. V důsledku toho většina vývojářů WPF nenapisuje rozhraní, které používá více než jedno vlákno. Vzhledem k tomu, že vícevláknové programy jsou složité a obtížně laděné, měly by se jim vyhnout, pokud existují řešení s jedním vláknem.

Bez ohledu na to, jak dobře navržená, ale žádná architektura uživatelského rozhraní nedokáže poskytnout jednovláknové řešení pro každý druh problému. WPF se blíží, ale stále existují situace, kdy více vláken zlepšuje odezvu uživatelského rozhraní (UI) nebo výkon aplikace. Po diskuzi o některých podkladových materiálech se tento článek seznámí s některými z těchto situací a pak skončí diskuzí o některých podrobnostech na nižší úrovni.

Poznámka:

Toto téma popisuje vlákna pomocí InvokeAsync metody pro asynchronní volání. Metoda InvokeAsync přebírá Action nebo Func<TResult> jako parametr a vrací DispatcherOperation nebo DispatcherOperation<TResult>, který má Task vlastnost. Klíčové slovo můžete použít await buď s přidruženým DispatcherOperation nebo přidruženým Taskslovem . Pokud potřebujete synchronně počkat na Task to, co je vráceno metodou DispatcherOperation nebo DispatcherOperation<TResult>, zavolejte metodu DispatcherOperationWait rozšíření. Volání Task.Wait způsobí zablokování. Další informace o použití k Task provádění asynchronních operací naleznete v tématu Asynchronní programování založené na úlohách.

Chcete-li provést synchronní volání, použijte metodu Invoke , která má také přetížení, které přebírá delegáta , Actionnebo Func<TResult> parametr.

Přehled a dispečer

Aplikace WPF obvykle začínají dvěma vlákny: jedno pro zpracování vykreslování a další pro správu uživatelského rozhraní. Vykreslovací vlákno efektivně běží na pozadí, zatímco vlákno uživatelského rozhraní přijímá vstup, zpracovává události, maluje obrazovku a spouští kód aplikace. Většina aplikací používá jedno vlákno uživatelského rozhraní, i když v některých situacích je nejlepší použít několik. Probereme to s příkladem později.

Vlákno uživatelského rozhraní zařadí pracovní položky uvnitř objektu nazývaného Dispatcher. Vybere Dispatcher pracovní položky podle priority a každý z nich se spustí až po dokončení. Každé vlákno uživatelského rozhraní musí mít alespoň jedno Dispatchera každý Dispatcher může spouštět pracovní položky v přesně jednom vlákně.

Trikem při vytváření responzivních a uživatelsky přívětivých aplikací je maximalizace Dispatcher propustnosti tím, že pracovní položky budou malé. Díky tomu se položky nikdy nezastarávají ve frontě Dispatcher čekající na zpracování. Jakákoli zjevná prodleva mezi vstupem a odpovědí může uživatele frustrovat.

Jak pak mají aplikace WPF zpracovávat velké operace? Co když váš kód zahrnuje velký výpočet nebo potřebuje dotazovat databázi na některém vzdáleném serveru? Odpovědí je obvykle zpracování velké operace v samostatném vlákně a ponechání vlákna uživatelského rozhraní volné na položky ve frontě Dispatcher . Po dokončení velké operace může nahlásit výsledek zpět do vlákna uživatelského rozhraní pro zobrazení.

Systém Windows umožňuje přístup k prvkům uživatelského rozhraní pouze vláknem, které je vytvořilo. To znamená, že vlákno na pozadí, které má na starosti některé dlouhotrvající úlohy, nemůže po dokončení aktualizovat textové pole. Systém Windows to dělá, aby se zajistila integrita komponent uživatelského rozhraní. Seznam by mohl vypadat divně, pokud byl jeho obsah aktualizován vláknem pozadí během malování.

WPF má integrovaný mechanismus vzájemného vyloučení, který tuto koordinaci vynucuje. Většina tříd v WPF je odvozena z DispatcherObject. Při sestavování DispatcherObject ukládá odkaz na Dispatcher propojení s aktuálně běžícím vláknem. V důsledku toho DispatcherObject se přidruží k vláknu, které ho vytvoří. Během provádění programu může volat svou veřejnou VerifyAccess metoduDispatcherObject. VerifyAccess zkontroluje Dispatcher přidružené k aktuálnímu vláknu a porovná ho s odkazem Dispatcher uloženým během výstavby. Pokud se neshodují, VerifyAccess vyvolá výjimku. VerifyAccessmá být volána na začátku každé metody, která patří do .DispatcherObject

Pokud uživatelské rozhraní může upravit jenom jedno vlákno, jak s uživatelem komunikují vlákna na pozadí? Vlákno na pozadí může požádat vlákno uživatelského rozhraní, aby provedlo operaci jejím jménem. Provede to registrací pracovní položky ve Dispatcher vlákně uživatelského rozhraní. Třída Dispatcher poskytuje metody pro registraci pracovních položek: Dispatcher.InvokeAsync, Dispatcher.BeginInvokea Dispatcher.Invoke. Tyto metody naplánují delegáta pro spuštění. Invoke je synchronní volání – to znamená, že se nevrací, dokud vlákno uživatelského rozhraní skutečně nedokončí provádění delegáta. InvokeAsync a BeginInvoke jsou asynchronní a vrací se okamžitě.

Pořadí Dispatcher prvků ve frontě podle priority. Při přidávání elementu Dispatcher do fronty může být zadáno deset úrovní. Tyto priority jsou zachovány ve výčtu DispatcherPriority .

Jednovláknová aplikace s dlouhotrvajícím výpočtem

Většina grafických uživatelských rozhraní (GUI) tráví velkou část času nečinností při čekání na události, které se generují v reakci na interakce uživatelů. Při pečlivém programování lze tento nečinný čas použít konstruktivně, aniž by to ovlivnilo odezvu uživatelského rozhraní. Model vláken WPF neumožňuje vstup přerušit operaci probíhající ve vlákně uživatelského rozhraní. To znamená, že se musíte vrátit k Dispatcher pravidelnému zpracování čekajících vstupních událostí, než budou zastaralé.

Ukázková aplikace demonstrující koncepty této části si můžete stáhnout z GitHubu pro jazyk C# nebo Visual Basic.

Představte si následující příklad:

Screenshot that shows threading of prime numbers.

Tato jednoduchá aplikace se počítá směrem nahoru ze tří a hledá první čísla. Když uživatel klikne na tlačítko Start , začne hledání. Když program najde základní, aktualizuje uživatelské rozhraní jeho zjišťováním. Kdykoli může uživatel hledání zastavit.

I když je to dost jednoduché, vyhledávání prime číslo může jít navždy, což představuje některé potíže. Pokud jsme zpracovali celé hledání uvnitř obslužné rutiny události kliknutí tlačítka, nikdy bychom vláknu uživatelského rozhraní nepřidali šanci zpracovat jiné události. Uživatelské rozhraní by nemohlo reagovat na vstupy nebo zpracování zpráv. Nikdy by se nepřekresloval a nikdy nereagoval na kliknutí na tlačítko.

Vyhledávání primárních čísel bychom mohli provést v samostatném vlákně, ale pak bychom se museli zabývat problémy se synchronizací. S přístupem s jedním vláknem můžeme přímo aktualizovat popisek, který obsahuje seznam největších nalezených hlavních položek.

Pokud rozdělíme úkol výpočtu na spravovatelné bloky dat, můžeme se pravidelně vracet k událostem Dispatcher a zpracovávat je. WpF můžeme dát příležitost překreslit a zpracovat vstup.

Nejlepším způsobem, jak rozdělit dobu zpracování mezi výpočet a zpracování událostí, je spravovat výpočet z objektu Dispatcher. Pomocí InvokeAsync této metody můžeme naplánovat kontroly primárních čísel ve stejné frontě, ze které se události uživatelského rozhraní načítají. V našem příkladu naplánujeme vždy jenom jednu kontrolu primárního čísla. Po dokončení kontroly primárního čísla naplánujeme další kontrolu okamžitě. Tato kontrola pokračuje až po zpracování čekajících událostí uživatelského rozhraní.

Screenshot that shows the dispatcher queue.

Microsoft Word provádí kontrolu pravopisu pomocí tohoto mechanismu. Kontrola pravopisu se provádí na pozadí pomocí doby nečinnosti vlákna uživatelského rozhraní. Podívejme se na kód.

Následující příklad ukazuje XAML, který vytvoří uživatelské rozhraní.

Důležité

XAML uvedený v tomto článku pochází z projektu jazyka C#. Visual Basic XAML se mírně liší při deklarování backing třídy pro 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>

Následující příklad ukazuje kód za sebou.

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

Kromě aktualizace textu na Buttonobslužné StartStopButton_Click rutině zodpovídá za naplánování první kontroly primárního čísla přidáním delegáta do fronty Dispatcher . Někdy poté, co tato obslužná rutina události dokončí svou práci, Dispatcher vybere delegáta pro spuštění.

Jak jsme zmínili dříve, je Dispatcher členem, InvokeAsync který slouží k naplánování delegáta pro provádění. V tomto případě zvolíme prioritu SystemIdle . Tento Dispatcher delegát se spustí pouze v případě, že neexistují žádné důležité události ke zpracování. Odezva uživatelského rozhraní je důležitější než kontrola čísel. Předáváme také nového delegáta představujícího rutinu kontroly čísel.

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

Tato metoda zkontroluje, jestli je první liché číslo. Pokud je základní, metoda přímo aktualizuje bigPrimeTextBlock , aby odrážela jeho zjišťování. Můžeme to udělat, protože výpočet probíhá ve stejném vlákně, které bylo použito k vytvoření ovládacího prvku. Pokud jsme se rozhodli pro výpočet použít samostatné vlákno, museli bychom použít složitější synchronizační mechanismus a provést aktualizaci ve vlákně uživatelského rozhraní. Tuto situaci předvedeme v dalším kroku.

Více oken, více vláken

Některé aplikace WPF vyžadují několik oken nejvyšší úrovně. Je naprosto přijatelné pro jednu kombinaci thread/dispatcher spravovat více oken, ale někdy několik vláken dělá lepší úlohu. To platí zejména v případě, že existuje šance, že jeden z oken bude monopolizovat vlákno.

Průzkumník Windows funguje tímto způsobem. Každé nové okno Průzkumníka patří do původního procesu, ale je vytvořeno pod kontrolou nezávislého vlákna. Když se Průzkumník stane nereagující, například když hledáte síťové prostředky, budou ostatní okna Průzkumníka dál responzivní a použitelná.

Tento koncept si můžeme předvést pomocí následujícího příkladu.

A screenshot of a WPF window that's duplicated four times. Three of the windows indicate that they're using the same thread, while the other two are on different threads.

První tři okna tohoto obrázku sdílejí stejný identifikátor vlákna: 1. Dvě další okna mají různé identifikátory vláken: Nine a 4. V pravém horním rohu každého okna je purpurově zbarvená barva! !️ glyf.

Tento příklad obsahuje okno s otočným ‼️ glyfem, tlačítkem Pozastavit a dvěma dalšími tlačítky, která vytvoří nové okno pod aktuálním vláknem nebo v novém vlákně. Glyph ‼️ se neustále otočí, dokud se nestiskne tlačítko Pozastavit , což po dobu pěti sekund pozastaví vlákno. V dolní části okna se zobrazí identifikátor vlákna.

Po stisknutí tlačítka Pozastavit se všechna okna pod stejným vláknem přestanou reagovat. Jakékoli okno pod jiným vláknem bude fungovat normálně.

Následující příklad je XAML pro okno:

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

Následující příklad ukazuje kód za sebou.

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

Tady jsou některé podrobnosti, které je třeba uvést:

  • Úloha Task.Delay(TimeSpan) se používá k pozastavení aktuálního vlákna po dobu pěti sekund při stisknutí tlačítka Pozastavit .

    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
    
  • Obslužná SameThreadWindow_Click rutina události se immediently zobrazí nové okno pod aktuálním vláknem. Obslužná rutina NewThreadWindow_Click události vytvoří nové vlákno, které spustí metodu ThreadStartingPoint , která pak zobrazí nové okno, jak je popsáno v dalším odrážkovém bodu.

    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
    
  • Metoda ThreadStartingPoint je výchozím bodem pro nové vlákno. Nové okno se vytvoří pod kontrolou tohoto vlákna. WPF automaticky vytvoří novou System.Windows.Threading.Dispatcher správu nového vlákna. Jediné, co musíme udělat, aby okno funkční, je spustit 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
    

Ukázková aplikace demonstrující koncepty této části si můžete stáhnout z GitHubu pro jazyk C# nebo Visual Basic.

Zpracování blokující operace pomocí task.Run

Zpracování blokujících operací v grafické aplikaci může být obtížné. Nechceme volat blokující metody z obslužných rutin událostí, protože se zdá, že aplikace zablokuje. Předchozí příklad vytvořil nová okna ve vlastním vlákně, takže každé okno běží nezávisle na sobě. I když můžeme vytvořit nové vlákno s System.Windows.Threading.Dispatcher, stává se obtížné synchronizovat nové vlákno s hlavním vláknem uživatelského rozhraní po dokončení práce. Protože nové vlákno nemůže upravit uživatelské rozhraní přímo, musíme použít Dispatcher.InvokeAsync, Dispatcher.BeginInvokenebo Dispatcher.Invoke, vložit delegáty do Dispatcher vlákna uživatelského rozhraní. Nakonec se tyto delegáty spustí s oprávněním k úpravě prvků uživatelského rozhraní.

Existuje jednodušší způsob, jak spustit kód v novém vlákně při synchronizaci výsledků, asynchronního vzoru založeného na úlohách (TAP). Je založená na typech Task a Task<TResult> typech System.Threading.Tasks v oboru názvů, které se používají k reprezentaci asynchronních operací. TAP používá jedinou metodu k reprezentaci zahájení a dokončení asynchronní operace. Tento model má několik výhod:

  • Volající se Task může rozhodnout spustit kód asynchronně nebo synchronně.
  • Průběh lze oznamovat z Tasknástroje .
  • Volající kód může pozastavit provádění a čekat na výsledek operace.

Příklad Task.Run

V tomto příkladu napodobujeme vzdálené volání procedury, které načte předpověď počasí. Po kliknutí na tlačítko se uživatelské rozhraní aktualizuje, aby indikoval, že probíhá načítání dat, zatímco úkol začne napodobovat načítání počasí. Po spuštění úkolu se kód obslužné rutiny události tlačítka pozastaví, dokud se úkol nedokončí. Po dokončení úlohy bude kód obslužné rutiny události nadále spuštěn. Kód je pozastavený a neblokuje zbytek vlákna uživatelského rozhraní. Kontext synchronizace WPF zpracovává pozastavení kódu, což umožňuje, aby WPF nadále běžel.

A diagram that demonstrates the workflow of the example app.

Diagram znázorňující pracovní postup ukázkové aplikace Aplikace má jedno tlačítko s textem "Načíst prognózu". Po stisknutí tlačítka se zobrazí šipka ukazující na další fázi aplikace, což je obrázek hodin umístěný uprostřed aplikace, který označuje, že aplikace je zaneprázdněná načítáním dat. Po nějaké době se aplikace vrátí buď s obrázkem slunce nebo dešťových mraků v závislosti na výsledku dat.

Ukázková aplikace demonstrující koncepty této části si můžete stáhnout z GitHubu pro jazyk C# nebo Visual Basic. XAML pro tento příklad je poměrně velký a není k dispozici v tomto článku. K procházení XAML použijte předchozí odkazy GitHubu. XAML používá k načtení počasí jedno tlačítko.

Vezměte v úvahu kód 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

Tady jsou některé podrobnosti, které je třeba poznamenat.

  • Obslužná rutina události tlačítka

    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
    

    Všimněte si, že obslužná rutina události byla deklarována pomocí async (nebo Async pomocí jazyka Visual Basic). Metoda "async" umožňuje pozastavení kódu, když je volána očekávaná metoda, například FetchWeatherFromServerAsync, . Toto je určeno klíčovým slovem (nebo Await pomocí jazyka await Visual Basic). FetchWeatherFromServerAsync Dokud se nedokončí, kód obslužné rutiny tlačítka se pozastaví a ovládací prvek se vrátí volajícímu. Podobá se synchronní metodě s tím rozdílem, že synchronní metoda čeká na dokončení každé operace v metodě, po které se ovládací prvek vrátí volajícímu.

    Očekávané metody využívají kontext vlákna aktuální metody, která s obslužnou rutinou tlačítka je vlákno uživatelského rozhraní. To znamená, že volání await FetchWeatherFromServerAsync(); (nebo Await FetchWeatherFromServerAsync() pomocí jazyka Visual Basic) způsobí spuštění kódu ve FetchWeatherFromServerAsync vlákně uživatelského rozhraní, ale není spuštěno na dispečeru, má čas ho spustit, podobně jako aplikace s jedním vláknem s dlouho běžícím příkladem výpočtu . Všimněte si však, že await Task.Run se používá. Tím se vytvoří nové vlákno ve fondu vláken pro určenou úlohu místo aktuálního vlákna. Takže FetchWeatherFromServerAsync běží na svém vlastním vlákně.

  • Načítání počasí

    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
    

    Abychom měli všechno jednoduché, v tomto příkladu ve skutečnosti nemáme žádný síťový kód. Místo toho simulujeme zpoždění síťového přístupu tím, že naše nové vlákno umístíme do režimu spánku po dobu čtyř sekund. V tomto okamžiku je původní vlákno uživatelského rozhraní stále spuštěné a reaguje na události uživatelského rozhraní, zatímco obslužná rutina události tlačítka se pozastaví, dokud se nové vlákno nedokončí. Abychom to mohli předvést, nechali jsme spuštěnou animaci a můžete změnit velikost okna. Pokud bylo vlákno uživatelského rozhraní pozastavené nebo zpožděné, animace se nezobrazila a s oknem se nepovedlo pracovat.

    Task.Delay Po dokončení a náhodně jsme vybrali předpověď počasí, stav počasí se vrátí volajícímu.

  • Aktualizace uživatelského rozhraní

    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
    

    Jakmile se úloha dokončí a vlákno uživatelského rozhraní má čas, volající Task.Runobslužné rutiny události tlačítka se obnoví. Zbytek metody zastaví animaci hodin a zvolí obrázek, který popisuje počasí. Zobrazí tento obrázek a povolí tlačítko Načíst prognózu.

Ukázková aplikace demonstrující koncepty této části si můžete stáhnout z GitHubu pro jazyk C# nebo Visual Basic.

Technické podrobnosti a body pro přeskakující body

Následující části popisují některé podrobnosti a přeskakující body, se kterými se můžete setkat s více vlákny.

Vnořené čerpadlo

Někdy není možné úplně uzamknout vlákno uživatelského rozhraní. Pojďme se podívat na Show metodu MessageBox třídy. Show se nevrátí, dokud uživatel neklikne na tlačítko OK. Vytvoří však okno, které musí mít smyčku zpráv, aby bylo možné interaktivně používat. Zatímco čekáme, až uživatel klikne na OK, původní okno aplikace nereaguje na vstup uživatele. Ale i nadále zpracovává malování zpráv. Původní okno se překresluje, když je pokryto a odhaleno.

Screenshot that shows a MessageBox with an OK button

Některé vlákno musí mít na starosti okno okna se zprávou. WPF může vytvořit nové vlákno pouze pro okno okna se zprávou, ale toto vlákno by nemohlo nakreslit zakázané prvky v původním okně (pamatujte si předchozí diskuzi o vzájemném vyloučení). Místo toho WPF používá vnořený systém zpracování zpráv. Třída Dispatcher obsahuje speciální metodu nazvanou PushFrame, která ukládá aktuální bod spuštění aplikace a pak zahájí novou smyčku zpráv. Po dokončení vnořené smyčky zpráv se provádění obnoví po původním PushFrame volání.

V tomto případě PushFrame udržuje kontext programu při volání MessageBox.Showa spustí novou smyčku zprávy, která znovu nakreslí okno pozadí a zpracuje vstup do okna okna se zprávou. Když uživatel klikne na OK a vymaže automaticky otevírané okno, vnořená smyčka se ukončí a řízení obnoví po volání Show.

Zastaralé směrované události

Směrovaný systém událostí ve WPF upozorní celé stromy při vyvolání událostí.

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

Po stisknutí levého tlačítka myši přes tři tečky handler2 se spustí. Po handler2 dokončení se událost předá objektu Canvas , který ji použije handler1 ke zpracování. K tomu dochází pouze v případě, že handler2 objekt události explicitně neoznačí jako zpracovaný.

Je možné, že handler2 zpracování této události bude trvat hodně času. handler2 může použít PushFrame k zahájení vnořené smyčky zpráv, která se nevrací po dobu hodin. Pokud handler2 událost neoznačí jako zpracovánu při dokončení této smyčky zprávy, událost se předá stromu, i když je velmi stará.

Opětovné zamykání a zamykání

Mechanismus uzamčení modulu CLR (Common Language Runtime) se nechová přesně tak, jak by si mohl představit; jeden může očekávat, že vlákno přestane fungovat úplně při žádosti o zámek. Ve skutečnosti vlákno nadále přijímá a zpracovává zprávy s vysokou prioritou. To pomáhá zabránit zablokování a zajistit minimální odezvu rozhraní, ale přináší možnost drobných chyb. Velká většina času, kterou o tom nepotřebujete vědět, ale za výjimečných okolností (obvykle zahrnující zprávy okna Win32 nebo komponenty MODELU COM STA) to může být užitečné vědět.

Většina rozhraní není vytvořená s ohledem na bezpečnost vláken, protože vývojáři pracují za předpokladu, že uživatelské rozhraní nikdy nemá přístup více než jedno vlákno. V tomto případě může jedno vlákno provádět změny životního prostředí v neočekávaných časech, což způsobuje ty špatné účinky, které DispatcherObject by mechanismus vzájemného vyloučení měl vyřešit. Vezměte v úvahu následující pseudokód:

Diagram that shows threading reentrancy.

Většinou je to správná věc, ale ve WPF existují chvíle, kdy taková neočekávaná reentrence může skutečně způsobit problémy. Takže v určitých klíčových časech, WPF volání DisableProcessing, které změní instrukce zámku pro toto vlákno použít wpF reentrancy-free zámek místo obvyklý CLR zámek.

Proč si tedy tým CLR vybral toto chování? Muselo to udělat s objekty COM STA a finalizační vlákno. Při uvolňování paměti objektu se jeho Finalize metoda spustí ve vyhrazeném finalizačním vlákně, nikoli ve vlákně uživatelského rozhraní. A v něm spočívá problém, protože objekt COM STA, který byl vytvořen ve vlákně uživatelského rozhraní, lze likvidovat pouze ve vlákně uživatelského rozhraní. CLR dělá ekvivalent a BeginInvoke (v tomto případě pomocí Win32 SendMessage). Pokud je však vlákno uživatelského rozhraní zaneprázdněno, vlákno finalizátoru se zastaví a objekt COM STA nelze odstranit, což vytvoří vážné nevracení paměti. Takže tým CLR udělal těžké volání, aby zámky fungovaly tak, jak to dělají.

Úkolem WPF je vyhnout se neočekávané reentrancy bez opětovného vytvoření nevracení paměti, což je důvod, proč neblokujeme opakované zaentrování všude.

Viz také