Freigeben über


Threadmodell

Windows Presentation Foundation (WPF) wurde entwickelt, um Entwicklern die Schwierigkeiten beim Threading zu sparen. Daher schreiben die meisten WPF-Entwickler keine Schnittstelle, die mehr als einen Thread verwendet. Da Multithreadprogramme komplex und schwierig zu debuggen sind, sollten sie vermieden werden, wenn Singlethread-Lösungen vorhanden sind.

Unabhängig davon, wie gut architekturiert, kann kein UI-Framework eine Singlethread-Lösung für jede Art von Problem bereitstellen. WPF kommt dem nahe, aber es gibt immer noch Situationen, in denen mehrere Threads die Reaktionsfähigkeit der Benutzeroberfläche oder die Anwendungsleistung verbessern. Nach der Erläuterung einiger Hintergrundmaterialien untersucht dieser Artikel einige dieser Situationen und endet dann mit einer Diskussion über einige Details auf niedrigerer Ebene.

Hinweis

In diesem Thema wird threading mithilfe der InvokeAsync Methode für asynchrone Aufrufe erläutert. Die InvokeAsync Methode verwendet einen Action oder Func<TResult> als Parameter und gibt ein DispatcherOperation oder DispatcherOperation<TResult>, das über eine Task Eigenschaft verfügt. Sie können das await-Schlüsselwort entweder mit dem DispatcherOperation oder dem Task verwenden. Wenn Sie synchrones Warten benötigen, bis Task von einem DispatcherOperation oder DispatcherOperation<TResult> zurückgegeben wird, rufen Sie die DispatcherOperationWait-Erweiterungsmethode auf. Ein Aufruf Task.Wait führt zu einer Verklemmung. Weitere Informationen über das Ausführen asynchroner Task Vorgänge finden Sie unter Aufgabenbasierte asynchrone Programmierung.

Um einen synchronen Aufruf durchzuführen, verwenden Sie die Invoke-Methode, die auch Überladungen enthält, die einen Delegaten, Action oder Func<TResult>-Parameter verwenden.

Übersicht und Verteiler

In der Regel beginnen WPF-Anwendungen mit zwei Threads: eines zum Behandeln des Renderings und eines zum Verwalten der Benutzeroberfläche. Der Renderingthread wird effektiv im Hintergrund ausgeführt, während der UI-Thread Eingaben empfängt, Ereignisse behandelt, den Bildschirm zeichnet und Anwendungscode ausführt. Die meisten Anwendungen verwenden einen einzelnen UI-Thread, obwohl es in einigen Situationen am besten ist, mehrere zu verwenden. Wir werden dies später mit einem Beispiel besprechen.

** Der UI-Thread reiht Arbeitsaufgaben innerhalb eines Objekts ein, das ein Dispatcher genannt wird. Die Dispatcher wählt Arbeitsaufgaben auf Prioritätsbasis aus und führt jede von ihnen bis zum Abschluss aus. Jeder UI-Thread muss mindestens einen Dispatcherhaben, und jede Dispatcher kann Arbeitsaufgaben in genau einem Thread ausführen.

Der Trick beim Erstellen reaktionsfähiger, benutzerfreundlicher Anwendungen besteht darin, den Dispatcher Durchsatz zu maximieren, indem die Arbeitsaufgaben klein gehalten werden. Auf diese Weise veralten Elemente nie, während sie in der Dispatcher Warteschlange auf ihre Verarbeitung warten. Jede erkennbare Verzögerung zwischen Eingabe und Reaktion kann einen Benutzer frustrieren.

Wie sollen dann WPF-Anwendungen große Vorgänge verarbeiten? Was geschieht, wenn Ihr Code eine große Berechnung umfasst oder eine Datenbank auf einem Remoteserver abfragen muss? Normalerweise besteht die Antwort darin, den großen Vorgang in einem separaten Thread zu verarbeiten und den UI-Thread frei zu lassen, um Elemente in der Dispatcher-Warteschlange zu bearbeiten. Wenn der große Vorgang abgeschlossen ist, kann er das Ergebnis zurück an den UI-Thread für die Anzeige melden.

