Megosztás a következőn keresztül:


Szálmodell

A Windows Presentation Foundation (WPF) célja, hogy mentse a fejlesztőket a szálkezelés nehézségeitől. Ennek eredményeképpen a LEGTÖBB WPF-fejlesztő nem egynél több szálat használó felületet ír. Mivel a többszálas programok összetettek és nehezen hibakereshetők, ha léteznek egyszálas megoldások, kerülni kell őket.

Függetlenül attól, hogy mennyire jól van megépítve, egyetlen felhasználói felületi keretrendszer sem képes egyetlenszálas megoldást nyújtani minden problémára. A WPF közel jár, de vannak olyan helyzetek, amikor több szál javítja a felhasználói felület (UI) válaszképességét vagy az alkalmazás teljesítményét. Néhány háttéranyag megvitatása után ez a cikk áttekint néhány ilyen helyzetet, majd néhány alacsonyabb szintű részlet megvitatásával fejeződik be.

Megjegyzés:

Ez a témakör az aszinkron hívások InvokeAsync metódusával történő szálkezelést ismerteti. A InvokeAsync metódus paraméterként egy Action vagy Func<TResult> vesz fel, és egy DispatcherOperation tulajdonságú DispatcherOperation<TResult> vagy Taskad vissza. A await kulcsszót a DispatcherOperation vagy a hozzá kapcsolódó Taskkulcsszóval használhatja. Ha szinkronban kell várnia a Task vagy a DispatcherOperationáltal visszaadott DispatcherOperation<TResult>-ra, hívja meg a DispatcherOperationWait kiterjesztési metódust. A Task.Wait hívása holtpontot eredményez. Az aszinkron műveletek Task használatával kapcsolatos további információkért tekintse meg a(z) feladatalapú aszinkron programozáscímű részt.

Szinkron hívás indításához használja a Invoke metódust, amely túlterhelésekkel is rendelkezik, amelyek egy delegált, Actionvagy Func<TResult> paramétert vesznek igénybe.

Áttekintés és a diszpécser

A WPF-alkalmazások általában két szálból indulnak ki: az egyik a renderelés kezelésére, a másik a felhasználói felület kezelésére. A renderelő szál gyakorlatilag rejtetten fut a háttérben, miközben a felhasználói felület szál bemenetet kap, kezeli az eseményeket, festi a képernyőt, és futtatja az alkalmazáskódot. A legtöbb alkalmazás egyetlen felhasználói felületi szálat használ, bár bizonyos esetekben a legjobb, ha több szálat használ. Ezt később egy példával fogjuk megvitatni.

A felhasználói felületi szál a munkaelemeket egy Dispatchernevű objektumban lévő üzenetsorokban helyezi el. A Dispatcher prioritás alapján választja ki a munkaelemeket, és mindegyiket a befejezésig futtatja. Minden felhasználói felületi szálnak rendelkeznie kell legalább egy Dispatcher-val, és minden Dispatcher pontosan egy szálon hajthat végre munkaelemeket.

A rugalmas, felhasználóbarát alkalmazások létrehozásának trükkje az, hogy maximalizálja a Dispatcher átviteli sebességet azáltal, hogy a munkaelemek kicsik maradnak. Így az elemek soha nem lesznek elavultak a feldolgozásra váró Dispatcher üzenetsorban. A bemenet és a válasz közötti érzékelhető késés frusztrálhatja a felhasználót.

Hogyan kezelik a WPF-alkalmazások a nagy műveleteket? Mi a teendő, ha a kód nagy számítást igényel, vagy egy adatbázist kell lekérdeznie egy távoli kiszolgálón? Általában a válasz az, hogy egy külön szálon kezeljük a nagy műveletet, így a felhasználói felület szála szabadon foglalkozhat a Dispatcher sor elemeivel. Ha a nagy művelet befejeződött, az eredményt vissza tudja jelenteni a felhasználói felületi szálnak megjelenítés céljából.

A Windows előzményként lehetővé teszi, hogy a felhasználói felület elemeit csak az őket létrehozó szál érje el. Ez azt jelenti, hogy egy hosszú ideig futó feladatért felelős háttérszál nem tudja frissíteni a szövegdobozt, ha befejeződött. A Windows ezzel biztosítja a felhasználói felület összetevőinek integritását. A listadobozok furcsának tűnhetnek, ha a tartalmát egy háttérszál frissítette a festés során.

