Model wątkowania

Program Windows Presentation Foundation (WPF) został zaprojektowany w celu ratowania deweloperów przed trudnościami wątkowania. W rezultacie większość deweloperów WPF nie pisze interfejsu, który używa więcej niż jednego wątku. Ponieważ programy wielowątkowe są złożone i trudne do debugowania, należy unikać ich, gdy istnieją rozwiązania jednowątkowe.

Niezależnie od tego, jak dobrze zaprojektowano architekturę, żadna struktura interfejsu użytkownika nie jest w stanie zapewnić jednowątkowego rozwiązania dla każdego rodzaju problemu. WPF jest blisko, ale nadal istnieją sytuacje, w których wiele wątków poprawia czas odpowiedzi interfejsu użytkownika lub wydajność aplikacji. Po omówieniu niektórych materiałów w tle ten artykuł omawia niektóre z tych sytuacji, a następnie kończy się dyskusją na temat niektórych szczegółów niższego poziomu.

Uwaga

W tym temacie omówiono wątkowanie przy użyciu InvokeAsync metody wywołań asynchronicznych. Metoda InvokeAsync przyjmuje parametr Action lub Func<TResult> jako parametr i zwraca właściwość DispatcherOperation lub DispatcherOperation<TResult>, która ma Task właściwość . Możesz użyć słowa kluczowego await z elementem DispatcherOperation lub skojarzonym Taskelementem . Jeśli musisz zaczekać synchronicznie na Task element zwracany przez metodę DispatcherOperation lub DispatcherOperation<TResult>, wywołaj metodę DispatcherOperationWait rozszerzenia. Wywołanie Task.Wait spowoduje zakleszczenie. Aby uzyskać więcej informacji na temat używania elementu Task do wykonywania operacji asynchronicznych, zobacz Programowanie asynchroniczne oparte na zadaniach.

Aby utworzyć wywołanie synchroniczne, użyj Invoke metody , która ma również przeciążenia, które pobiera delegata , Actionlub Func<TResult> parametru.

Omówienie i dyspozytor

Zazwyczaj aplikacje WPF zaczynają się od dwóch wątków: jeden do obsługi renderowania, a drugi do zarządzania interfejsem użytkownika. Wątek renderowania skutecznie działa ukryty w tle, podczas gdy wątek interfejsu użytkownika odbiera dane wejściowe, obsługuje zdarzenia, maluje ekran i uruchamia kod aplikacji. Większość aplikacji używa jednego wątku interfejsu użytkownika, chociaż w niektórych sytuacjach najlepiej użyć kilku. Omówimy to za pomocą przykładu później.

Wątek interfejsu użytkownika kolejkuje elementy robocze wewnątrz obiektu o nazwie Dispatcher. Wybiera Dispatcher elementy robocze na zasadzie priorytetu i uruchamia każdy z nich do ukończenia. Każdy wątek interfejsu użytkownika musi zawierać co najmniej jeden Dispatcherwątek, a każdy z nich Dispatcher może wykonywać elementy robocze w dokładnie jednym wątku.

Sztuczka tworzenia dynamicznych, przyjaznych dla użytkownika aplikacji polega na maksymalizacji Dispatcher przepływności dzięki zachowaniu małych elementów roboczych. Dzięki temu elementy nigdy nie będą przestarzałe w Dispatcher kolejce oczekujące na przetwarzanie. Każde możliwe opóźnienie między danymi wejściowymi a odpowiedzią może sfrustrować użytkownika.

W jaki sposób aplikacje WPF mają obsługiwać duże operacje? Co zrobić, jeśli kod obejmuje duże obliczenie lub wymaga wykonywania zapytań dotyczących bazy danych na jakimś serwerze zdalnym? Zazwyczaj odpowiedzią jest obsługa dużej operacji w osobnym wątku, pozostawiając wątek interfejsu użytkownika wolny od elementów w Dispatcher kolejce. Po zakończeniu dużej operacji może zgłosić wynik z powrotem do wątku interfejsu użytkownika w celu wyświetlenia.