In der Vergangenheit ermöglicht Windows den Zugriff auf UI-Elemente nur durch den Thread, der sie erstellt hat. Dies bedeutet, dass ein Hintergrundthread, der für einige lange ausgeführte Aufgaben verantwortlich ist, ein Textfeld nicht aktualisieren kann, wenn er abgeschlossen ist. Windows gewährleistet dies, um die Integrität von UI-Komponenten sicherzustellen. Ein Listenfeld könnte seltsam aussehen, wenn der Inhalt während der Zeichnung von einem Hintergrundthread aktualisiert wurde.

WPF verfügt über einen integrierten gegenseitigen Ausschlussmechanismus, der diese Koordinierung erzwingt. Die meisten Klassen in WPF erben von DispatcherObject. Bei der Konstruktion speichert ein DispatcherObject einen Verweis auf den Dispatcher, der mit dem aktuell laufenden Thread verknüpft ist. Tatsächlich ist die DispatcherObject mit dem Thread verknüpft, der sie erstellt. Während der Programmausführung kann ein DispatcherObject seine öffentliche VerifyAccess Methode aufrufen. VerifyAccess untersucht den Dispatcher aktuellen Thread und vergleicht ihn mit dem während der Dispatcher Konstruktion gespeicherten Verweis. Wenn sie nicht übereinstimmen, löst VerifyAccess eine Ausnahme aus. Am Anfang jeder Methode, die zu einem VerifyAccess gehört, soll DispatcherObject aufgerufen werden.

Wenn nur ein Thread die Benutzeroberfläche ändern kann, wie interagieren Hintergrundthreads mit dem Benutzer? Ein Hintergrundthread kann den UI-Thread bitten, einen Vorgang in seinem Auftrag auszuführen. Dazu registrieren Sie eine Arbeitsaufgabe für den Dispatcher UI-Thread. Die Dispatcher Klasse stellt die Methoden zum Registrieren von Arbeitsaufgaben bereit: Dispatcher.InvokeAsync, , Dispatcher.BeginInvokeund Dispatcher.Invoke. Diese Methoden planen einen Delegaten für die Ausführung. Invoke ist ein synchroner Aufruf, das heißt, er kehrt erst zurück, wenn der UI-Thread die Ausführung des Delegaten abgeschlossen hat. InvokeAsync und BeginInvoke sind asynchron und geben sofort zurück.

Die Dispatcher Elemente in der Warteschlange werden nach Priorität sortiert. Es gibt zehn Ebenen, die beim Hinzufügen eines Elements zur Dispatcher Warteschlange angegeben werden können. Diese Prioritäten werden in der DispatcherPriority Enumeration beibehalten.

Einfädelige App mit lang andauernder Berechnung

Die meisten grafischen Benutzeroberflächen (GUIs) verbringen einen großen Teil ihrer Zeit im Leerlauf, während sie auf Ereignisse warten, die als Reaktion auf Benutzerinteraktionen generiert werden. Mit sorgfältiger Programmierung kann diese Leerlaufzeit konstruktiv verwendet werden, ohne die Reaktionsfähigkeit der Benutzeroberfläche zu beeinträchtigen. Das WPF-Threadingmodell lässt keine Eingabe zu, um einen Vorgang im UI-Thread zu unterbrechen. Dies bedeutet, dass Sie in regelmäßigen Abständen unbedingt zum Dispatcher zurückkehren müssen, um ausstehende Eingabeereignisse zu verarbeiten, bevor sie veraltet werden.

Eine Beispiel-App, die die Konzepte dieses Abschnitts veranschaulicht, kann von GitHub für C# oder Visual Basic heruntergeladen werden.

Betrachten Sie das folgenden Beispiel:

Ein Screenshot, der die Parallelisierung von Primzahlen zeigt.

Diese einfache Anwendung zählt von drei nach oben und sucht nach Primzahlen. Wenn der Benutzer auf die Schaltfläche "Start " klickt, beginnt die Suche. Wenn das Programm eine Primzahl findet, aktualisiert es die Benutzeroberfläche mit seiner Ermittlung. Der Benutzer kann die Suche jederzeit beenden.

