Freigeben über


Threading-Modell

Windows Presentation Foundation (WPF) dient dazu, Entwicklern die Schwierigkeiten des Threadings zu ersparen. Die meisten WPF-Entwickler müssen daher keine Schnittstellen mit mehr als einem Thread schreiben. Da Multithreadprogramme komplex und schwierig zu debuggen sind, sollten sie vermieden werden, wenn Singlethreadlösungen zur Verfügung stehen.

Doch so gut die Architektur auch sein mag, ist kein UInframework in der Lage, eine Singlethreadlösung für jedes Problem bereitzustellen. Obwohl WPF diesem Ideal nahe kommt, gibt es immer noch Situationen, in denen die Reaktionsgeschwindigkeit der user interface (UI) oder die Anwendungsleistung durch mehrere Threads verbessert wird. Nach der Erläuterung von Hintergrundmaterial werden in diesem Beitrag einige dieser Situationen untersucht, bevor zum Schluss tiefergehende Details erörtert werden.

Dieses Thema enthält folgende Abschnitte.

  • Übersicht und Verteiler
  • Threads in Aktion: Beispiele
  • Technische Details und Stolpersteine
  • Verwandte Abschnitte

Übersicht und Verteiler

In der Regel beginnen WPF-Anwendungen mit zwei Threads: einem für die Behandlung des Renderings und einem weiteren zum Verwalten der UI. Der Renderingthread wird effektiv verborgen im Hintergrund ausgeführt, während der UInthread Eingaben empfängt, Ereignisse behandelt, den Bildschirm zeichnet und Anwendungscode ausführt. Die meisten Anwendungen verwenden nur einen UInthread, obwohl in manchen Situationen die Verwendung mehrerer Threads vorteilhaft ist. Dies wird an späterer Stelle anhand eines Beispiels erörtert.

Der UInthread reiht Arbeitsaufgaben innerhalb eines als Dispatcher bezeichneten Objekts in eine Warteschlange ein. Der Dispatcher wählt Arbeitsaufgaben auf Grundlage einer Priorität aus und führt jede Aufgabe bis zu ihrem Abschluss aus. Jeder UInthread muss über mindestens einen Dispatcher verfügen, und jeder Dispatcher kann Arbeitsaufgaben in genau einem Thread ausführen.

Der Schlüssel zum Erstellen reaktionsschneller, benutzerfreundlicher Anwendungen besteht darin, den Dispatcher-Durchsatz zu optimieren, indem die Größe der Arbeitsaufgaben gering gehalten wird. Auf diese Weise veralten die Elemente in der Dispatcher-Warteschlange nicht, wo sie auf die Verarbeitung warten. Jede spürbare Verzögerung zwischen Eingabe und Antwort kann einen Benutzer frustrieren.

Wie aber sollen WPF-Anwendungen dann umfangreiche Vorgänge behandeln? Was, wenn Ihr Code eine aufwändige Berechnung enthält oder eine Datenbankabfrage auf einem Remoteserver durchgeführt werden muss? Normalerweise wird in einem solchen Fall der umfangreiche Vorgang in einen separaten Thread ausgelagert, sodass der UInthread für Elemente in der Dispatcher-Warteschlange zur Verfügung steht. Nachdem der umfangreiche Vorgang abgeschlossen ist, kann er sein Ergebnis zur Anzeige an den UInthread zurückübermitteln.

Traditionell ermöglicht Windows nur dem Thread den Zugriff auf die UInelemente, der sie erstellt hat. Dies bedeutet, dass ein für eine zeitintensive Aufgabe zuständiger Hintergrundthread ein Textfeld bei seiner Beendigung nicht aktualisieren kann. Dadurch stellt Windows die Integrität von UInkomponenten sicher. Ein Listenfeld könnte merkwürdig aussehen, wenn sein Inhalt während des Zeichnens von einem Hintergrundthread aktualisiert würde.

WPF verfügt über einen integrierten gegenseitigen Ausschlussmechanismus, der diese Koordination erzwingt. Die meisten Klassen in WPF sind von DispatcherObject abgeleitet. Beim Erstellen speichert ein DispatcherObject einen Verweis auf den Dispatcher, der mit dem aktuell ausgeführten Thread verknüpft ist. Tatsächlich ist das DispatcherObject dem Thread zugeordnet, von dem es erstellt wird. Während der Programmausführung kann ein DispatcherObject seine öffentliche VerifyAccess-Methode aufrufen. VerifyAccess überprüft den Dispatcher, der dem aktuellen Thread zugeordnet ist, und vergleicht ihn mit dem während der Erstellung gespeicherten Dispatcher-Verweis. Stimmen sie nicht überein, löst VerifyAccess eine Ausnahme aus. VerifyAccess soll zu Beginn jeder Methode aufgerufen werden, die zu einem DispatcherObject gehört.

Wenn nur ein Thread die UI ändern kann, wie interagieren Hintergrundthreads dann mit Benutzern? Ein Hintergrundthread kann den UInthread auffordern, einen Vorgang in seinem Auftrag auszuführen. Zu diesem Zweck registriert er eine Arbeitsaufgabe im Dispatcher des UInthreads. Die Dispatcher-Klasse stellt zwei Methoden zum Registrieren von Arbeitsaufgaben bereit: Invoke und BeginInvoke. Beide Methoden planen einen Delegaten für die Ausführung. Invoke ist ein synchroner Aufruf, d. h. er gibt erst dann ein Ergebnis zurück, wenn der UInthread die Ausführung des Delegaten tatsächlich beendet. BeginInvoke ist asynchron und gibt sein Ergebnis sofort zurück.