A WPF beépített kölcsönös kizárási mechanizmussal rendelkezik, amely kényszeríti ezt a koordinációt. A WPF legtöbb osztálya a DispatcherObjectszármazik. Az építés során egy DispatcherObject a jelenleg futó szálhoz társított Dispatcher hivatkozását tárolja. A DispatcherObject valójában az azt létrehozó szálhoz társítja. A program végrehajtása során egy DispatcherObject meghívhatja a nyilvános VerifyAccess metódust. VerifyAccess megvizsgálja az aktuális szálhoz társított Dispatcher, és összehasonlítja az építés során tárolt Dispatcher hivatkozással. Ha nem egyeznek, VerifyAccess kivételt jelez. VerifyAccess-t szándékoznak minden DispatcherObjectmetódus elején meghívni.

Ha csak egy szál tudja módosítani a felhasználói felületet, hogyan tudnak a háttérszálak kapcsolatba lépni a felhasználóval? A háttérszál megkérheti a felhasználói felületi szálat, hogy végezzen el egy műveletet a nevében. Úgy teszi ezt, hogy regisztrál egy feladatot a Dispatcher szálán a felhasználói felületnek. A Dispatcher osztály a következő módszereket kínálja a munkaelemek regisztrálásához: Dispatcher.InvokeAsync, Dispatcher.BeginInvokeés Dispatcher.Invoke. Ezek a metódusok egy meghatalmazottat ütemeznek a végrehajtáshoz. Invoke szinkron hívás, vagyis nem tér vissza addig, amíg a felhasználói felületi szál nem fejeződik be a meghatalmazott végrehajtásával. InvokeAsync és BeginInvoke aszinkronok, és azonnal visszatérnek.

A Dispatcher prioritás szerint rendeli meg az üzenetsor elemeit. Tíz szint adható meg, amikor elemet ad hozzá a Dispatcher üzenetsorhoz. Ezeket a prioritásokat a DispatcherPriority enumerálásban tartják fenn.

Egyszálas alkalmazás hosszú ideig futó számítással

A legtöbb grafikus felhasználói felület (GUI) az idő nagy részét tétlenül tölti, miközben a felhasználói interakciókra válaszul létrehozott eseményekre vár. Gondos programozással ez az üresjárati idő konstruktívan használható, anélkül, hogy befolyásolná a felhasználói felület válaszkészségét. A WPF szálmodell nem teszi lehetővé, hogy a bemenet megszakítsa a felhasználói felületen zajló műveletet. Ez azt jelenti, hogy rendszeresen vissza kell térnie a Dispatcher-hoz, hogy feldolgozhassa a függőben lévő bemeneti eseményeket, mielőtt elavulnának.

A szakasz alapelveit bemutató mintaalkalmazás letölthető a GitHubról C# vagy Visual Basic.

Vegye figyelembe a következő példát:

Prímszámok menetét ábrázoló képernyőkép.

Ez az egyszerű alkalmazás háromtól felfelé számlál, és prímszámokat keres. Amikor a felhasználó a Start gombra kattint, megkezdődik a keresés. Amikor a program megtalálja a prímszámot, frissíti a felhasználói felületet a felfedezéssel. A felhasználó bármikor leállíthatja a keresést.

Bár elég egyszerű, a prímszám keresés örökké tart, ami nehézségeket okoz. Ha a teljes keresést a gomb kattintási eseménykezelőjében kezelnénk, soha nem adnánk esélyt a felhasználói felület szálának, hogy más eseményeket is kezeljen. A felhasználói felület nem tud válaszolni a bemeneti vagy feldolgozási üzenetekre. Soha nem festené újra, és soha nem válaszolna a gombkattintásokra.

Az elsődleges számkeresést külön szálon is elvégezhetjük, de utána szinkronizálási problémákat kell kezelnünk. Egyszálas megközelítéssel közvetlenül frissíthetjük a legnagyobb prímeket tartalmazó címkét.

Ha a számítási feladatot kezelhető adattömbökre bontjuk, rendszeresen visszatérhetünk a Dispatcher és feldolgozhatjuk az eseményeket. Lehetőséget adhatunk a WPF-nek a bemenet újrafestésére és feldolgozására.