Obwohl einfach genug, könnte die Primzahlsuche für immer weitergehen, was einige Schwierigkeiten darstellt. Wenn wir die gesamte Suche innerhalb des Click-Ereignishandlers der Schaltfläche behandeln würden, könnten wir dem UI-Thread niemals die Möglichkeit geben, andere Ereignisse zu verarbeiten. Die Benutzeroberfläche kann nicht auf Eingabe- oder Verarbeitungsmeldungen reagieren. Es würde nie neu zeichnen und nie auf Klicks auf Schaltflächen reagieren.

Wir könnten die Primzahlsuche in einem separaten Thread durchführen, aber dann müssten wir mit Synchronisierungsproblemen umgehen. Mit einem einzelnen Thread-Ansatz können wir das Label direkt aktualisieren, das die größte gefundene Primzahl auflistet.

Wenn wir die Berechnungsaufgabe in verwaltbare Blöcke aufteilen, können wir regelmäßig zu den Dispatcher Ereignissen und Prozessen zurückkehren. Wir können WPF die Möglichkeit geben, neu zu zeichnen und Eingaben zu verarbeiten.

Der beste Weg, die Verarbeitungszeit zwischen Berechnung und Ereignisbehandlung aufzuteilen, besteht darin, die Berechnung vom Dispatcher aus zu steuern. Unter Verwendung der InvokeAsync-Methode können wir Primzahlprüfungen in derselben Warteschlange planen, aus der UI-Ereignisse abgerufen werden. In unserem Beispiel planen wir jeweils nur eine einzige Primzahlüberprüfung. Nach Abschluss der Primzahlüberprüfung planen wir die nächste Prüfung sofort. Diese Überprüfung wird erst fortgesetzt, nachdem ausstehende UI-Ereignisse behandelt wurden.

Screenshot, der die Dispatcher-Warteschlange zeigt.

Microsoft Word führt die Rechtschreibprüfung mithilfe dieses Mechanismus durch. Die Rechtschreibprüfung erfolgt im Hintergrund mithilfe der Leerlaufzeit des UI-Threads. Sehen wir uns den Code an.

Das folgende Beispiel zeigt den XAML-Code, der die Benutzeroberfläche erstellt.

Von Bedeutung

Der in diesem Artikel gezeigte XAML stammt aus einem C#-Projekt. Visual Basic XAML unterscheidet sich geringfügig beim Deklarieren der Hintergrundklasse für den XAML-Code.

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

Das folgende Beispiel zeigt das Code-Behind.

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

Neben der Aktualisierung des Texts auf dem Button ist der StartStopButton_Click Handler für die Planung der ersten Primzahl-Prüfung zuständig, indem er einen Delegierten zur Dispatcher Warteschlange hinzufügt. Irgendwann nachdem dieser Ereignishandler seine Arbeit abgeschlossen hat, wird der Delegat für die Ausführung durch Dispatcher ausgewählt.

Wie bereits erwähnt, ist InvokeAsync das Dispatcher-Mitglied, das verwendet wird, um einen Delegaten zur Ausführung zu planen. In diesem Fall wählen wir die SystemIdle Priorität aus. Der Delegat Dispatcher wird nur ausgeführt, wenn keine wichtigen Ereignisse zur Verarbeitung vorhanden sind. Die Reaktionsfähigkeit der Benutzeroberfläche ist wichtiger als die Zahlenüberprüfung. Außerdem wird eine neue Stellvertretung übergeben, die die Routine für die Nummernüberprüfung darstellt.

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

Mit dieser Methode wird überprüft, ob die nächste ungerade Zahl primiert ist. Wenn es eine Primzahl ist, aktualisiert die Methode bigPrimeTextBlock direkt, um deren Entdeckung widerzuspiegeln. Dies ist möglich, da die Berechnung im selben Thread erfolgt, der zum Erstellen des Steuerelements verwendet wurde. Wenn wir einen separaten Thread für die Berechnung verwenden wollten, müssten wir einen komplizierteren Synchronisierungsmechanismus verwenden und das Update im UI-Thread ausführen. Wir zeigen diese Situation als Nächstes.

Mehrere Fenster, mehrere Threads

Für einige WPF-Anwendungen sind mehrere Fenster der obersten Ebene erforderlich. Es ist vollkommen akzeptabel, dass eine Thread-/Dispatcher-Kombination mehrere Fenster verwaltet, aber manchmal führen mehrere Threads einen besseren Auftrag aus. Dies gilt insbesondere, wenn es die Möglichkeit gibt, dass eines der Fenster den Thread monopolisiert.