Der Dispatcher ordnet die Elemente in seiner Warteschlange nach Priorität an. Es gibt zehn Ebenen, die angegeben werden können, wenn der Dispatcher-Warteschlange ein Element hinzugefügt wird. Diese Prioritäten werden in der DispatcherPriority-Enumeration beibehalten. Ausführliche Informationen über DispatcherPriority-Ebenen finden Sie in der Windows SDK-Dokumentation.

Threads in Aktion: Beispiele

Singlethread-Anwendung mit einer Berechnung mit langer Laufzeit

Die meisten graphical user interfaces (GUIs) verbringen einen großen Teil ihrer Zeit im Leerlauf, während sie auf Ereignisse warten, die als Reaktion auf Benutzerinteraktionen generiert werden. Bei sorgfältiger Programmierung kann diese Leerlaufzeit konstruktiv genutzt werden, ohne die Reaktionszeit der UI zu beeinflussen. Das WPF-Threadmodell lässt nicht zu, dass Eingaben einen Vorgang im UInthread unterbrechen. Daher müssen Sie regelmäßig zum Dispatcher zurückkehren, um ausstehende Eingabeereignisse zu verarbeiten, bevor sie veralteten.

Betrachten Sie das folgende Beispiel:

Bildschirmabbildung für Primzahlen

Diese einfache Anwendung zählt von drei an aufwärts und sucht dabei nach Primzahlen. Wenn der Benutzer auf die Schaltfläche Start klickt, beginnt die Suche. Wenn das Programm eine Primzahl findet, wird die Benutzeroberfläche entsprechend aktualisiert. Der Benutzer kann die Suche jederzeit beenden.

Obwohl es sich bei der Primzahlensuche um eine einfache Aufgabe handelt, könnte sie endlos weitergehen, was einige Schwierigkeiten mit sich bringt. Wenn die gesamte Suche innerhalb des Handlers für das Click-Ereignis der Schaltfläche verarbeitet wird, hat der UInthread nie die Möglichkeit, andere Ereignisse zu behandeln. Die UI wäre nicht in der Lage, auf Eingaben zu reagieren oder Meldungen zu verarbeiten. Sie würde nie aktualisiert werden und nie auf Schaltflächenklicks reagieren.

Die Primzahlensuche könnte in einem separaten Thread ausgeführt werden, doch dann müssten Synchronisierungsprobleme abgefangen werden. Mit einem Singlethread-Ansatz kann die Bezeichnung, die die größte gefundene Primzahl auflistet, direkt aktualisiert werden.

Wenn die Berechnungsaufgabe in einfach zu handhabende Abschnitte aufgeteilt wird, können Sie regelmäßig zum Dispatcher zurückkehren und Ereignisse verarbeitet. WPF erhält die Möglichkeit, Eingaben neu zu zeichnen und zu verarbeiten.

Die beste Art, Verarbeitungszeit zwischen der Berechnung und der Ereignisbehandlung aufzuteilen, besteht darin, die Berechnung vom Dispatcher aus zu verwalten. Mit der BeginInvoke-Methode lassen sich Primzahlüberprüfungen in der gleichen Warteschlange planen, aus der auch UInereignisse stammen. In diesem Beispiel wird jeweils nur eine Primzahlüberprüfung geplant. Nachdem die Primzahlüberprüfung abgeschlossen ist, wird sofort die nächste Überprüfung geplant. Diese Überprüfung wird erst fortgesetzt, nachdem ausstehende UInereignisse behandelt wurden.

Darstellung der Dispatcher-Warteschlange

Mit diesem Mechanismus führt Microsoft Word die Rechtschreibprüfung aus. Die Rechtschreibprüfung wird im Hintergrund in der Leerlaufzeit des UInthreads ausgeführt. Sehen Sie sich einmal den Code an.

Im folgenden Beispiel wird der XAML-Code gezeigt, mit dem die Benutzeroberfläche erstellt wird.

<Window x:Class="SDKSamples.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="Prime Numbers" Width="260" Height="75"
    >
  <StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
    <Button Content="Start"  
            Click="StartOrStop"
            Name="startStopButton"
            Margin="5,0,5,0"
            />
    <TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
    <TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
  </StackPanel>
</Window>
<Window x:Class="SDKSamples.MainWindow"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="Prime Numbers" Width="260" Height="75"
    >
    <StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
        <Button Content="Start"  
            Click="StartOrStop"
            Name="startStopButton"
            Margin="5,0,5,0"
            />
        <TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
        <TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
    </StackPanel>
</Window>

Das folgende Beispiel zeigt den Code-Behind.

Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Threading
Imports System.Threading