A feldolgozási idő számítás és eseménykezelés közötti felosztásának legjobb módja a számítások Dispatcher-ból történő irányítása. A InvokeAsync metódus használatával az elsődleges számellenőrzéseket ugyanabban az üzenetsorban ütemezhetjük, amelyből a felhasználói felület eseményei származnak. Példánkban egyszerre csak egyetlen prímszám-ellenőrzést ütemezünk. Az elsődleges szám ellenőrzése után azonnal ütemezzük a következő ellenőrzést. Ez az ellenőrzés csak a függőben lévő felhasználói felületi események kezelése után folytatódik.

Képernyőkép, amely a diszpécsersorról mutat egy képet.

A Microsoft Word ezzel a mechanizmussal hajtja végre a helyesírás-ellenőrzést. A helyesírás-ellenőrzés a háttérben történik a felhasználói felületi szál tétlenségi idejének használatával. Vizsgáljuk meg a kódot.

Az alábbi példa a felhasználói felületet létrehozó XAML-t mutatja be.

Fontos

A cikkben látható XAML egy C#-projektből származik. A Visual Basic XAML kissé eltér az XAML háttérosztályának deklarálásakor.

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

Az alábbi példa a kód mögötti kódot mutatja be.

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

A Buttonszövegének frissítése mellett a StartStopButton_Click kezelője felelős az első prímszám-ellenőrzés ütemezéséért azáltal, hogy hozzáad egy delegáltat a Dispatcher sorhoz. Az eseménykezelő munkájának befejezése után a Dispatcher kiválasztja a meghatalmazottat a végrehajtáshoz.

Ahogy korábban említettük, InvokeAsync az a Dispatcher tag, amellyel egy meghatalmazottat ütemezhet végrehajtásra. Ebben az esetben a SystemIdle prioritást választjuk. A Dispatcher csak akkor hajtja végre ezt a delegáltat, ha nincsenek feldolgozandó fontos események. A felhasználói felület válaszkészsége fontosabb, mint a számellenőrzés. A számellenőrzési rutint képviselő új meghatalmazottat is átadunk.

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

Ez a metódus ellenőrzi, hogy a következő páratlan szám prímérték-e. Ha elsődleges, a metódus közvetlenül frissíti a bigPrimeTextBlock, hogy tükrözze a felderítést. Ez azért lehetséges, mert a számítás ugyanabban a szálban történik, amelyet a vezérlő létrehozásához használtak. Ha külön szálat használnánk a számításhoz, akkor egy bonyolultabb szinkronizálási mechanizmust kellene használnunk, és végre kellene hajtanunk a frissítést a felhasználói felületen. Ezt a helyzetet a következő lépésben mutatjuk be.

Több ablak, több szál

Egyes WPF-alkalmazásokhoz több felső szintű ablak szükséges. Egy szál/diszpécser kombináció tökéletesen elfogadható több ablak kezeléséhez, de néha több szál is jobb munkát végez. Ez különösen igaz, ha van rá esély, hogy az egyik ablak monopolizálja a szálat.

A Windows Explorer így működik. Minden új Explorer-ablak az eredeti folyamathoz tartozik, de egy független szál felügyelete alatt jön létre. Amikor az Explorer nem válaszol, például hálózati erőforrásokat keres, a többi Explorer-ablak továbbra is rugalmas és használható lesz.

Ezt a fogalmat az alábbi példával szemléltethetjük.

Egy négyszer duplikált WPF-ablak képernyőképe. Három ablak azt jelzi, hogy ugyanazt a szálat használják, míg a másik kettő különböző szálakon van.

A kép első három ablaka ugyanazzal a szálazonosítóval rendelkezik: 1. A két másik ablak különböző szálazonosítókkal rendelkezik: Kilenc és 4. Minden ablak jobb felső sarkában van egy magenta színű, forgó ‼️ glif.

Ez a példa egy forgó ‼️ karakterjelet, egy Szüneteltetés gombot és két másik gombot tartalmaz, amelyek új ablakot hoznak létre az aktuális szál alatt vagy egy új szálban. A ‼️ glyph folyamatosan forog, amíg a Szüneteltetés gombot nem lenyomja, amely öt másodpercig szünetelteti a szálat. Az ablak alján megjelenik a szálazonosító.

Ha a Szüneteltetés gombot lenyomja, az ugyanazon a szálon lévő összes ablak nem válaszol. Egy másik szál alatti ablak továbbra is normálisan működik.

Az alábbi példa az ablak XAML-je:

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

Az alábbi példa a kód mögötti kódot mutatja be.

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