Windows Explorer funktioniert auf diese Weise. Jedes neue Explorer-Fenster gehört zum ursprünglichen Prozess, wird jedoch unter der Kontrolle eines unabhängigen Threads erstellt. Wenn Explorer nicht mehr reagiert, z. B. bei der Suche nach Netzwerkressourcen, sind andere Explorer-Fenster weiterhin reaktionsfähig und verwendbar.

Wir können dieses Konzept mit dem folgenden Beispiel veranschaulichen.

Screenshot eines WPF-Fensters, das viermal dupliziert wird. Drei Der Fenster geben an, dass sie denselben Thread verwenden, während die anderen beiden in verschiedenen Threads vorhanden sind.

Die drei oberen Fenster dieses Bilds haben denselben Threadbezeichner: 1. Die beiden anderen Fenster weisen unterschiedliche Thread-IDs auf: Neun und 4. Eine magentafarbene rotierende ‼️ Glyphe befindet sich in der oberen rechten Ecke jedes Fensters.

Dieses Beispiel enthält ein Fenster mit einer gedrehten ‼️ Glyphe , einer Pause-Schaltfläche und zwei anderen Schaltflächen, die ein neues Fenster unter dem aktuellen Thread oder in einem neuen Thread erstellen. Die ‼️ Glyphe wird ständig gedreht, bis die Pause-Taste gedrückt wird, wodurch der Thread fünf Sekunden lang angehalten wird. Am unteren Rand des Fensters wird der Threadbezeichner angezeigt.

Wenn die Schaltfläche "Anhalten" gedrückt wird, reagieren alle Fenster, die demselben Thread zugeordnet sind, nicht mehr. Jedes Fenster unter einem anderen Thread funktioniert weiterhin normal.

Das folgende Beispiel ist der XAML-Code für das Fenster:

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

Das folgende Beispiel zeigt das Code-Behind.

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

Im Folgenden finden Sie einige der zu beachtenden Details:

  • Die Task.Delay(TimeSpan) Aufgabe wird verwendet, damit der aktuelle Thread fünf Sekunden lang angehalten wird, wenn die Schaltfläche " Anhalten " gedrückt wird.

    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
    
  • Der SameThreadWindow_Click Ereignishandler zeigt sofort ein neues Fenster im aktuellen Thread an. Der NewThreadWindow_Click Ereignishandler erstellt einen neuen Thread, der mit der Ausführung der ThreadStartingPoint Methode beginnt, die wiederum ein neues Fenster anzeigt, wie im nächsten Aufzählungspunkt beschrieben.

    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
    
  • Die ThreadStartingPoint Methode ist der Ausgangspunkt für den neuen Thread. Das neue Fenster wird unter der Kontrolle dieses Threads erstellt. WPF erstellt automatisch eine neue System.Windows.Threading.Dispatcher zum Verwalten des neuen Threads. Alles, was wir tun müssen, um das Fenster funktionsfähig zu machen, besteht darin, das System.Windows.Threading.DispatcherFenster zu starten.

    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
    

Eine Beispiel-App, die die Konzepte dieses Abschnitts veranschaulicht, kann von GitHub für C# oder Visual Basic heruntergeladen werden.

Behandeln eines Blockierungsvorgangs mit Task.Run

Das Behandeln von Blockierungsvorgängen in einer grafischen Anwendung kann schwierig sein. Wir möchten keine blockierenden Methoden von Ereignishandlern aufrufen, da die Anwendung scheinbar anfriert. Im vorherigen Beispiel wurden neue Fenster in ihrem eigenen Thread erstellt, sodass jedes Fenster unabhängig voneinander ausgeführt werden kann. Während wir mit System.Windows.Threading.Dispatcher einen neuen Thread erstellen können, wird es schwierig, diesen nach Abschluss der Arbeit mit dem Haupt-UI-Thread zu synchronisieren. Da der neue Thread die Benutzeroberfläche nicht direkt ändern kann, müssen wir Dispatcher.InvokeAsync, Dispatcher.BeginInvoke oder Dispatcher.Invoke verwenden, um Delegaten in den Dispatcher des UI-Threads der Benutzeroberfläche einzufügen. Schließlich werden diese Delegierten mit der Berechtigung zur Änderung von Benutzeroberflächenelementen ausgeführt.