Namespace SDKSamples
    Partial Public Class MainWindow
        Inherits Window
        Public Delegate Sub NextPrimeDelegate()

        'Current number to check 
        Private num As Long = 3

        Private continueCalculating As Boolean = False

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
            If continueCalculating Then
                continueCalculating = False
                startStopButton.Content = "Resume"
            Else
                continueCalculating = True
                startStopButton.Content = "Stop"
                startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
            End If
        End Sub

        Public Sub CheckNextNumber()
            ' Reset flag.
            NotAPrime = False

            For i As Long = 3 To Math.Sqrt(num)
                If num Mod i = 0 Then
                    ' Set not a prime flag to true.
                    NotAPrime = True
                    Exit For
                End If
            Next

            ' If a prime number.
            If Not NotAPrime Then
                bigPrime.Text = num.ToString()
            End If

            num += 2
            If continueCalculating Then
                startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
            End If
        End Sub

        Private NotAPrime As Boolean = False
    End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        public delegate void NextPrimeDelegate();

        //Current number to check 
        private long num = 3;   

        private bool continueCalculating = false;

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void StartOrStop(object sender, EventArgs e)
        {
            if (continueCalculating)
            {
                continueCalculating = false;
                startStopButton.Content = "Resume";
            }
            else
            {
                continueCalculating = true;
                startStopButton.Content = "Stop";
                startStopButton.Dispatcher.BeginInvoke(
                    DispatcherPriority.Normal,
                    new NextPrimeDelegate(CheckNextNumber));
            }
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            NotAPrime = false;

            for (long i = 3; i <= Math.Sqrt(num); i++)
            {
                if (num % i == 0)
                {
                    // Set not a prime flag to true.
                    NotAPrime = true;
                    break;
                }
            }

            // If a prime number.
            if (!NotAPrime)
            {
                bigPrime.Text = num.ToString();
            }

            num += 2;
            if (continueCalculating)
            {
                startStopButton.Dispatcher.BeginInvoke(
                    System.Windows.Threading.DispatcherPriority.SystemIdle, 
                    new NextPrimeDelegate(this.CheckNextNumber));
            }
        }

        private bool NotAPrime = false;
    }
}

Im folgenden Beispiel wird der Ereignishandler für die Button dargestellt.

Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
    If continueCalculating Then
        continueCalculating = False
        startStopButton.Content = "Resume"
    Else
        continueCalculating = True
        startStopButton.Content = "Stop"
        startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
    End If
End Sub
private void StartOrStop(object sender, EventArgs e)
{
    if (continueCalculating)
    {
        continueCalculating = false;
        startStopButton.Content = "Resume";
    }
    else
    {
        continueCalculating = true;
        startStopButton.Content = "Stop";
        startStopButton.Dispatcher.BeginInvoke(
            DispatcherPriority.Normal,
            new NextPrimeDelegate(CheckNextNumber));
    }
}

Außer für die Textaktualisierung von Button ist dieser Handler für die Planung der ersten Primzahlenüberprüfung verantwortlich, indem er der Dispatcher-Warteschlange einen Delegaten hinzufügt. Nachdem dieser Ereignishandler seine Arbeit abgeschlossen hat, wählt der Dispatcher diesen Delegaten zur Ausführung aus.

Wie bereits erwähnt, ist BeginInvoke der Dispatcher-Member, mit dem ein Delegat zur Ausführung geplant wird. In diesem Fall wird die SystemIdle-Priorität ausgewählt. Der Dispatcher führt diesen Delegaten nur aus, wenn keine wichtigen zu verarbeitenden Ereignisse vorhanden sind. Die Reaktionsgeschwindigkeit der UI ist wichtiger als die Zahlenüberprüfung. Außerdem wird ein neuer Delegat übergeben, der die Routine für die Zahlenüberprüfung darstellt.

Public Sub CheckNextNumber()
    ' Reset flag.
    NotAPrime = False

    For i As Long = 3 To Math.Sqrt(num)
        If num Mod i = 0 Then
            ' Set not a prime flag to true.
            NotAPrime = True
            Exit For
        End If
    Next

    ' If a prime number.
    If Not NotAPrime Then
        bigPrime.Text = num.ToString()
    End If

    num += 2
    If continueCalculating Then
        startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
    End If
End Sub

Private NotAPrime As Boolean = False
public void CheckNextNumber()
{
    // Reset flag.
    NotAPrime = false;

    for (long i = 3; i <= Math.Sqrt(num); i++)
    {
        if (num % i == 0)
        {
            // Set not a prime flag to true.
            NotAPrime = true;
            break;
        }
    }

    // If a prime number.
    if (!NotAPrime)
    {
        bigPrime.Text = num.ToString();
    }

    num += 2;
    if (continueCalculating)
    {
        startStopButton.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.SystemIdle, 
            new NextPrimeDelegate(this.CheckNextNumber));
    }
}

private bool NotAPrime = false;

Diese Methode überprüft, ob es sich bei der nächsten ungeraden Zahl um eine Primzahl handelt. Ist das der Fall, aktualisiert die Methode direkt den bigPrime TextBlock, um auf die erkannte Primzahl hinzuweisen. Dies ist möglich, weil die Berechnung im selben Thread stattfindet, mit dem die Komponente erstellt wurde. Wäre ein anderer Thread für die Berechnung ausgewählt worden, wäre ein komplizierterer Synchronisierungsmechanismus notwendig, und die Aktualisierung müsste im UInthread ausgeführt werden. Diese Situation wird im Folgenden dargestellt.

Den vollständigen Quellcode für dieses Beispiel finden Sie unter Beispiel für eine Singlethread-Anwendung mit Berechnung mit langer Laufzeit

Behandeln eines Blockierungsvorgangs mithilfe eines Hintergrundthreads