W przeszłości system Windows umożliwia dostęp do elementów interfejsu użytkownika tylko przez wątek, który je utworzył. Oznacza to, że wątek w tle odpowiedzialny za niektóre długotrwałe zadanie nie może zaktualizować pola tekstowego po zakończeniu. System Windows zapewnia integralność składników interfejsu użytkownika. Pole listy może wyglądać dziwnie, jeśli jego zawartość została zaktualizowana przez wątek tła podczas malowania.

WPF ma wbudowany mechanizm wzajemnego wykluczania, który wymusza tę koordynację. Większość klas w WPF pochodzi z klasy DispatcherObject. W budowie, przechowuje DispatcherObject odwołanie do Dispatcher połączonego z aktualnie uruchomionym wątkiem. W efekcie element DispatcherObject kojarzy się z wątkiem, który go tworzy. Podczas wykonywania programu obiekt może wywołać jego publiczną DispatcherObjectVerifyAccess metodę. VerifyAccessDispatcher analizuje skojarzony z bieżącym wątkiem i porównuje go z Dispatcher odwołaniem przechowywanym podczas budowy. Jeśli nie są one zgodne, VerifyAccess zgłasza wyjątek. VerifyAccess ma być wywoływana na początku każdej metody należącej DispatcherObjectdo klasy .

Jeśli tylko jeden wątek może modyfikować interfejs użytkownika, jak wątki w tle współdziałają z użytkownikiem? Wątek w tle może poprosić wątek interfejsu użytkownika o wykonanie operacji w jego imieniu. Robi to przez zarejestrowanie elementu roboczego w Dispatcher wątku interfejsu użytkownika. Klasa Dispatcher udostępnia metody rejestrowania elementów roboczych: Dispatcher.InvokeAsync, Dispatcher.BeginInvokei Dispatcher.Invoke. Te metody umożliwiają zaplanowanie delegata do wykonania. Invoke jest wywołaniem synchronicznym — oznacza to, że nie zwraca się, dopóki wątek interfejsu użytkownika rzeczywiście zakończy wykonywanie delegata. InvokeAsync i BeginInvoke są asynchroniczne i zwracane natychmiast.

Kolejność Dispatcher elementów w kolejce według priorytetu. Istnieje dziesięć poziomów, które można określić podczas dodawania Dispatcher elementu do kolejki. Te priorytety są utrzymywane w DispatcherPriority wyliczeniem.

Aplikacja jednowątkowa z długotrwałym obliczeniem

Większość graficznych interfejsów użytkownika (GUI) spędza dużą część czasu bezczynności podczas oczekiwania na zdarzenia generowane w odpowiedzi na interakcje użytkowników. Dzięki starannemu programowaniu ten czas bezczynności można używać konstruktywnie, bez wpływu na czas odpowiedzi interfejsu użytkownika. Model wątkowy WPF nie zezwala na przerywanie operacji wykonywanej w wątku interfejsu użytkownika. Oznacza to, że należy powrócić do okresowo, Dispatcher aby przetworzyć oczekujące zdarzenia wejściowe, zanim zostaną nieaktualne.

Przykładowa aplikacja przedstawiająca koncepcje tej sekcji można pobrać z usługi GitHub dla języka C# lub Visual Basic.

Rozważmy następujący przykład:

Screenshot that shows threading of prime numbers.

Ta prosta aplikacja liczy się w górę od trzech, wyszukując liczby pierwsze. Gdy użytkownik kliknie przycisk Start , rozpocznie się wyszukiwanie. Gdy program znajdzie element prime, aktualizuje interfejs użytkownika za pomocą odnajdywania. W dowolnym momencie użytkownik może zatrzymać wyszukiwanie.