Es gibt eine einfachere Möglichkeit, den Code in einem neuen Thread auszuführen, während die Ergebnisse synchronisiert werden, das aufgabenbasierte asynchrone Muster (TAP). Sie basiert auf den Task Und Task<TResult> Typen im System.Threading.Tasks Namespace, die verwendet werden, um asynchrone Vorgänge darzustellen. TAP verwendet eine einzelne Methode, um die Initiierung und den Abschluss eines asynchronen Vorgangs darzustellen. Für dieses Muster gibt es einige Vorteile:

  • Der Aufrufer eines Task Codes kann auswählen, dass der Code asynchron oder synchron ausgeführt werden soll.
  • Der Fortschritt lässt sich vom Task berichten.
  • Der aufrufende Code kann die Ausführung anhalten und auf das Ergebnis des Vorgangs warten.

Beispiel für Task.Run

In diesem Beispiel imitieren wir einen Remoteprozeduraufruf, der eine Wettervorhersage abruft. Wenn auf die Schaltfläche geklickt wird, wird die Benutzeroberfläche aktualisiert, um anzugeben, dass der Datenabruf ausgeführt wird, während eine Aufgabe gestartet wird, um das Abrufen der Wettervorhersage nachzuahmen. Wenn die Aufgabe gestartet wird, wird der Ereignishandlercode der Schaltfläche angehalten, bis die Aufgabe abgeschlossen ist. Nach Abschluss der Aufgabe wird der Ereignishandlercode weiterhin ausgeführt. Der Code wird ausgesetzt, und er blockiert den Rest des UI-Threads nicht. Der Synchronisierungskontext von WPF behandelt das Anhalten des Codes, wodurch WPF weiterhin ausgeführt werden kann.

Ein Diagramm, das den Workflow der Beispiel-App veranschaulicht.

Ein Diagramm, das den Workflow der Beispiel-App veranschaulicht. Die App verfügt über eine einzelne Schaltfläche mit dem Text "Fetch Forecast". Es gibt einen Pfeil, der auf die nächste Phase der App zeigt, nachdem die Schaltfläche gedrückt wurde. Dabei handelt es sich um ein Uhrbild in der Mitte der App, der angibt, dass die App mit dem Abrufen von Daten beschäftigt ist. Nach einiger Zeit gibt die App je nach Ergebnis der Daten entweder ein Bild der Sonne oder regenwolken zurück.

Eine Beispiel-App, die die Konzepte dieses Abschnitts veranschaulicht, kann von GitHub für C# oder Visual Basic heruntergeladen werden. Der XAML-Code für dieses Beispiel ist ziemlich groß und wird in diesem Artikel nicht bereitgestellt. Verwenden Sie die vorherigen GitHub-Links, um den XAML-Code zu durchsuchen. Der XAML-Code verwendet eine einzelne Schaltfläche zum Abrufen des Wetters.

Berücksichtigen Sie den Code-Behind für den 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