Die Behandlung von Blockierungsvorgängen in einer grafischen Anwendung kann schwierig sein. Blockierungmethoden sollen nicht von Ereignishandlern aufgerufen werden, da die Anwendung nicht mehr zu reagieren scheint. Diese Vorgänge können mithilfe eines separaten Threads behandelt werden, aber anschließend muss eine Synchronisierung mit dem UInthread erfolgen, da die GUI nicht direkt vom Arbeitsthread aus geändert werden kann. Mithilfe von Invoke oder BeginInvoke können Delegaten in den Dispatcher des UInthreads eingefügt werden. Letztlich werden diese Delegaten mit der Berechtigung ausgeführt, UInelemente zu ändern.

In diesem Beispiel soll ein Remoteprozeduraufruf eine Wettervorhersage abrufen. Dieser Aufruf wird mithilfe eines separaten Arbeitsthreads ausgeführt, und anschließend wird eine Aktualisierungsmethode im Dispatcher des UI-Threads geplant.

Bildschirmabbildung der Wetter-Benutzeroberfläche


Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Media
Imports System.Windows.Media.Animation
Imports System.Windows.Media.Imaging
Imports System.Windows.Shapes
Imports System.Windows.Threading
Imports System.Threading

Namespace SDKSamples
    Partial Public Class Window1
        Inherits Window
        ' Delegates to be used in placking jobs onto the Dispatcher.
        Private Delegate Sub NoArgDelegate()
        Private Delegate Sub OneArgDelegate(ByVal arg As String)

        ' Storyboards for the animations.
        Private showClockFaceStoryboard As Storyboard
        Private hideClockFaceStoryboard As Storyboard
        Private showWeatherImageStoryboard As Storyboard
        Private hideWeatherImageStoryboard As Storyboard

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Load the storyboard resources.
            showClockFaceStoryboard = CType(Me.Resources("ShowClockFaceStoryboard"), Storyboard)
            hideClockFaceStoryboard = CType(Me.Resources("HideClockFaceStoryboard"), Storyboard)
            showWeatherImageStoryboard = CType(Me.Resources("ShowWeatherImageStoryboard"), Storyboard)
            hideWeatherImageStoryboard = CType(Me.Resources("HideWeatherImageStoryboard"), Storyboard)
        End Sub

        Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Change the status image and start the rotation animation.
            fetchButton.IsEnabled = False
            fetchButton.Content = "Contacting Server"
            weatherText.Text = ""
            hideWeatherImageStoryboard.Begin(Me)

            ' Start fetching the weather forecast asynchronously.
            Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)

            fetcher.BeginInvoke(Nothing, Nothing)
        End Sub

        Private Sub FetchWeatherFromServer()
            ' Simulate the delay from network access.
            Thread.Sleep(4000)

            ' Tried and true method for weather forecasting - random numbers.
            Dim rand As New Random()
            Dim weather As String

            If rand.Next(2) = 0 Then
                weather = "rainy"
            Else
                weather = "sunny"
            End If

            ' Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
        End Sub

        Private Sub UpdateUserInterface(ByVal weather As String)
            'Set the weather image
            If weather = "sunny" Then
                weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
            ElseIf weather = "rainy" Then
                weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
            End If

            'Stop clock animation
            showClockFaceStoryboard.Stop(Me)
            hideClockFaceStoryboard.Begin(Me)

            'Update UI text
            fetchButton.IsEnabled = True
            fetchButton.Content = "Fetch Forecast"
            weatherText.Text = weather
        End Sub

        Private Sub HideClockFaceStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showWeatherImageStoryboard.Begin(Me)
        End Sub

        Private Sub HideWeatherImageStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showClockFaceStoryboard.Begin(Me, True)
        End Sub
    End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        // Delegates to be used in placking jobs onto the Dispatcher.
        private delegate void NoArgDelegate();
        private delegate void OneArgDelegate(String arg);

        // Storyboards for the animations.
        private Storyboard showClockFaceStoryboard;
        private Storyboard hideClockFaceStoryboard;
        private Storyboard showWeatherImageStoryboard;
        private Storyboard hideWeatherImageStoryboard;

        public Window1(): base()
        {
            InitializeComponent();
        }  

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Load the storyboard resources.
            showClockFaceStoryboard = 
                (Storyboard)this.Resources["ShowClockFaceStoryboard"];
            hideClockFaceStoryboard = 
                (Storyboard)this.Resources["HideClockFaceStoryboard"];
            showWeatherImageStoryboard = 
                (Storyboard)this.Resources["ShowWeatherImageStoryboard"];
            hideWeatherImageStoryboard = 
                (Storyboard)this.Resources["HideWeatherImageStoryboard"];   
        }

        private void ForecastButtonHandler(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            hideWeatherImageStoryboard.Begin(this);

            // Start fetching the weather forecast asynchronously.
            NoArgDelegate fetcher = new NoArgDelegate(
                this.FetchWeatherFromServer);

            fetcher.BeginInvoke(null, null);
        }

        private void FetchWeatherFromServer()
        {
            // Simulate the delay from network access.
            Thread.Sleep(4000);              

            // Tried and true method for weather forecasting - random numbers.
            Random rand = new Random();
            String weather;

            if (rand.Next(2) == 0)
            {
                weather = "rainy";
            }
            else
            {
                weather = "sunny";
            }

            // Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(
                System.Windows.Threading.DispatcherPriority.Normal,
                new OneArgDelegate(UpdateUserInterface), 
                weather);
        }

        private void UpdateUserInterface(String weather)
        {    
            //Set the weather image
            if (weather == "sunny")
            {       
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "SunnyImageSource"];
            }
            else if (weather == "rainy")
            {
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "RainingImageSource"];
            }

            //Stop clock animation
            showClockFaceStoryboard.Stop(this);
            hideClockFaceStoryboard.Begin(this);

            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;     
        }

        private void HideClockFaceStoryboard_Completed(object sender,
            EventArgs args)
        {         
            showWeatherImageStoryboard.Begin(this);
        }

        private void HideWeatherImageStoryboard_Completed(object sender,
            EventArgs args)
        {           
            showClockFaceStoryboard.Begin(this, true);
        }        
    }
}

