Threadingmodell

Windows Presentation Foundation (WPF) soll Entwicklern bei Problemen mit Threading helfen. Daher schreiben die meisten WPF-Entwickler keine Schnittstelle, die mehrere Threads verwendet. Da Multithreadprogramme komplex und schwierig zu debuggen sind, sollten sie vermieden werden, wenn Singlethread-Lösungen vorhanden sind.

Ganz gleich, wie gut es entworfen wurde, ist kein UI-Framework in der Lage, eine Singlethread-Lösung für jede Art von Problem bereitzustellen. WPF kommt dem nahe, aber es gibt noch immer Situationen, in denen mehrere Threads die Benutzeroberfläche (UI) oder die Anwendungsleistung verbessern. In diesem Artikel werden zunächst einige Hintergrundinformationen angegeben, dann einige dieser Situationen beschrieben und am Ende einige Details auf niedriger Ebene besprochen.

Hinweis

Dieses Thema diskutiert den Thread, indem sie die InvokeAsync-Methode für asynchroner Aufrufe benutzt. Die InvokeAsync-Methode verwendet Action oder Func<TResult> als Parameter und gibt DispatcherOperation oder DispatcherOperation<TResult> zurück, das über eine Task-Eigenschaft verfügt. Sie können das await-Schlüsselwort entweder mit DispatcherOperation oder dem zugehörigen Task verwenden. Wenn Sie synchron auf das Task warten müssen, das von DispatcherOperation oder DispatcherOperation<TResult> zurückgegeben wird, rufen Sie die DispatcherOperationWait-Erweiterungsmethode auf. Das Aufrufen von Task.Wait führt zu einem Deadlock. Weitere Informationen zur Verwendung eines Task, um asynchrone Vorgänge auszuführen, finden Sie unter Aufgabenbasierte asynchrone Programmierung.

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

Übersicht und Verteiler

In der Regel beginnen WPF-Anwendungen mit zwei Threads: Einer für das Rendering und einer für die Verwaltung der UI. Der Rendering-Thread läuft effektiv verborgen im Hintergrund, während der UI-Thread Eingaben empfängt, Ereignisse verarbeitet, den Bildschirm zeichnet und Anwendungscode ausführt. Die meisten Anwendungen verwenden einen einzelnen UI-Thread, aber in manchen Situationen sollten Sie besser mehrere verwenden. Dies wird weiter unten mit einem Beispiel erläutert.

Der UI-Thread setzt Arbeitseinheiten innerhalb eines Objekts, das Dispatcher genannt wird, in die Warteschlange. Vom Dispatcher werden Arbeitsaufgaben nach Priorität ausgewählt und jeweils vollständig ausgeführt. Jeder UI-Thread muss mindestens ein Dispatcher haben und jeder Dispatcher kann Arbeitselemente in genau einem Thread ausführen.

Der Trick beim Erstellen von reaktionsfähigen und benutzerfreundlichen Anwendungen besteht darin, den Dispatcher-Durchsatz zu maximieren, indem die Arbeitselemente klein gehalten werden. So veralten Elemente nicht, während Sie in der Dispatcher-Warteschlange auf die Verarbeitung warten. Jede spürbare Verzögerung zwischen Eingabe und Antwort kann für einen Benutzer frustrierend sein.

Wie sollen WPF-Anwendungen dann große Vorgänge verarbeiten? Was geschieht, wenn der Code eine aufwändige Berechnung beinhaltet oder eine Abfrage einer Datenbank auf einem Remoteserver ausführen muss? Die Antwort ist normalerweise, dass der umfangreiche Vorgang in einem separaten Thread verarbeitet wird, sodass der UI-Thread frei bleibt, um sich um Elemente in der Dispatcher-Warteschlange zu kümmern. Wenn der umfangreiche Vorgang abgeschlossen ist, kann er sein Ergebnis zur Anzeige zurück an den UI-Thread melden.

Historisch erlaubt Windows den Zugriff auf UI-Elemente nur über den Thread, von dem die Elemente erstellt wurden. Dies bedeutet, dass ein Hintergrundthread bei einer Aufgabe mit langer Laufzeit kein Textfeld aktualisieren kann, wenn er abgeschlossen ist. Windows stellt so die Integrität von UI-Komponenten sicher. Ein Listenfeld könnte merkwürdig aussehen, wenn sein Inhalt während des Zeichnens von einem Hintergrundthread aktualisiert werden würde.