Chociaż wystarczająco proste, wyszukiwanie numerów pierwszych może trwać na zawsze, co stanowi pewne trudności. Jeśli obsłużyliśmy całe wyszukiwanie wewnątrz procedury obsługi zdarzeń kliknięcia przycisku, nigdy nie dalibyśmy wątkowi interfejsu użytkownika szansy na obsługę innych zdarzeń. Interfejs użytkownika nie może odpowiadać na komunikaty wejściowe lub przetwarzać. Nigdy nie przemaluje i nigdy nie odpowiada na kliknięcia przycisków.

Możemy przeprowadzić wyszukiwanie numerów pierwszych w osobnym wątku, ale wtedy musimy radzić sobie z problemami z synchronizacją. W przypadku podejścia jednowątkowego możemy bezpośrednio zaktualizować etykietę, która zawiera listę największych znalezionych pierwszych.

Jeśli podzielimy zadanie obliczania na fragmenty, którymi można zarządzać, możemy okresowo powrócić do zdarzeń i przetwarzania Dispatcher . Możemy dać WPF możliwość przemalowania i przetwarzania danych wejściowych.

Najlepszym sposobem podzielenia czasu przetwarzania między obliczeniami a obsługą zdarzeń jest zarządzanie obliczeniami z elementu Dispatcher. Za pomocą InvokeAsync metody możemy zaplanować kontrole liczb pierwszych w tej samej kolejce, z których pochodzą zdarzenia interfejsu użytkownika. W naszym przykładzie planujemy tylko jedno sprawdzenie liczby prime naraz. Po zakończeniu sprawdzania numeru podstawowego zaplanujemy natychmiastowe sprawdzenie następnego. Ta kontrola jest kontynuowana dopiero po obsłużeniu oczekujących zdarzeń interfejsu użytkownika.

Screenshot that shows the dispatcher queue.

Program Microsoft Word wykonuje sprawdzanie pisowni przy użyciu tego mechanizmu. Sprawdzanie pisowni odbywa się w tle przy użyciu czasu bezczynności wątku interfejsu użytkownika. Przyjrzyjmy się kodowi.

W poniższym przykładzie pokazano kod XAML, który tworzy interfejs użytkownika.

Ważne

Kod XAML przedstawiony w tym artykule pochodzi z projektu języka C#. Język XAML języka Visual Basic jest nieco inny podczas deklarowania klasy zapasowej dla języka 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>

W poniższym przykładzie pokazano kod za pomocą kodu.

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

Oprócz aktualizowania tekstu w Buttonobiekcie program StartStopButton_Click obsługi jest odpowiedzialny za zaplanowanie pierwszego sprawdzenia numeru podstawowego przez dodanie delegata Dispatcher do kolejki. Gdy ten program obsługi zdarzeń zakończy pracę, Dispatcher wybierze delegata do wykonania.

Jak wspomniano wcześniej, InvokeAsync jest elementem Dispatcher członkowskim używanym do planowania delegata do wykonania. W tym przypadku wybieramy SystemIdle priorytet. Ten Dispatcher delegat zostanie wykonany tylko wtedy, gdy nie ma ważnych zdarzeń do przetworzenia. Czas odpowiedzi interfejsu użytkownika jest ważniejszy niż sprawdzanie liczby. Przekazujemy również nowego delegata reprezentującego procedurę sprawdzania liczby.

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

Ta metoda sprawdza, czy następna liczba nieparzysta jest liczbą pierwszą. Jeśli jest ona podstawowa, metoda bezpośrednio aktualizuje element , bigPrimeTextBlock aby odzwierciedlić jego odnajdywanie. Możemy to zrobić, ponieważ obliczenie występuje w tym samym wątku, który został użyty do utworzenia kontrolki. Gdyby do obliczenia użyto oddzielnego wątku, musielibyśmy użyć bardziej skomplikowanego mechanizmu synchronizacji i wykonać aktualizację w wątku interfejsu użytkownika. W następnej kolejności pokażemy tę sytuację.

Wiele okien, wiele wątków