Az alábbiakban néhányat fel kell jegyezni:

  • A Task.Delay(TimeSpan) feladat azt eredményezi, hogy az aktuális szál öt másodpercig szünetel, amikor a Szüneteltetés gombot lenyomja.

    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
    
  • Az SameThreadWindow_Click eseménykezelője egy új ablakot jelenít meg az aktuális szál alatt. Az NewThreadWindow_Click eseménykezelő létrehoz egy új szálat, amely elkezdi végrehajtani a ThreadStartingPoint metódust, amely viszont egy új ablakot jelenít meg a következő felsoroláspontban leírtak szerint.

    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
    
  • A ThreadStartingPoint metódus az új szál kiindulópontja. Az új ablak ennek a szálnak a felügyelete alatt jön létre. A WPF automatikusan létrehoz egy új System.Windows.Threading.Dispatcher az új szál kezeléséhez. Ahhoz, hogy az ablak működjön, mindössze annyit kell tennünk, hogy elindítjuk a System.Windows.Threading.Dispatcher-t.

    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
    

A szakasz alapelveit bemutató mintaalkalmazás letölthető a GitHubról C# vagy Visual Basic.

Blokkoló művelet kezelése a Task.Run használatával

A grafikus alkalmazások blokkolási műveleteinek kezelése nehéz lehet. Nem szeretnénk blokkoló metódusokat meghívni az eseménykezelőktől, mert úgy tűnik, hogy az alkalmazás lefagy. Az előző példa új ablakokat hozott létre a saját szálában, így az egyes ablakok egymástól függetlenül futhatnak. Bár létrehozhatunk egy új szálat System.Windows.Threading.Dispatcher, a munka befejezése után nehéz lesz szinkronizálni az új szálat a fő felhasználói felület szálával. Mivel az új szál nem tudja közvetlenül módosítani a felhasználói felületet, Dispatcher.InvokeAsync, Dispatcher.BeginInvokevagy Dispatcher.Invokekell használnunk, hogy delegáltakat szúrjunk be a felhasználói felületi szál Dispatcher. Végül ezek a meghatalmazottak a felhasználói felület elemeinek módosítására való engedéllyel lesznek végrehajtva.

Az eredmények szinkronizálása mellett van egy egyszerűbb módja annak, hogy egy új szálon futtassa a kódot: a Feladatalapú aszinkron minta (TAP). A Task névtér Task<TResult> és System.Threading.Tasks típusán alapul, amelyek az aszinkron műveletek megjelenítésére szolgálnak. A TAP egyetlen módszert használ az aszinkron művelet indításának és befejezésének ábrázolására. Ennek a mintának néhány előnye van:

  • Egy Task hívója dönthet úgy, hogy a kódot aszinkron módon vagy szinkron módon futtatja.
  • Az előrehaladásról a Task-nál lehet jelenteni.
  • A hívó kód felfüggesztheti a végrehajtást, és megvárhatja a művelet eredményét.

Task.Run példa

Ebben a példában egy távoli eljáráshívást utánozunk, amely egy időjárás-előrejelzést kér le. Amikor a gombra kattint, a felhasználói felület frissül, hogy jelezze, hogy az adatbeolvasás folyamatban van, miközben egy feladat elkezdi utánozni az időjárás-előrejelzést. A tevékenység indításakor a gombesemény-kezelő kódja fel van függesztve, amíg a tevékenység befejeződik. A feladat befejezése után az eseménykezelő kódja továbbra is fut. A kód fel van függesztve, és nem blokkolja a felhasználói felület többi szálát. A WPF szinkronizálási környezete kezeli a kód felfüggesztését, ami lehetővé teszi a WPF futtatását.

A példaalkalmazás munkafolyamatát bemutató diagram.

A példaalkalmazás munkafolyamatát bemutató diagram. Az alkalmazásnak egyetlen gombja van az "Előrejelzés lekérése" szöveggel. A gomb megnyomása után egy nyíl mutat az alkalmazás következő fázisára. Középen egy óra képe látható az alkalmazásban, jelezve, hogy az alkalmazás foglalt az adatok lekérésével. Egy idő után az alkalmazás egy képpel tér vissza a napról vagy az esőfelhőkről az adatok eredményétől függően.

A szakasz alapelveit bemutató mintaalkalmazás letölthető a GitHubról C# vagy Visual Basic. A példához tartozó XAML meglehetősen nagy, és ebben a cikkben nem szerepel. Az XAML böngészéséhez használja az előző GitHub-hivatkozásokat. Az XAML egyetlen gombot használ az időjárás lekéréséhez.