WPF verfügt über einen integrierten gegenseitigen Ausschlussmechanismus, der diese Koordination erzwingt. Die meisten Klassen in WPF werden von DispatcherObject abgeleitet von. Bei der Konstruktion speichert ein DispatcherObject eine Verweis zur Dispatcher, der mit dem aktuell ausgeführten Thread verbunden ist. Tatsächlich assoziiert sich der DispatcherObject mit dem Thread, der ihn erstellt. Während der Programmausführung kann ein DispatcherObject seine öffentliche VerifyAccess-Methode aufrufen. VerifyAccess untersucht die dem aktuellen Thread zugeordnete Dispatcher und vergleicht diese mit der während der Konstruktion hergestellten Dispatcher-Referenz. Stimmen sie nicht überein, löst VerifyAccess eine Ausnahme aus. VerifyAccess soll zu Beginn jeder zu einer DispatcherObject gehörenden Methode aufgerufen werden.

Wenn nur ein Thread die UI ändern kann, wie interagieren Hintergrund-Threads dann mit dem Benutzer? Ein Hintergrundthread kann den UI-Thread auffordern, einen Vorgang in seinem Auftrag auszuführen. Dies macht es während der Registrierung eines Arbeitselements mit der Dispatcher oder dem UI-Thread. Die Dispatcher-Klasse bietet die Methoden zum Registrieren von Arbeitselementen: Dispatcher.InvokeAsync, Dispatcher.BeginInvoke und Dispatcher.Invoke. Diese Methoden planen einen Delegaten für die Ausführung ein. Invoke ist ein synchroner Aufruf – das heißt, dass die Rückgabe erst erfolgt, wenn der UI-Thread die Ausführung des Delegaten tatsächlich ausgeführt hat. InvokeAsync und BeginInvoke sind asynchron und werden sofort zurückgegeben.

Die Dispatcher ordnet die Elemente in seiner Warteschlange nach Priorität. Es gibt zehn Stufen, die angegeben werden können, wenn ein Element zur Dispatcher-Warteschlange hinzugefügt wird. Diese Prioritäten werden in der DispatcherPriority-Enumeration verwaltet.

Eine Singlethread-App mit einer Berechnung mit langer Laufzeit

Die meisten grafischen Benutzeroberflächen (GUIs) verbringen einen Groß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 genutzt werden, ohne die Reaktionsfähigkeit der UI zu beeinträchtigen. Das WPF-Threadingmodell lässt nicht zu, dass eine Eingabe einen Vorgang im UI-Thread unterbricht. Sie müssen deshalb sicherstellen, dass Sie in regelmäßigen Abständen zu Dispatcher zurückkehren, um ausstehende Eingabeereignisse zu verarbeiten, bevor sie veralten.

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

Sehen Sie sich das folgende Beispiel an:

Screenshot: Threading von Primzahlen

Diese einfache Anwendung zählt ab drei aufwärts und sucht dabei nach Primzahlen. Wenn der Benutzer auf die Start-Schaltfläche klickt, beginnt die Suche. Wenn das Programm eine Primzahl findet, wird die Benutzeroberfläche mit dieser Entdeckung aktualisiert. Der Benutzer kann die Suche zu jedem Zeitpunkt beenden.

Obwohl es sich um eine einfache Anwendung handelt, könnte die Suche nach Primzahlen endlos fortgesetzt werden, was einige Probleme bereitet. Wenn die gesamte Suche innerhalb des Click-Ereignishandlers der Schaltfläche verarbeitet werden würde, hätte der UI-Thread nie die Gelegenheit, andere Ereignisse zu verarbeiten. Die UI wäre nicht in der Lage, auf Eingaben zu reagieren oder Meldungen zu verarbeiten. Sie würde nie neu zeichnen und nie auf Mausklicks auf die Schaltflächen reagieren.

Wir könnten die Suche nach Primzahlen in einem separaten Thread ausführen, aber dann hätten wir Synchronisierungsprobleme. Mit einem Singlethread-Ansatz können wir die Bezeichnung, die die größte gefundene Primzahl auflistet, direkt aktualisieren.

Wenn wir die Berechnung in verwaltbare Teile aufteilen, können wir in regelmäßigen Abständen zu Dispatcher zurückkehren und Ereignisse verarbeiten. Wir können WPF die Gelegenheit geben, neu zu zeichnen und Eingaben zu verarbeitet.