Im Folgenden finden Sie einige Details, die beachtet werden müssen.

  • Erstellen des Schaltflächenhandlers

            Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
                ' Change the status image and start the rotation animation.
                fetchButton.IsEnabled = False
                fetchButton.Content = "Contacting Server"
                weatherText.Text = ""
                hideWeatherImageStoryboard.Begin(Me)
    
                ' Start fetching the weather forecast asynchronously.
                Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)
    
                fetcher.BeginInvoke(Nothing, Nothing)
            End Sub
    
    private void ForecastButtonHandler(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        hideWeatherImageStoryboard.Begin(this);
    
        // Start fetching the weather forecast asynchronously.
        NoArgDelegate fetcher = new NoArgDelegate(
            this.FetchWeatherFromServer);
    
        fetcher.BeginInvoke(null, null);
    }
    

Wenn auf die Schaltfläche geklickt wird, wird die Uhrzeichnung angezeigt und animiert. Die Schaltfläche wird deaktiviert. Die FetchWeatherFromServer-Methode wird in einem neuen Thread aufgerufen, und nach der Rückkehr kann der Dispatcher Ereignisse verarbeiten, während auf den Abruf der Wettervorhersage gewartet wird.

  • Abrufen der Wettervorhersage

            Private Sub FetchWeatherFromServer()
                ' Simulate the delay from network access.
                Thread.Sleep(4000)
    
                ' Tried and true method for weather forecasting - random numbers.
                Dim rand As New Random()
                Dim weather As String
    
                If rand.Next(2) = 0 Then
                    weather = "rainy"
                Else
                    weather = "sunny"
                End If
    
                ' Schedule the update function in the UI thread.
                tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
            End Sub
    
    private void FetchWeatherFromServer()
    {
        // Simulate the delay from network access.
        Thread.Sleep(4000);              
    
        // Tried and true method for weather forecasting - random numbers.
        Random rand = new Random();
        String weather;
    
        if (rand.Next(2) == 0)
        {
            weather = "rainy";
        }
        else
        {
            weather = "sunny";
        }
    
        // Schedule the update function in the UI thread.
        tomorrowsWeather.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.Normal,
            new OneArgDelegate(UpdateUserInterface), 
            weather);
    }
    

Um dieses Beispiel möglichst einfach zu halten, enthält es keinen Netzwerkcode. Stattdessen wird die Verzögerung beim Netzwerkzugriff simuliert, indem der neue Thread für vier Sekunden in den Ruhezustand versetzt wird. In dieser Zeit wird der ursprüngliche UInthread weiter ausgeführt, und er reagiert auf Ereignisse. Um dies zu verdeutlichen, wird eine Animation weiter ausgeführt, und die Schaltflächen zum Minimieren und Maximieren funktionieren ebenfalls nach wie vor.

Wenn die Verzögerung beendet wird und die Wettervorhersagen zufällig ausgewählt wurden, ist es Zeit, das Ergebnis an den UInthread zu melden. Dazu wird ein Aufruf von UpdateUserInterface im UInthread mit dem Dispatcher dieses Threads geplant. Es wird eine Zeichenfolge übergeben, die das Wetter für diesen geplanten Methodenaufruf beschreibt.

  • Aktualisieren der UI

            Private Sub UpdateUserInterface(ByVal weather As String)
                'Set the weather image
                If weather = "sunny" Then
                    weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
                ElseIf weather = "rainy" Then
                    weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
                End If
    
                'Stop clock animation
                showClockFaceStoryboard.Stop(Me)
                hideClockFaceStoryboard.Begin(Me)
    
                'Update UI text
                fetchButton.IsEnabled = True
                fetchButton.Content = "Fetch Forecast"
                weatherText.Text = weather
            End Sub
    
    private void UpdateUserInterface(String weather)
    {    
        //Set the weather image
        if (weather == "sunny")
        {       
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "SunnyImageSource"];
        }
        else if (weather == "rainy")
        {
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "RainingImageSource"];
        }
    
        //Stop clock animation
        showClockFaceStoryboard.Stop(this);
        hideClockFaceStoryboard.Begin(this);
    
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;     
    }
    

Wenn der Dispatcher im UInthread Zeit hat, führt er den geplanten Aufruf von UpdateUserInterface aus. Diese Methode beendet die Uhranimation und wählt ein Bild aus, um das Wetter zu beschreiben. Dieses Bild wird angezeigt, und die Schaltfläche "fetch forecast" wird wiederhergestellt.

Mehrere Fenster, mehrere Threads

Einige WPF-Anwendungen erfordern mehrere Fenster der obersten Ebene. Es spricht absolut nichts dagegen, dass eine Kombination aus Thread und Dispatcher mehrere Fenster verwaltet, mitunter eignen sich mehrere Threads jedoch besser. Dies gilt umso mehr, wenn die Möglichkeit besteht, dass eines der Fenster den Thread für sich allein beansprucht.