Im Folgenden sind einige der Details aufgeführt, die zu beachten sind.

  • Der Schaltflächenereignishandler

    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
    

    Beachten Sie, dass der Ereignishandler mit async (oder Async mit Visual Basic) deklariert wurde. Eine Async-Methode ermöglicht das Anhalten des Codes, wenn eine Methode, wie z. B. FetchWeatherFromServerAsync, aufgerufen wird. Dies wird durch das await Schlüsselwort (oder Await mit Visual Basic) festgelegt. Bis zum FetchWeatherFromServerAsync Abschluss wird der Handlercode der Schaltfläche angehalten, und das Steuerelement wird an den Aufrufer zurückgegeben. Dies ähnelt einer synchronen Methode, außer dass eine synchrone Methode darauf wartet, dass jeder Vorgang in der Methode abgeschlossen ist, bevor die Steuerung an den Aufrufer zurückgegeben wird.

    Die erwarteten Methoden verwenden den Threadingkontext der aktuellen Methode, die mit dem Schaltflächenhandler den UI-Thread darstellt. Dies bedeutet, dass der Aufruf von await FetchWeatherFromServerAsync(); (oder Await FetchWeatherFromServerAsync() mit Visual Basic) bewirkt, dass der Code in FetchWeatherFromServerAsync im UI-Thread ausgeführt wird. Der Dispatcher führt diesen jedoch erst aus, wenn er Zeit dafür hat, ähnlich wie das Beispiel Singlethread-App mit lang andauernder Berechnung arbeitet. Beachten Sie jedoch, dass await Task.Run verwendet wird. Dadurch wird ein neuer Thread im Threadpool für die angegebene Aufgabe anstelle des aktuellen Threads erstellt. Läuft FetchWeatherFromServerAsync somit auf einem eigenen Thread.

  • Wetter abrufen

    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
    

    Um die Dinge einfach zu halten, haben wir in diesem Beispiel keinen Netzwerkcode. Stattdessen simulieren wir die Verzögerung des Netzwerkzugriffs, indem wir unseren neuen Thread vier Sekunden lang in den Ruhezustand versetzen. In dieser Zeit wird der ursprüngliche UI-Thread weiterhin ausgeführt und reagiert auf UI-Ereignisse, während der Ereignishandler der Schaltfläche angehalten wird, bis der neue Thread abgeschlossen ist. Um dies zu veranschaulichen, haben wir eine Animation ausgeführt, und Sie können die Größe des Fensters ändern. Wenn der UI-Thread angehalten oder verzögert wurde, wird die Animation nicht angezeigt, und Sie konnten nicht mit dem Fenster interagieren.

    Wenn der Task.Delay Vorgang abgeschlossen ist und wir zufällig unsere Wettervorhersage ausgewählt haben, wird der Wetterstatus an den Anrufer zurückgegeben.

  • Aktualisieren der Benutzeroberfläche

    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
    

    Wenn die Aufgabe abgeschlossen ist und der UI-Thread Zeit hat, wird der Aufrufer des Task.Run-Ereignishandlers der Taste wiederaufgenommen. Der Rest der Methode stoppt die Uhranimation und wählt ein Bild aus, um das Wetter zu beschreiben. Es zeigt dieses Bild an und aktiviert die Schaltfläche "Prognose abrufen".

Eine Beispiel-App, die die Konzepte dieses Abschnitts veranschaulicht, kann von GitHub für C# oder Visual Basic heruntergeladen werden.

Technische Details und Stumblingpunkte

In den folgenden Abschnitten werden einige der Details und Stumblingpunkte beschrieben, auf die Sie möglicherweise multithreading stoßen.

Geschachtelte Pumpen

Manchmal ist es nicht möglich, den UI-Thread vollständig zu sperren. Betrachten wir die Show Methode der MessageBox Klasse. Show wird erst zurückgegeben, wenn der Benutzer auf die Schaltfläche "OK" klickt. Es wird jedoch ein Fenster erstellt, das über eine Nachrichtenschleife verfügen muss, um interaktiv zu sein. Während wir warten, dass der Benutzer auf "OK" klickt, reagiert das ursprüngliche Anwendungsfenster nicht auf Benutzereingaben. Sie verarbeitet jedoch weiterhin Paint-Nachrichten. Das ursprüngliche Fenster wird neu gezeichnet, wenn es verdeckt und offenbart wird.

Screenshot eines Meldungsfeldes mit der Schaltfläche

Ein Thread muss für das Nachrichtenfenster zuständig sein. WPF könnte nur für das Meldungsfeldfenster einen neuen Thread erstellen, aber dieser Thread konnte die deaktivierten Elemente nicht im ursprünglichen Fenster zeichnen (denken Sie sich an die frühere Diskussion über gegenseitige Ausgrenzung). Stattdessen verwendet WPF ein geschachteltes Nachrichtenverarbeitungssystem. Die Dispatcher Klasse enthält eine spezielle Methode namens PushFrame, die den aktuellen Ausführungspunkt einer Anwendung speichert und dann eine neue Nachrichtenschleife beginnt. Wenn die geschachtelte Nachrichtenschleife beendet ist, wird die Ausführung nach dem ursprünglichen PushFrame Aufruf fortgesetzt.