Die beste Möglichkeit, Verarbeitungszeit auf Berechnung und Ereignisbehandlung aufzuteilen, ist die Verwaltung der Berechnung von Dispatcher. Mit der Verwendung einer InvokeAsync-Methode können wir planen, die Primzahlen in derselben Warteschlange einzuchecken, in der UI-Ereignisse gezeichnet werden. In unserem Beispiel planen wir nur eine einzige Primzahlüberprüfung zu einem Zeitpunkt ein. Nach Abschluss der Primzahlüberprüfung planen wir sofort die nächste Überprüfung ein. Diese Überprüfung erfolgt erst, wenn ausstehende UI-Ereignisse behandelt wurden.

Screenshot: Dispatcher-Warteschlange

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

Das folgende Beispiel zeigt die XAML, die die Benutzeroberfläche erstellt.

Wichtig

Der in diesem Artikel gezeigte XAML stammt aus einem C#-Projekt. Visual Basic XAML unterscheidet sich geringfügig beim Deklarieren der Sicherungsklasse 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>

Im folgenden Beispiel wird das CodeBehind gezeigt.

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 dem Aktualisieren des Textes auf Button ist der StartStopButton_Click-Handler auch für die Planung der ersten Primzahlüberprüfung durch Hinzufügen eines Delegaten zur Dispatcher-Warteschlange verantwortlich. Nachdem dieser Ereignishandler seine Arbeit abgeschlossen hat, wählt der Dispatcher den Delegaten nach einer Weile für die Ausführung aus.

Wie wir schon früher erwähnt haben, ist InvokeAsync das Dispatcher-Member, das verwendet wird, um die Ausführung zu delegieren. In diesem Fall haben wir die SystemIdle-Priorität gewählt. Der Dispatcher führt diesen Delegaten nur aus, wenn keine wichtigen Ereignisse zu verarbeiten sind. UI-Reaktionsfähigkeit ist wichtiger als die Zahlenüberprüfung. Wir übergeben auch einen neuen Delegaten, der die Zahlenüberprüfungsroutine 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

Diese Methode überprüft, ob die nächste ungerade Zahl eine Primzahl ist. Wenn sie eine Primzahl ist, aktualisiert die Methode bigPrime TextBlock direkt entsprechend. Dies ist möglich, da die Berechnung im selben Thread ausgeführt wird, der für die Erstellung des Steuerelements verwendet wurde. Wenn wir einen separaten Thread für die Berechnung verwendet hätten, müssten wir einen komplizierteren Synchronisierungsmechanismus verwenden und die Aktualisierung im UI-Thread ausführen. Wir werden diese Situation im Folgenden zeigen.

Mehrere Fenster, mehrere Threads

Für einige WPF-Anwendungen sind mehrere Fenster der obersten Ebene erforderlich. Es ist durchaus akzeptabel, dass eine Thread/Dispatcher-Kombination, mehrere Fenster zu verwalten, aber in manchen Fällen eignen sich mehrere Threads besser. Dies trifft besonders zu, wenn die Möglichkeit besteht, dass eines der Fenster den Thread für sich beansprucht.

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: WPF-Fenster, 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, sich drehende !! Glyphe befindet sich oben rechts in jedem Fenster.

Dieses Beispiel enthält ein Fenster mit einer sich drehenden ‼️-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 dreht sich ständig, bis die Pause-Schaltfläche 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 Pause gedrückt wird, reagieren die Fenster unter dem selben Thread 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>

Im folgenden Beispiel wird das CodeBehind gezeigt.

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 sind einige der Details aufgeführt, die beachtet werden sollten:

  • Die Task.Delay(TimeSpan)-Aufgabe wird verwendet, damit der aktuelle Thread fünf Sekunden lang angehalten wird, wenn die Schaltfläche Pause 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 unmittelbar ein neues Fenster unter dem 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 ein neues System.Windows.Threading.Dispatcher, um den neuen Thread zu verwalten. Wir müssen nur System.Windows.Threading.Dispatcher starten, um das Fenster funktionsfähig zu machen.

    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