Windows-Explorer arbeitet auf diese Weise. Jedes neue Explorer-Fenster gehört zum ursprünglichen Prozess, seine Erstellung wird jedoch von einem unabhängigen Thread gesteuert.

Webseiten können mithilfe eines WPF-Frame-Steuerelements angezeigt werden. Ein einfacher Internet Explorer-Ersatz kann leicht erstellt werden. Am Anfang steht eine wichtige Funktion: die Fähigkeit, ein neues Explorer-Fenster zu öffnen. Wenn der Benutzer auf die Schaltfläche "Neues Fenster" klickt, wird eine Kopie des Fensters in einem separaten Thread geöffnet. Auf diese Weise sperren Vorgänge mit langer Laufzeit oder Blockierungsvorgänge in einem der Fenster nicht alle anderen Fenster.

In Wirklichkeit besitzt das Webbrowsermodell ein eigenes kompliziertes Threadmodell. Es ist deshalb ausgewählt worden, weil die meisten Leser damit vertraut sein sollten.

Im folgenden Beispiel wird der Code gezeigt.

<Window x:Class="SDKSamples.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="MultiBrowse"
    Height="600" 
    Width="800"
    Loaded="OnLoaded"
    >
  <StackPanel Name="Stack" Orientation="Vertical">
    <StackPanel Orientation="Horizontal">
      <Button Content="New Window"
              Click="NewWindowHandler" />
      <TextBox Name="newLocation"
               Width="500" />
      <Button Content="GO!"
              Click="Browse" />
    </StackPanel>

    <Frame Name="placeHolder"
            Width="800"
            Height="550"></Frame>
  </StackPanel>
</Window>

Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Data
Imports System.Windows.Threading
Imports System.Threading


Namespace SDKSamples
    Partial Public Class Window1
        Inherits Window

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub OnLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
           placeHolder.Source = New Uri("https://www.msn.com")
        End Sub

        Private Sub Browse(ByVal sender As Object, ByVal e As RoutedEventArgs)
            placeHolder.Source = New Uri(newLocation.Text)
        End Sub

        Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
            newWindowThread.SetApartmentState(ApartmentState.STA)
            newWindowThread.IsBackground = True
            newWindowThread.Start()
        End Sub

        Private Sub ThreadStartingPoint()
            Dim tempWindow As New Window1()
            tempWindow.Show()
            System.Windows.Threading.Dispatcher.Run()
        End Sub
    End Class
End Namespace
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using System.Threading;


namespace SDKSamples
{
    public partial class Window1 : Window
    {

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
           placeHolder.Source = new Uri("https://www.msn.com");
        }

        private void Browse(object sender, RoutedEventArgs e)
        {
            placeHolder.Source = new Uri(newLocation.Text);
        }

        private void NewWindowHandler(object sender, RoutedEventArgs e)
        {       
            Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
            newWindowThread.SetApartmentState(ApartmentState.STA);
            newWindowThread.IsBackground = true;
            newWindowThread.Start();
        }

        private void ThreadStartingPoint()
        {
            Window1 tempWindow = new Window1();
            tempWindow.Show();       
            System.Windows.Threading.Dispatcher.Run();
        }
    }
}

Die folgenden Threadsegmente dieses Codes sind in diesem Kontext die interessantesten:

        Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
            newWindowThread.SetApartmentState(ApartmentState.STA)
            newWindowThread.IsBackground = True
            newWindowThread.Start()
        End Sub
private void NewWindowHandler(object sender, RoutedEventArgs e)
{       
    Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
    newWindowThread.SetApartmentState(ApartmentState.STA);
    newWindowThread.IsBackground = true;
    newWindowThread.Start();
}

Diese Methode wird aufgerufen, wenn auf die Schaltfläche "Neues Fenster" geklickt wird. Durch sie wird ein neuer Thread erstellt und asynchron gestartet.

        Private Sub ThreadStartingPoint()
            Dim tempWindow As New Window1()
            tempWindow.Show()
            System.Windows.Threading.Dispatcher.Run()
        End Sub
private void ThreadStartingPoint()
{
    Window1 tempWindow = new Window1();
    tempWindow.Show();       
    System.Windows.Threading.Dispatcher.Run();
}

Diese Methode ist der Ausgangspunkt für den neuen Thread. Unter dem Steuerelement dieses Threads wird ein neues Fenster erstellt. WPF erstellt automatisch einen neuen Dispatcher, um den neuen Thread zu verwalten. Damit das Fenster funktionsfähig ist, muss lediglich der Dispatcher gestartet werden.

Technische Details und Stolpersteine

Schreiben von Komponenten mithilfe von Threading

Im Microsoft .NET Framework-Entwicklerhandbuch wird ein Muster beschrieben, anhand dessen eine Komponente für ihre Clients asynchrones Verhalten verfügbar machen kann (siehe Übersicht über ereignisbasierte asynchrone Muster). Angenommen, die FetchWeatherFromServer-Methode soll in eine wiederverwendbare, nicht grafische Komponente gepackt werden. Nach dem Microsoft .NET Framework-Standardmuster würde dies ungefähr wie folgt aussehen.

    Public Class WeatherComponent
        Inherits Component
        'gets weather: Synchronous 
        Public Function GetWeather() As String
            Dim weather As String = ""

            'predict the weather

            Return weather
        End Function

        'get weather: Asynchronous 
        Public Sub GetWeatherAsync()
            'get the weather
        End Sub

        Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
    End Class

    Public Class GetWeatherCompletedEventArgs
        Inherits AsyncCompletedEventArgs
        Public Sub New(ByVal [error] As Exception, ByVal canceled As Boolean, ByVal userState As Object, ByVal weather As String)
            MyBase.New([error], canceled, userState)
            _weather = weather
        End Sub

        Public ReadOnly Property Weather() As String
            Get
                Return _weather
            End Get
        End Property
        Private _weather As String
    End Class

    Public Delegate Sub GetWeatherCompletedEventHandler(ByVal sender As Object, ByVal e As GetWeatherCompletedEventArgs)