Niektóre aplikacje WPF wymagają wielu okien najwyższego poziomu. Jest to całkowicie akceptowalne dla jednej kombinacji Thread/Dispatcher do zarządzania wieloma oknami, ale czasami kilka wątków wykonuje lepszą pracę. Jest to szczególnie prawdziwe, jeśli istnieje szansa, że jeden z okien zmonopolizuje wątek.

Eksplorator Windows działa w ten sposób. Każde nowe okno Eksploratora należy do oryginalnego procesu, ale jest ono tworzone pod kontrolą niezależnego wątku. Gdy Eksplorator staje się nieodpowiadczy, na przykład podczas wyszukiwania zasobów sieciowych, inne okna Eksploratora nadal reagują i mogą być użyteczne.

Możemy zademonstrować tę koncepcję przy użyciu poniższego przykładu.

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.

Trzy pierwsze okna tego obrazu mają ten sam identyfikator wątku: 1. Dwa pozostałe okna mają różne identyfikatory wątków: Dziewięć i 4. W prawym górnym rogu każdego okna znajduje się kolor magenty ! !️ glyph.

Ten przykład zawiera okno z obracającym ‼️ się glifem, przyciskiem Wstrzymywanie i dwoma innymi przyciskami, które tworzą nowe okno w bieżącym wątku lub w nowym wątku. Glif ‼️ jest stale obracany do momentu naciśnięcia przycisku Wstrzymaj , który wstrzymuje wątek przez pięć sekund. W dolnej części okna zostanie wyświetlony identyfikator wątku.

Po naciśnięciu przycisku Wstrzymaj wszystkie okna w tym samym wątku stają się nieodpowiadalne. Każde okno w innym wątku nadal działa normalnie.

W poniższym przykładzie do okna jest kod XAML:

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

W poniższym przykładzie pokazano kod za pomocą kodu.

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

Poniżej przedstawiono niektóre szczegóły, które należy zauważyć:

  • Zadanie Task.Delay(TimeSpan) jest używane do spowodowania wstrzymania bieżącego wątku przez pięć sekund po naciśnięciu przycisku Wstrzymaj.

    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
    
  • Program SameThreadWindow_Click obsługi zdarzeń immediently pokazuje nowe okno w bieżącym wątku. Procedura NewThreadWindow_Click obsługi zdarzeń tworzy nowy wątek, który rozpoczyna wykonywanie ThreadStartingPoint metody, co z kolei pokazuje nowe okno zgodnie z opisem w następnym punkcie.

    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 jest punktem wyjścia dla nowego wątku. Nowe okno jest tworzone pod kontrolą tego wątku. WPF automatycznie tworzy nowy System.Windows.Threading.Dispatcher wątek do zarządzania nowym wątkiem. Wszystko, co musimy zrobić, aby okno działało, to uruchomić 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
    

Przykładowa aplikacja przedstawiająca koncepcje tej sekcji można pobrać z usługi GitHub dla języka C# lub Visual Basic.

Obsługa operacji blokowania za pomocą polecenia Task.Run

Obsługa operacji blokowania w aplikacji graficznej może być trudna. Nie chcemy wywoływać metod blokowania z programów obsługi zdarzeń, ponieważ aplikacja wydaje się blokować. W poprzednim przykładzie utworzono nowe okna we własnym wątku, dzięki czemu każde okno będzie działać niezależnie od siebie. Chociaż możemy utworzyć nowy wątek za pomocą System.Windows.Threading.Dispatcherpolecenia , po zakończeniu pracy trudno jest zsynchronizować nowy wątek z głównym wątkiem interfejsu użytkownika. Ponieważ nowy wątek nie może bezpośrednio zmodyfikować interfejsu użytkownika, musimy użyć Dispatcher.InvokeAsyncmetody , Dispatcher.BeginInvokelub Dispatcher.Invoke, aby wstawić delegatów do Dispatcher wątku interfejsu użytkownika. W końcu te delegaty są wykonywane z uprawnieniami do modyfikowania elementów interfejsu użytkownika.