Die Behandlung von blockierenden Vorgängen in einer grafischen Anwendung kann schwierig sein. Wir möchten keine blockierenden Methoden von Ereignishandlern aufrufen, da die Anwendung sonst scheinbar einfriert. Im vorherigen Beispiel wurden neue Fenster in ihrem eigenen Thread erstellt, sodass jedes Fenster unabhängig voneinander ausgeführt werden kann. Während wir einen neuen Thread mit System.Windows.Threading.Dispatcher erstellen können, wird es schwierig, den neuen Thread mit dem Standard UI-Thread zu synchronisieren, nachdem die Arbeit abgeschlossen wurde. Da der neue Thread die Benutzeroberfläche nicht direkt ändern kann, müssen wir Dispatcher.InvokeAsync, Dispatcher.BeginInvoke oder Dispatcher.Invoke verwenden, um Delegate in den Dispatcher des UI-Threads einzufügen. Schließlich werden diese Delegate mit der Berechtigung zum Ändern von UI-Elementen 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). Es basiert auf den Typen Task und Task<TResult> im System.Threading.Tasks-Namespace, mit denen asynchrone Vorgänge dargestellt werden. TAP verwendet eine einfache Methode, um die Initiierung und den Abschluss eines asynchronen Vorgangs darzustellen. Für dieses Muster gibt es einige Vorteile:

  • Der Aufrufer von Task kann auswählen, dass der Code asynchron oder synchron ausgeführt werden soll.
  • Der Fortschritt kann von Task gemeldet werden.
  • Der aufrufende Code kann die Ausführung anhalten und auf das Ergebnis des Vorgangs warten.

Beispiel für Task.Run

In diesem Beispiel simulieren 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 angehalten, und der Rest des UI-Threads wird nicht blockiert. Der Synchronisierungskontext von WPF behandelt das Anhalten des Codes, wodurch WPF weiterhin ausgeführt werden kann.

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 „Prognose abrufen“. 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, das 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 mit der Sonne oder mit 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 code-behind für den XAML-Code:

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 beachtet werden sollten.

  • Der Ereignishandler für die Schaltflä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
    

    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 erwartete Methode wie z. B. FetchWeatherFromServerAsync aufgerufen wird. Diese wird durch das Stichwort await (oder Await mit Visual Basic) bestimmt. Bis FetchWeatherFromServerAsync abgeschlossen ist, wird der Handlercode der Schaltfläche angehalten und die Steuerung wieder an den Aufrufer zurückgegeben. Dies ähnelt einer synchronen Methode, mit der Ausnahme, dass eine synchrone Methode wartet, bis alle Vorgänge in der Methode abgeschlossen sind, bis das Steuerelement 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 FetchWeatherFromServerAsync im UI-Thread ausgeführt wird, aber nicht für den Dispatcher ausgeführt wird und Zeit zum Ausführen hat, ähnlich wie die Singlethread-App mit einem Beispiel für lange ausgeführte Berechnungen. 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. FetchWeatherFromServerAsync wird also im eigenen Thread ausgeführt.

  • Abrufen der Wettervorhersage

    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
    

    Aus Gründen der Einfachheit verwenden wir in diesem Beispiel keinen Netzwerk-Code. Stattdessen simulieren wir die Verzögerung des Netzwerkzugriffs, indem wir den neuen Thread für vier Sekunden 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 Aufrufer zurückgegeben.

  • Aktualisieren der UI

    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 von Task.Run, dem Ereignishandler der Schaltfläche, fortgesetzt. Die restliche Methode hält die Uhr-Animation an und wählt ein Bild aus, um das Wetter zu beschreiben. Sie zeigt dieses Bild an und aktiviert die Schaltfläche „Wettervorhersage abrufen“.

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

Technische Details und Stolpersteine

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

Geschachtelte Verteilung

Manchmal ist es nicht möglich, den UI-Thread vollständig zu sperren. Rufen Sie die Show-Methode der MessageBox-Klasse auf. Show kehrt nicht zurück, solange der Benutzer nicht auf die OK-Schaltfläche klickt. Es wird jedoch ein Fenster erstellt, das über eine Meldungsschleife verfügen muss, um interaktiv zu sein. Während wir warten, bis der Benutzer auf „OK“ klickt, reagiert das ursprüngliche Anwendungsfenster nicht auf Benutzereingaben. Es verarbeitet jedoch weiterhin Paint-Meldungen. Das ursprüngliche Fenster zeichnet sich selbst neu, wenn es verdeckt war und dann wieder angezeigt wird.

Screenshot: MessageBox mit Schaltfläche „OK“

Ein Thread muss für das Meldungsfenster zuständig sein. WPF könnte einen neuen Thread nur für das Meldungsfenster erstellen, aber dieser Thread wäre nicht in der Lage, die deaktivierten Elemente im ursprünglichen Fenster zu zeichnen (beachten Sie den bereits erwähnten gegenseitigen Ausschluss). Stattdessen verwendet WPF ein geschachteltes Meldungsverarbeitungssystem. Die Dispatcher-Klasse beinhaltet eine spezielle Methode namens PushFrame, womit ein aktueller Ausführungspunkt der Anwendung dann eine neue Nachrichtenschleife beginnt. Wenn die geschachtelte Nachrichtenschleife beendet ist, setzt die Ausführung nach dem ursprünglichen PushFrame-Aufruf ein.