public class WeatherComponent : Component
{
    //gets weather: Synchronous 
    public string GetWeather()
    {
        string weather = "";

        //predict the weather

        return weather;
    }

    //get weather: Asynchronous 
    public void GetWeatherAsync()
    {
        //get the weather
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
}

public class GetWeatherCompletedEventArgs : AsyncCompletedEventArgs
{
    public GetWeatherCompletedEventArgs(Exception error, bool canceled,
        object userState, string weather)
        :
        base(error, canceled, userState)
    {
        _weather = weather;
    }

    public string Weather
    {
        get { return _weather; }
    }
    private string _weather;
}

public delegate void GetWeatherCompletedEventHandler(object sender,
    GetWeatherCompletedEventArgs e);

GetWeatherAsync würde eine der zuvor beschriebenen Techniken verwenden, z. B. das Erstellen eines Hintergrundthreads, um die Arbeit asynchron auszuführen und den aufrufenden Thread nicht zu blockieren.

Einer der interessantesten Teile dieses Musters ist der Aufruf der MethodNameCompleted-Methode im selben Thread, der zu Beginn die MethodNameAsync-Methode aufgerufen hat. Dies kann sehr leicht mit WPF durch das Speichern von CurrentDispatcher erreicht werden, aber dann kann die nicht grafische Komponente nur in WPF-Anwendungen verwendet werden, nicht in Windows Forms-Programmen oder in ASP.NET-Programmen.

Für diese Anforderung wurde die DispatcherSynchronizationContext-Klasse konzipiert. Stellen Sie sich diese Klasse als vereinfachte Version von Dispatcher vor, die auch mit anderen UI-Frameworks funktioniert.

    Public Class WeatherComponent2
        Inherits Component
        Public Function GetWeather() As String
            Return fetchWeatherFromServer()
        End Function

        Private requestingContext As DispatcherSynchronizationContext = Nothing

        Public Sub GetWeatherAsync()
            If requestingContext IsNot Nothing Then
                Throw New InvalidOperationException("This component can only handle 1 async request at a time")
            End If

            requestingContext = CType(DispatcherSynchronizationContext.Current, DispatcherSynchronizationContext)

            Dim fetcher As New NoArgDelegate(AddressOf Me.fetchWeatherFromServer)

            ' Launch thread
            fetcher.BeginInvoke(Nothing, Nothing)
        End Sub

        Private Sub [RaiseEvent](ByVal e As GetWeatherCompletedEventArgs)
            RaiseEvent GetWeatherCompleted(Me, e)
        End Sub

        Private Function fetchWeatherFromServer() As String
            ' do stuff
            Dim weather As String = ""

            Dim e As New GetWeatherCompletedEventArgs(Nothing, False, Nothing, weather)

            Dim callback As New SendOrPostCallback(AddressOf DoEvent)
            requestingContext.Post(callback, e)
            requestingContext = Nothing

            Return e.Weather
        End Function

        Private Sub DoEvent(ByVal e As Object)
            'do stuff
        End Sub

        Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
        Public Delegate Function NoArgDelegate() As String
    End Class
public class WeatherComponent2 : Component
{
    public string GetWeather()
    {
        return fetchWeatherFromServer();
    }

    private DispatcherSynchronizationContext requestingContext = null;

    public void GetWeatherAsync()
    {
        if (requestingContext != null)
            throw new InvalidOperationException("This component can only handle 1 async request at a time");

        requestingContext = (DispatcherSynchronizationContext)DispatcherSynchronizationContext.Current;

        NoArgDelegate fetcher = new NoArgDelegate(this.fetchWeatherFromServer);

        // Launch thread
        fetcher.BeginInvoke(null, null);
    }

    private void RaiseEvent(GetWeatherCompletedEventArgs e)
    {
        if (GetWeatherCompleted != null)
            GetWeatherCompleted(this, e);
    }

    private string fetchWeatherFromServer()
    {
        // do stuff
        string weather = "";

        GetWeatherCompletedEventArgs e =
            new GetWeatherCompletedEventArgs(null, false, null, weather);

        SendOrPostCallback callback = new SendOrPostCallback(DoEvent);
        requestingContext.Post(callback, e);
        requestingContext = null;

        return e.Weather;
    }