Vegye figyelembe az XAML mögötti kódot:

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

Az alábbiakban néhány feljegyezendő részletet talál.

  • A gombesemény-kezelő

    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
    

    Figyelje meg, hogy az eseménykezelőt async-val (vagy a Visual Basic-nél Async-gyel) deklarálták. Az "aszinkron" metódus lehetővé teszi a kód felfüggesztését egy várt metódus , például FetchWeatherFromServerAsyncmeghívása esetén. Ezt a await (vagy Await Visual Basic) kulcsszó jelöli. Amíg a FetchWeatherFromServerAsync befejeződik, a gomb kezelőkódja fel van függesztve, és a vezérlő visszakerül a hívóhoz. Ez hasonló a szinkron metódushoz, azzal a kivételrel, hogy a szinkron metódus megvárja, amíg a metódus minden művelete befejeződik, majd a vezérlő visszakerül a hívóhoz.

    A várt módszerek az aktuális módszer szálkezelési környezetét használják, amely a gombkezelő esetén a felhasználói felület szála. Ez azt jelenti, hogy a await FetchWeatherFromServerAsync(); meghívása (vagy Await FetchWeatherFromServerAsync() Visual Basic használatával) miatt a FetchWeatherFromServerAsync kód a felhasználói felületen fut, de a diszpécsernek nincs ideje futtatni, hasonlóan ahhoz, ahogyan az egyszálas alkalmazás hosszú ideig futó számítási példa működik. Figyelje meg azonban, hogy await Task.Run van használatban. Ez új szálat hoz létre a szálkészleten az aktuális szál helyett a kijelölt tevékenységhez. Tehát FetchWeatherFromServerAsync a saját szálán fut.

  • Az időjárás lekérése

    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
    

    Az egyszerűség érdekében ebben a példában valójában nincs hálózati kód. Ehelyett a hálózati hozzáférés késleltetését szimuláljuk úgy, hogy négy másodpercre alvó állapotba állítjuk az új szálat. Ebben az esetben az eredeti felhasználói felületi szál továbbra is fut, és válaszol a felhasználói felület eseményeire, miközben a gomb eseménykezelője az új szál befejezéséig szünetel. Ennek bemutatásához egy animációt futtattunk, és átméretezheti az ablakot. Ha a felhasználói felület szála fel lett függesztve vagy késleltetve, az animáció nem jelenik meg, és nem tudja használni az ablakot.

    Amikor a Task.Delay elkészült, és véletlenszerűen kiválasztottuk az időjárás-előrejelzést, az időjárás állapota visszakerül a hívóhoz.

  • A felhasználói felület frissítése

    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
    

    Amikor a feladat befejeződik, és a felhasználói felület szálának van ideje, a Task.Runhívója , a gomb eseménykezelője újraindul. A módszer többi része leállítja az óra animációját, és kiválaszt egy képet az időjárás leírásához. Megjeleníti a képet, és aktiválja az „előrejelzés beolvasása” gombot.

A szakasz alapelveit bemutató mintaalkalmazás letölthető a GitHubról C# vagy Visual Basic.

Műszaki részletek és elakadáspontok

A következő szakaszok ismertetik a többszálúság néhány részletét és nehézségi pontját.

Beágyazott szivattyúzás

Néha nem lehetséges teljesen zárolni a felhasználói felületi szálat. Tekintsük át a Show osztály MessageBox metódusát. Show csak akkor tér vissza, ha a felhasználó az OK gombra kattint. Az interaktívság érdekében azonban létrehoz egy olyan ablakot, amely rendelkezik üzenethurkossal. Amíg arra várunk, hogy a felhasználó az OK gombra kattintson, az eredeti alkalmazásablak nem válaszol a felhasználói bemenetre. Azonban továbbra is dolgozza fel a festőüzeneteket. Az eredeti ablak újrarajzolja magát, amikor lefedik és felfedik.

Képernyőkép az OK gombot tartalmazó Üzenetmezőről