In diesem Fall PushFrame verwaltet der Programmkontext beim Aufruf MessageBox.Showund startet eine neue Nachrichtenschleife, um das Hintergrundfenster neu zu überschreiben und die Eingabe im Meldungsfeldfenster zu verarbeiten. Wenn der Benutzer auf "OK" klickt und das Popup-Fenster schließt, wird die geschachtelte Schleife beendet und die Kontrolle wird nach dem Aufruf Show fortgesetzt.

Abgestandene Routing-Ereignisse

Das Routingereignissystem in WPF benachrichtigt ganze Baumstrukturen, wenn Ereignisse ausgelöst werden.

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

Wenn die linke Maustaste über die Ellipse gedrückt wird, handler2 wird ausgeführt. Nach Abschluss von handler2 wird das Ereignis an das Canvas-Objekt übergeben, das handler1 verwendet, um es zu verarbeiten. Dies geschieht nur, wenn handler2 das Ereignisobjekt nicht explizit als behandelt markiert wird.

Es ist möglich, dass handler2 die Verarbeitung dieses Ereignisses viel Zeit in Anspruch nimmt. handler2 kann PushFrame verwenden, um eine geschachtelte Nachrichtenschleife zu beginnen, die stundenlang nicht zurückgegeben wird. Wenn handler2 das Ereignis nicht als behandelt markiert, wenn diese Meldungsschleife abgeschlossen ist, wird das Ereignis im Baum weitergereicht, obwohl es sehr alt ist.

Reentrancy und Sperren

Der Sperrmechanismus der Common Language Runtime (CLR) verhält sich nicht genau so, wie man es sich vorstellen könnte; man könnte erwarten, dass ein Thread vollständig stillgelegt wird, wenn eine Sperre angefordert wird. Tatsächlich empfängt der Thread weiterhin Nachrichten mit hoher Priorität und verarbeitet sie. Dies hilft, Deadlocks zu verhindern und Schnittstellen minimal reaktionsfähig zu machen, aber es führt die Möglichkeit für subtile Fehler ein. In den meisten Fällen müssen Sie darüber nichts wissen, aber unter seltenen Umständen (normalerweise bei Win32-Fensternachrichten oder COM-STA-Komponenten) kann es nützlich sein, dies zu wissen.

Die meisten Schnittstellen werden nicht mit Threadsicherheit erstellt, da Entwickler unter der Annahme arbeiten, dass auf eine Benutzeroberfläche niemals von mehr als einem Thread zugegriffen wird. In diesem Fall kann dieser einzelne Thread zu unerwarteten Zeiten Umweltänderungen vornehmen, was zu den schlechten Auswirkungen führt, die der DispatcherObject gegenseitige Ausschlussmechanismus lösen soll. Betrachten Sie den folgenden Pseudocode:

Diagramm, das threading reentrancy zeigt.

Meistens ist das das Richtige, aber es gibt Zeiten in WPF, in denen unerwarteter Wiedereintritt wirklich Probleme verursachen kann. Daher ruft WPF zu bestimmten Zeitpunkten DisableProcessing auf, die die Sperranweisung für diesen Thread so ändert, dass die WPF-Wiedereintrittsfreie Sperre verwendet wird, anstelle der üblichen CLR-Sperre.

Warum hat das CLR-Team dieses Verhalten gewählt? Es musste mit COM STA-Objekten und dem Finalisierungsthread zu tun sein. Wenn ein Objekt der Garbage-Collection unterliegt, wird seine Finalize-Methode im dedizierten Finalisierungs-Thread ausgeführt, nicht auf dem UI-Thread. Und genau darin liegt das Problem, da ein COM STA-Objekt, das im UI-Thread erstellt wurde, nur im UI-Thread freigegeben werden kann. Die CLR führt das Äquivalent eines BeginInvoke (in diesem Fall unter Verwendung von Win32s SendMessage) aus. Wenn der UI-Thread jedoch ausgelastet ist, wird der Finalizerthread angehalten, und das COM STA-Objekt kann nicht verworfen werden, wodurch ein schwerwiegender Speicherverlust entsteht. So hat das CLR-Team die schwierige Entscheidung getroffen, damit Sperren so funktionieren, wie sie es tun.

Die Aufgabe für WPF besteht darin, unerwartete Reentranz zu vermeiden, ohne den Speicherverlust wieder hervorzurufen. Deshalb blockieren wir die Reentranz nicht an allen Stellen.

Siehe auch