Istnieje łatwiejszy sposób uruchamiania kodu w nowym wątku podczas synchronizowania wyników — wzorca asynchronicznego opartego na zadaniach (TAP). Jest ona oparta na typach Task i Task<TResult> w System.Threading.Tasks przestrzeni nazw, które są używane do reprezentowania operacji asynchronicznych. We wzorcu TAP do tworzenia wystąpienia i wykonywania operacji asynchronicznej jest używana jedna metoda. Ten wzorzec ma kilka korzyści:

  • Obiekt wywołujący Task obiektu może uruchomić kod asynchronicznie lub synchronicznie.
  • Postęp można zgłaszać z poziomu elementu Task.
  • Kod wywołujący może zawiesić wykonywanie i poczekać na wynik operacji.

Przykład task.Run

W tym przykładzie naśladujemy zdalne wywołanie procedury, które pobiera prognozę pogody. Po kliknięciu przycisku interfejs użytkownika zostanie zaktualizowany, aby wskazać, że pobieranie danych jest w toku, podczas gdy zadanie jest uruchamiane, aby naśladować pobieranie prognozy pogody. Po uruchomieniu zadania kod obsługi zdarzeń przycisku jest zawieszony do momentu zakończenia zadania. Po zakończeniu zadania kod programu obsługi zdarzeń będzie nadal uruchamiany. Kod jest zawieszony i nie blokuje pozostałej części wątku interfejsu użytkownika. Kontekst synchronizacji WPF obsługuje zawieszanie kodu, co umożliwia kontynuowanie działania WPF.

A diagram that demonstrates the workflow of the example app.

Diagram przedstawiający przepływ pracy przykładowej aplikacji. Aplikacja ma jeden przycisk z tekstem "Fetch Forecast". Po naciśnięciu przycisku znajduje się strzałka wskazująca kolejną fazę aplikacji, która jest obrazem zegara umieszczonym w środku aplikacji wskazującym, że aplikacja jest zajęta pobieraniem danych. Po pewnym czasie aplikacja zwraca obraz słońca lub chmur deszczu, w zależności od wyniku danych.

Przykładowa aplikacja przedstawiająca koncepcje tej sekcji można pobrać z usługi GitHub dla języka C# lub Visual Basic. Kod XAML dla tego przykładu jest dość duży i nie jest podany w tym artykule. Użyj poprzednich linków usługi GitHub, aby przeglądać kod XAML. Język XAML używa jednego przycisku do pobierania pogody.

Rozważ użycie kodu do kodu 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