Egy szálnak felelősséget kell vállalnia az üzenetablakért. A WPF létrehozhat egy új szálat csak az üzenetdoboz ablakához, de ez a szál nem tudja festeni a letiltott elemeket az eredeti ablakban (ne feledje a kölcsönös kizárás korábbi vitáját). Ehelyett a WPF beágyazott üzenetfeldolgozó rendszert használ. A Dispatcher osztály tartalmaz egy speciális, PushFramenevű metódust, amely egy alkalmazás aktuális végrehajtási pontját tárolja, majd egy új üzenethurkot indít el. A beágyazott üzenethurok befejeződésekor a végrehajtás folytatódik az eredeti PushFrame hívás után.

Ebben az esetben PushFrame fenntartja a programkörnyezetet a MessageBox.Showhívásánál, és egy új üzenethurkot indít el a háttérablak újrafestéséhez és az üzenetmező ablakának bemenetének kezeléséhez. Amikor a felhasználó az OK gombra kattint, és bezárja az előugró ablakot, a beágyazott ciklus kilép, és a vezérlés a Showhívása után folytatódik.

Elavult irányított események

A WPF-ben az irányított eseményrendszer értesíti a teljes fákat, amikor események történnek.

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

Amikor a bal egérgombot lenyomja az ellipszén, a handler2 végrehajtódik. A handler2 befejeződése után az esemény tovább lesz adva a Canvas objektumnak, amely handler1 használatával dolgozza fel. Ez csak akkor fordul elő, ha handler2 nem kifejezetten kezeli az eseményobjektumot.

Lehetséges, hogy handler2 sok időt vesz igénybe az esemény feldolgozása. handler2 PushFrame használatával olyan beágyazott üzenethurkot indíthat el, amely órákig nem tér vissza. Ha handler2 nem jelöli meg az eseményt az üzenethurok befejezésekor kezeltként, akkor az esemény még akkor is átkerül a hierarchiában felfelé, ha nagyon régi.

Újrabezárás és zárolás

A közös nyelvi futtatókörnyezet (CLR) zárolási mechanizmusa nem pontosan úgy viselkedik, ahogy azt el lehet képzelni; előfordulhat, hogy egy szál teljesen leáll a zárolás kérésekor. Valójában a szál továbbra is magas prioritású üzeneteket fogad és dolgoz fel. Ez segít megelőzni a holtpontokat, és minimálisan rugalmassá teszi az interfészeket, de lehetővé teszi a finom hibák lehetőségét. Az esetek túlnyomó többségében nem kell tudnia erről semmit, de ritka körülmények között (általában Win32-ablaküzeneteket vagy COM STA-összetevőket) érdemes tudni.

A legtöbb interfész nem a szálbiztonságra épül, mert a fejlesztők azzal a feltételezéssel dolgoznak, hogy a felhasználói felületet soha nem éri el több szál. Ebben az esetben az egyetlen szál váratlan időpontokban környezeti változásokat okozhat, ami azokat a rossz hatásokat okozza, amelyeket a DispatcherObject kölcsönös kizárási mechanizmusnak meg kell oldania. Vegye figyelembe a következő pszeudokódot:

diagram, amely a szálkezelés visszaléphetőségét mutatja.

Többnyire ez a helyes dolog, de vannak olyan esetek a WPF-ben, amikor az ilyen váratlan újrabeilleszkedés valóban problémákat okozhat. Ezért bizonyos kulcsidőkben a WPF meghívja DisableProcessing, amely módosítja a szál zárolási utasítását, hogy a SZOKÁSOS CLR-zárolás helyett a WPF újrarezentálásmentes zárolást használja.

Miért választotta ezt a viselkedést a CLR-csapat? Ehhez COM STA-objektumokat és véglegesítési szálat kellett létrehoznia. Ha egy objektum szemétgyűjtésre kerül, a Finalize metódusa a dedikált véglegesítő szálon fut, nem a felhasználói felületen. És ebben rejlik a probléma, mert a felhasználói felületen létrehozott COM STA-objektumot csak a felhasználói felületi szálon lehet megsemmisíteni. A CLR végrehajtja a BeginInvoke-nak megfelelő műveletet (ebben az esetben a Win32 SendMessagehasználatával). Ha azonban a felhasználói felület szála foglalt, a véglegesítő szál elakadt, és a COM STA objektumot nem lehet megsemmisíteni, ami komoly memóriavesztést okoz. Ezért a CLR csapata meghozta a nehéz döntést, hogy a zárak úgy működjenek, ahogyan most teszik.

A WPF feladata, hogy elkerülje a váratlan újrabetörést anélkül, hogy újra előidézne egy memóriaszivárgást, ezért nem tiltjuk le mindenhol az újrabetörést.

Lásd még