In diesem Fall behält PushFrame den Programmkontext bei dem Aufruf zu MessageBox.Show bei und es startet eine neue Nachrichtenschleife, um das Hintergrundfenster neu zu kopieren und Eingaben an das Meldungsfeldfenster zu behandeln. Wenn der Benutzer auf OK klickt und das Popup-Fenster löscht, wird die geschachtelte Schleife beendet und die Steuerung wird nach dem Aufruf von Show wieder aufgenommen.

Veraltete Routingereignisse

Das Routingereignissystem in WPF benachrichtigt ganze Strukturen, 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 der Ellipse gedrückt wird, wird handler2 ausgeführt. Nach dem Abschluss von handler2 wird das Ereignis an das Canvas-Objekt übergeben, das handler1 zur Verarbeitung verwendet. Dies geschieht nur, wenn handler2 das Ereignisobjekt nicht explizit als behandelt kennzeichnet.

Es ist möglich, dass handler2 sehr viel Zeit benötigt, um dieses Ereignis zu verarbeiten. handler2 könnte PushFrame verwenden, um eine geschachtelte Nachrichtenschleife zu beginnen, die stundenlang nicht zurückkehrt. Wenn handler2 das Ereignis nicht als behandelt kennzeichnet, wenn diese Nachrichtenschleife abgeschlossen ist, wird das Ereignis in der Struktur nach oben übergeben, obwohl es sehr alt ist.

Wiedereintritt und Sperrung

Der Mechanismus zum Sperren der Common Language Runtime (CLR) verhält sich nicht genau so, wie man es sich vorstellen würde. Man könnte erwarten, dass ein Thread beim Anfordern einer Sperre den Vorgang vollständig beendet. In Wirklichkeit empfängt und verarbeitet der Thread weiterhin Meldungen mit hoher Priorität. Dadurch können Deadlocks vermieden und Schnittstellen minimal reaktionsfähig gemacht werden, aber dadurch können auch schwer erkennbare Fehler entstehen. Die meiste Zeit über müssen Sie dies nicht beachten, aber in seltenen Fällen (normalerweise im Zusammenhang mit WIN32-Fenstermeldungen oder COM-STA-Komponenten) kann dieses Wissen nützlich sein.

Die meisten Schnittstellen werden nicht im Hinblick auf Thread-Sicherheit erstellt, da Entwickler annehmen, dass auf eine UI nie durch mehrere Threads zugegriffen wird. In diesem Fall kann dieser einzelne Thread die Umgebung zu unerwarteten Zeiten ändern und so diese negativen Effekte verursachen, die der gegenseitige Ausschlussmechanismus von DispatcherObject lösen soll. Betrachten Sie den folgenden Pseudocode:

Diagramm: Wiedereintritt in den Thread

In den meisten Fällen ist dies das Richtige, aber manchmal kann solch ein unerwarteter Wiedereintritt in WPF wirklich zu Problemen führen. Deshalb ruft WPF zu bestimmten Zeiten DisableProcessing auf, wodurch die Sperranweisung für diesen Thread so geändert wird, dass sie die WPF-Sperre ohne Wiedereintritt anstatt der üblichen CLR-Sperre verwendet.

Warum hat sich das CLR-Team also für dieses Verhalten entschieden? Dies hatte mit COM-STA-Objekten und dem Finalizerthread zu tun. Wenn für ein Objekt eine automatische Speicherbereinigung durchgeführt wird, wird dessen Finalize-Methode auf dem dedizierten Finalizer-Thread ausgeführt und nicht auf dem UI-Thread. Und darin liegt das Problem, denn ein COM-STA-Objekt, das auf dem UI-Thread erstellt wurde, kann nur auf dem UI-Thread verworfen werden. Die CLR führt das Äquivalent zu einer BeginInvoke aus (in diesem Fall unter Verwendung einer SendMessage von Win32). Wenn der UI-Thread allerdings ausgelastet ist, wird der Finalizer-Thread verzögert und das COM-STA-Objekt kann nicht verworfen werden, was zu einem schwerwiegenden Speicherverlust führt. Deshalb hat sich das CLR-Team für diesen Aufruf entschieden, damit Sperren wie gewünscht funktionieren.

Die Aufgabe von WPF ist es, unerwarteten Wiedereintritt zu verhindern, ohne Speicherverlust zu verursachen, weshalb wir den Wiedereintritt nicht überall blockieren.

Siehe auch