Poniżej przedstawiono niektóre szczegóły, które należy zanotować.

  • Procedura obsługi zdarzeń przycisku

    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
    

    Zwróć uwagę, że program obsługi zdarzeń został zadeklarowany za pomocą async (lub Async w Visual Basic). Metoda "async" umożliwia zawieszenie kodu, gdy wywoływana jest oczekiwana metoda, taka jak FetchWeatherFromServerAsync, . Jest to wyznaczone przez await słowo kluczowe (lub Await z językiem Visual Basic). FetchWeatherFromServerAsync Do momentu zakończenia kod obsługi przycisku jest zawieszony, a kontrolka jest zwracana do obiektu wywołującego. Jest to podobne do metody synchronicznej, z tą różnicą, że metoda synchroniczna czeka na każdą operację w metodzie, po której kontrolka jest zwracana do obiektu wywołującego.

    Metody awaited korzystają z kontekstu wątkowego bieżącej metody, która z procedurą obsługi przycisków jest wątkiem interfejsu użytkownika. Oznacza to, że wywołanie await FetchWeatherFromServerAsync(); (lub Await FetchWeatherFromServerAsync() w języku Visual Basic) powoduje uruchomienie kodu w FetchWeatherFromServerAsync wątku interfejsu użytkownika, ale nie jest wykonywane na dyspozytocie, ma czas na jego uruchomienie, podobnie jak w przypadku działania aplikacji jednowątkowej z długotrwałym przykładem obliczeń . Należy jednak zauważyć, że await Task.Run jest używany. Spowoduje to utworzenie nowego wątku w puli wątków dla wyznaczonego zadania zamiast bieżącego wątku. Więc FetchWeatherFromServerAsync działa na własnym wątku.

  • Pobieranie pogody

    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
    

    Aby zachować prostotę, w tym przykładzie nie mamy żadnego kodu sieciowego. Zamiast tego symulujemy opóźnienie dostępu do sieci przez umieszczenie nowego wątku w stanie uśpienia przez cztery sekundy. W tym czasie oryginalny wątek interfejsu użytkownika jest nadal uruchomiony i odpowiada na zdarzenia interfejsu użytkownika, gdy program obsługi zdarzeń przycisku jest wstrzymany do momentu zakończenia nowego wątku. Aby to zademonstrować, pozostawiliśmy uruchomioną animację i możesz zmienić rozmiar okna. Jeśli wątek interfejsu użytkownika został wstrzymany lub opóźniony, animacja nie zostanie wyświetlona i nie można wchodzić w interakcję z oknem.

    Po zakończeniu Task.Delay i losowo wybraliśmy naszą prognozę pogody, stan pogody jest zwracany do obiektu wywołującego.

  • Aktualizowanie interfejsu użytkownika

    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
    

    Gdy zadanie zakończy się, a wątek interfejsu użytkownika ma czas, obiekt wywołujący Task.Runprogramu obsługi zdarzeń przycisku zostanie wznowiony. Pozostała część metody zatrzymuje animację zegara i wybiera obraz do opisania pogody. Wyświetla ten obraz i włącza przycisk "pobierz prognozę".

Przykładowa aplikacja przedstawiająca koncepcje tej sekcji można pobrać z usługi GitHub dla języka C# lub Visual Basic.

Szczegóły techniczne i punkty potknięcia

W poniższych sekcjach opisano niektóre szczegóły i potknięcia, które można napotkać z wielowątkowymi elementami.

Pompowanie zagnieżdżone

Czasami nie jest możliwe całkowite zablokowanie wątku interfejsu użytkownika. Rozważmy metodę ShowMessageBox klasy . Show nie zwraca się, dopóki użytkownik nie kliknie przycisku OK. Jednak tworzy okno, które musi mieć pętlę komunikatów, aby być interakcyjne. Podczas oczekiwania na kliknięcie przycisku OK przez użytkownika oryginalne okno aplikacji nie odpowiada na dane wejściowe użytkownika. Nadal jednak przetwarza komunikaty malowania. Oryginalne okno ponownie rysuje się po pokryciu i ujawnieniu.

Screenshot that shows a MessageBox with an OK button

Jakiś wątek musi być odpowiedzialny za okno okna okna okna komunikatu. WPF może utworzyć nowy wątek tylko dla okna okna komunikatu, ale ten wątek nie będzie mógł malować wyłączonych elementów w oryginalnym oknie (pamiętaj wcześniejsze omówienie wzajemnego wykluczenia). Zamiast tego WPF używa zagnieżdżonego systemu przetwarzania komunikatów. Klasa Dispatcher zawiera specjalną metodę o nazwie PushFrame, która przechowuje bieżący punkt wykonywania aplikacji, a następnie rozpoczyna nową pętlę komunikatów. Po zakończeniu zagnieżdżonej pętli komunikatów wykonywanie zostanie wznowione po oryginalnym PushFrame wywołaniu.

W takim przypadku kontekst programu jest utrzymywany w wywołaniu MessageBox.Showmetody i uruchamia nową pętlę komunikatów w PushFrame celu przemalowania okna tła i obsługi danych wejściowych w oknie pola komunikatu. Gdy użytkownik kliknie przycisk OK i wyczyści okno podręczne, zagnieżdżona pętla zostanie zakończona i kontrolka zostanie wznowiona po wywołaniu metody Show.