    private void DoEvent(object e)
    {
        //do stuff
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
    public delegate string NoArgDelegate();
}

Geschachteltes Verschieben

Manchmal ist es nicht möglich, den UInthread vollständig zu sperren. Sehen Sie sich die Show-Methode der MessageBox-Klasse an. Show wird erst zurückgegeben, wenn der Benutzer auf die Schaltfläche "OK" klickt. Allerdings wird ein Fenster erstellt, das eine Meldungsschleife enthalten muss, um interaktiv zu sein. Während gewartet wird, dass der Benutzer auf "OK" klickt, reagiert das ursprüngliche Anwendungsfenster nicht auf Benutzereingaben. Es verarbeitet jedoch weiterhin Zeichenmeldungen. Wenn das ursprüngliche Fenster verdeckt und dann angezeigt wird, wird es aktualisiert. 

MessageBox mit einer Schaltfläche "OK"

Es muss einen Thread geben, der das Meldungsfenster steuert. WPF könnte nur für das Meldungsfenster einen neuen Thread erstellen, dieser Thread wäre jedoch nicht in der Lage, die deaktivierten Elemente im ursprünglichen Fenster zu zeichnen (der oben erwähnte gegenseitige Ausschlussmechanismus). Stattdessen verwendet WPF ein geschachteltes System zur Meldungsverarbeitung. Die Dispatcher-Klasse enthält eine spezielle Methode mit dem Namen PushFrame, die den aktuellen Ausführungspunkt einer Anwendung speichert und dann eine neue Meldungsschleife beginnt. Nach Beendigung der geschachtelten Meldungsschleife wird die Ausführung nach dem ursprünglichen PushFrame-Aufruf fortgesetzt.

In diesem Fall behält PushFrame den Programmkontext beim Aufruf von MessageBox.Show bei und beginnt eine neue Meldungsschleife, um das Hintergrundfenster neu zu zeichnen und Eingaben für das Meldungsfenster zu behandeln. Wenn der Benutzer auf OK klickt und das Popupfenster schließt, wird die geschachtelte Schleife beendet, und das Steuerelement wird nach dem Aufruf von Show fortgesetzt.

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. Nachdem handler2 beendet ist, wird das Ereignis an das Canvas-Objekt übergeben, das es mithilfe von handler1 verarbeitet. Dies geschieht nur, wenn handler2 das Ereignisobjekt nicht explizit als behandelt markiert.

Es kann sein, dass handler2 sehr viel Zeit für die Verarbeitung dieses Ereignisses benötigt. handler2 verwendet möglicherweise PushFrame, um eine geschachtelte Meldungsschleife zu beginnen, die stundenlang nicht zurückgegeben wird. Wenn handler2 das Ereignis nicht als behandelt markiert, nachdem die Meldungsschleife vollständig ist, wird das Ereignis in der Struktur nach oben weitergereicht, obwohl es sehr alt ist.

Reentranz und Sperrung

Der Sperrmechanismus der common language runtime (CLR) verhält sich nicht genau so wie anzunehmen wäre: Bei der Anforderung einer Sperre sollte ein Thread eigentlich alle Aktivitäten vollständig einstellen. Tatsächlich aber empfängt der Thread weiterhin Meldungen mit hoher Priorität und verarbeitet sie. Dadurch können zwar Deadlocks vermieden und eine minimale Reaktionsfähigkeit von Schnittstellen aufrechterhalten werden, es besteht aber auch die Möglichkeit, dass sich fast unmerkliche Fehler einschleichen. Obwohl dieses Verhalten meist keine Rolle spielt, kann es unter seltenen Umständen nützlich sein, dies zu wissen (in diesen Fällen sind in der Regel Win32-Fenstermeldungen oder COM-STA-Komponenten beteiligt).

Die meisten Schnittstellen werden nicht im Hinblick auf Threadsicherheit erstellt, da Entwickler bei ihrer Arbeit davon ausgehen, dass nie mehr als ein Thread auf eine UI zugreift. In diesem Fall führt dieser einzelne Thread vielleicht zu unerwarteten Zeitpunkten Änderungen an der Umgebung aus und verursacht dadurch die unschönen Effekte, die vom gegenseitigen Ausschlussmechanismus von DispatcherObject behoben werden sollen. Sehen Sie sich den folgenden Pseudocode an:

Diagramm des Verkettungswiedereintritts

Meistens ist dies genau richtig, aber manchmal kann es in WPF vorkommen, dass solche unerwartete Reentranz Probleme verursacht. Es kann also u. U. dazu kommen, dass DisableProcessing von WPF aufgerufen wird, wodurch die Sperranweisung für diesen Thread so geändert wird, dass anstelle der üblichen CLR-Sperre die WPF-Sperre ohne Reentranz verwendet wird. 

Warum also hat das CLR-Team dieses Verhalten gewählt? Der Grund dafür liegt in den COM-STA-Objekten und im Finalisierungsthread. Wenn ein Objekt an die Garbage Collection übergeben wird, wird seine Finalize-Methode im dedizierten Finalizerthread ausgeführt, nicht im UInthread. Und hierin liegt das Problem: Ein COM-STA-Objekt, das im UInthread erstellt wurde, kann nur im UInthread verworfen werden. Das Verhalten der CLR entspricht dem von BeginInvoke (in diesem Fall mit SendMessage von Win32). Wenn der UInthread aber ausgelastet ist, wird der Finalizerthread verzögert, und das COM-STA-Objekt kann nicht verworfen werden, sodass ein ernsthafter Speicherverlust entsteht. Deshalb hat das CLR-Team die Notbremse gezogen und die Sperren auf diese Weise eingesetzt.  

Die Aufgabe für WPF besteht darin, die unerwartete Reentranz zu verhindern, ohne den Speicherverlust wieder zuzulassen. Aus diesem Grund wird die Reentranz nicht überall blockiert.

Siehe auch

Weitere Ressourcen

Beispiel für eine Singlethread-Anwendung mit Berechnung mit langer Laufzeit