Nieaktywne zdarzenia kierowane

System zdarzeń kierowanych w WPF powiadamia całe drzewa o zgłaszaniu zdarzeń.

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

Po naciśnięciu lewego przycisku myszy na wielokropek handler2 jest wykonywany. Po handler2 zakończeniu zdarzenie jest przekazywane do Canvas obiektu, który używa handler1 go do jego przetworzenia. Dzieje się tak tylko wtedy, gdy handler2 obiekt zdarzenia nie jest jawnie oznaczany jako obsługiwany.

Możliwe, że handler2 przetwarzanie tego zdarzenia zajmie dużo czasu. handler2 może służyć PushFrame do rozpoczęcia zagnieżdżonej pętli komunikatów, która nie zwraca się przez wiele godzin. Jeśli handler2 zdarzenie nie jest oznaczone jako obsługiwane po zakończeniu tej pętli komunikatów, zdarzenie jest przekazywane w drzewie, mimo że jest bardzo stary.

Ponowne wdrażanie i blokowanie

Mechanizm blokowania środowiska uruchomieniowego języka wspólnego (CLR) nie działa dokładnie tak, jak można sobie wyobrazić; można oczekiwać, że wątek zakończy działanie całkowicie podczas żądania blokady. W rzeczywistości wątek nadal odbiera i przetwarza komunikaty o wysokim priorytcie. Pomaga to zapobiegać zakleszczeniom i sprawiać, że interfejsy są minimalnej reakcji, ale wprowadza możliwość subtelnych usterek. Zdecydowana większość czasu nie musisz nic o tym wiedzieć, ale w rzadkich okolicznościach (zwykle z udziałem komunikatów okien Win32 lub składników COM STA) może to być warte poznania.

Większość interfejsów nie jest zbudowana z myślą o bezpieczeństwie wątków, ponieważ deweloperzy pracują zgodnie z założeniem, że interfejs użytkownika nigdy nie jest uzyskiwany przez więcej niż jeden wątek. W takim przypadku pojedynczy wątek może wprowadzać zmiany środowiskowe w nieoczekiwanych momentach, powodując te złe skutki, które DispatcherObject ma rozwiązać mechanizm wzajemnego wykluczania. Rozważmy następujący pseudokod:

Diagram that shows threading reentrancy.

Przez większość czasu jest to właściwe, ale są czasy w WPF, gdzie taka nieoczekiwana reentrancy może naprawdę powodować problemy. Tak więc, w niektórych kluczowych momentach, WPF wywołuje DisableProcessing, który zmienia instrukcję blokady dla tego wątku, aby użyć blokady ponownej enentrancy-free WPF, zamiast zwykłej blokady CLR.

Dlaczego więc zespół CLR wybrał to zachowanie? Musiał to zrobić z obiektami COM STA i wątkiem finalizacji. Gdy obiekt jest odśmiecany, jego Finalize metoda jest uruchamiana w dedykowanym wątku finalizatora, a nie wątku interfejsu użytkownika. I tam leży problem, ponieważ obiekt STA COM, który został utworzony w wątku interfejsu użytkownika, można usunąć tylko w wątku interfejsu użytkownika. ClR wykonuje odpowiednik elementu BeginInvoke (w tym przypadku przy użyciu win32 SendMessage). Jeśli jednak wątek interfejsu użytkownika jest zajęty, wątek finalizatora zostanie zatrzymany, a obiekt STA COM nie może zostać usunięty, co powoduje poważny wyciek pamięci. Więc zespół CLR się trudne wezwanie, aby blokady działają tak, jak robią.

Zadaniem WPF jest uniknięcie nieoczekiwanej ponownej reentrancy bez ponownego wprowadzenia przecieku pamięci, dlatego nie blokujemy ponownego wprowadzania wszędzie.

Zobacz też