Threading-Modell

WPF-Entwickler müssen keine Schnittstelle schreiben, 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, wird kein UI-Framework in der Lage sein, 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 BeginInvoke-Methode für asynchroner Aufrufe benutzt. Sie können auch asynchrone Aufrufe vornehmen, indem Sie die InvokeAsync-Methode aufrufen, die einen Action oder Func<TResult> als Parameter übernehmen. Die InvokeAsync-Methode gibt einen DispatcherOperation oder DispatcherOperation<TResult> zurück, der eine Task-Eigenschaft aufweist. 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. Die Invoke-Methode hat auch Überladungen, die einen Action oder Func<TResult> als Parameter übernehmen. Sie können die Invoke-Methode verwenden, um synchrone Aufrufe zu machen, indem Sie einen Delegaten passieren, Action oder Func<TResult>.

Übersicht und Verteiler

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

So können UI-Threads frei sein, um Elemente in der Dispatcher-Warteschlange zu neigen. 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 zwei Methoden zum Registrieren von Arbeitselementen: Invoke und BeginInvoke. Beide 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. BeginInvoke ist asynchron und kehrt sofort zurück.

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. Detaillierte Informationen über DispatcherPriority Ebenen können in der Windows SDK-Dokumentation gefunden werden.

Threads in Aktion: Beispiele

Eine Singlethread-Anwendung 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. Der UI-Thread. Sie müssen deshalb sicherstellen, dass Sie in regelmäßigen Abständen zu Dispatcher zurückkehren, um ausstehende Eingabeereignisse zu verarbeiten, bevor sie veralten.

Betrachten Sie das folgenden Beispiel:

Screenshot that shows threading of prime numbers.

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 BeginInvoke-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 that shows the dispatcher queue.

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.

<Window x:Class="SDKSamples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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>

Im folgenden Beispiel wird das CodeBehind gezeigt.

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

Das folgende Beispiel zeigt den Ereignishandler für die Button.

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));
    }
}
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

Neben dem Aktualisieren des Textes auf Button ist dieser Handler auch für die Planung der ersten Primzahlüberprüfung durch Hinzufügen eines Delegaten in die Dispatcher-Warteschlange verantwortlich. Nachdem dieser Ereignishandler seine Arbeit abgeschlossen hat, wählt der Dispatcher diesen Delegaten nach einer Weile für die Ausführung aus.

Wie wir schon früher erwähnt haben, ist BeginInvoke 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.
    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;
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

Diese Methode überprüft, ob die nächste ungerade Zahl eine Primzahl ist. Wenn sie eine Primzahl ist, aktualisiert die Methode die bigPrimeTextBlock direkt entsprechend. Dies ist möglich, da die Berechnung im selben Thread ausgeführt wird, der für die Erstellung der Komponente 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.

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

Behandeln eines blockierenden Vorgangs mit einem Hintergrundthread

Die Behandlung von blockierenden Vorgängen in einer grafischen Anwendung kann schwierig sein. Wir wollen keine blockierenden Methoden von Ereignishandlern aufrufen, da die Anwendung sonst scheinbar einfriert. Wir können einen separaten Thread verwenden, um diese Vorgänge zu behandeln, aber nach dem Abschluss müssen wir eine Synchronisierung mit dem UI-Thread durchführen, da wir die GUI nicht direkt vom Arbeitsthread aus ändern können. Wir können Invoke oder BeginInvoke verwenden, um Delegaten in die Dispatcher des UI-Threads einzufügen. Schließlich werden diese Delegaten mit der Berechtigung zum Ändern von UI-Elementen ausgeführt.

In diesem Beispiel simulieren wir einen Remoteprozeduraufruf, der eine Wettervorhersage abruft. Wir verwenden einen separaten Arbeitsthread für die Ausführung dieses Aufrufs und planen eine Aktualisierungsmethode im Dispatcher des UI-Threads ein, wenn wir fertig sind.

Screenshot that shows the weather UI.

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);
        }
    }
}

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

Im Folgenden sind einige der Details aufgeführt, die beachtet werden sollten.

  • Erstellen des Schaltflächenhandlers

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

Wenn auf die Schaltfläche geklickt wird, wird die Uhr-Zeichnung angezeigt, und wir beginnen mit der Animation. Die Schaltfläche wird deaktiviert. Wir rufen die FetchWeatherFromServer-Methode in einem neuen Thread auf und kehren dann zurück, sodass Dispatcher Ereignisse verarbeiten kann, während wir auf die Wettervorhersage warten.

  • Abrufen der Wettervorhersage

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

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 Ereignisse. Um dies zu zeigen, führen wir eine Animation weiterhin aus, und die Schaltflächen „Minimieren“ und „Maximieren“ funktionieren ebenfalls weiterhin.

Wenn die Verzögerung beendet ist und wir die Wettervorhersage zufällig ausgewählt haben, ist es Zeit, an den UI-Thread zurückzumelden. Das machen wir mit der Planung eines Aufrufs an UpdateUserInterface im UI-Thread, indem wir die Dispatcher dieses Threads verwenden. Wir übergeben eine Zeichenfolge, die das Wetter beschreibt, an diesen eingeplanten Methodenaufruf.

  • Aktualisieren der UI

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

Wenn die Dispatcher im UI-Thread Zeit hat, führt sie den geplanten Aufruf von UpdateUserInterface aus. Diese Methode hält die Uhr-Animation an und wählt ein Bild aus, um das Wetter zu beschreiben. Sie zeigt dieses Bild an und stellt die Schaltfläche „fetch forecast“ (Wettervorhersage abrufen) wieder her.

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.

Indem wir ein WPFFrame-Steuerelement verwenden, können wir Webseiten anzeigen. Wir können problemlos einen einfachen Internet Explorer-Ersatz erstellen. Wir beginnen mit einer wichtigen Funktion, und zwar der Möglichkeit, ein neues Explorerfenster zu öffnen. Wenn der Benutzer auf die Schaltfläche „Neues Fenster“ klickt, starten wir eine Kopie des Fensters in einem separaten Thread. Auf diese Weise sperren lange andauernde oder blockierende Vorgänge in einem der Fenster nicht alle anderen Fenster.

In Wirklichkeit verfügt das Webbrowser-Modell über ein eigenes kompliziertes Threadingmodell. Wir haben uns dafür entschieden, da die meisten Leser damit vertraut sein sollten.

Im folgenden Beispiel wird der Code angezeigt.

<Window x:Class="SDKSamples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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>
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("http://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();
        }
    }
}

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("http://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

Die folgenden Threadingsegmente dieses Codes sind für uns in diesem Kontext am interessantesten:

private void NewWindowHandler(object sender, RoutedEventArgs e)
{
    Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
    newWindowThread.SetApartmentState(ApartmentState.STA);
    newWindowThread.IsBackground = true;
    newWindowThread.Start();
}
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

Diese Methode wird aufgerufen, wenn die Schaltfläche „Neues Fenster“ angeklickt wird. Sie erstellt einen neuen Thread und startet diesen asynchron.

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

Diese Methode ist der Ausgangspunkt für den neuen Thread. Wir erstellen ein neues Fenster unter der Kontrolle dieses Threads. WPF erstellt automatisch ein neues Dispatcher, um den neuen Thread zu verwalten. Wir müssen nur Dispatcher starten, um das Fenster funktionsfähig zu machen.

Technische Details und Stolpersteine

Schreiben von Komponenten mithilfe von Threading

Das Microsoft .NET-Framework-Entwicklerhandbuch beschreibt ein Muster, wie eine Komponente asynchrones Verhalten für ihre Clients verfügbar machen kann (siehe Übersicht über das ereignisbasierte asynchrone Muster). Nehmen wir beispielsweise an, dass wir die FetchWeatherFromServer-Methode in eine wiederverwendbare, nichtgrafische Komponente verpacken möchten. Nach dem Standardmuster von Microsoft .NET-Framework würde dies ungefähr wie im Folgenden aussehen.

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

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

Einer der wichtigsten Teile dieses Musters ist der Aufruf der MethodNameCompleted-Methode auf dem gleichen Thread, der am Anfang die MethodNameAsync-Methode aufgerufen hat. Sie könnten dies mit WPF relativ einfach erreichen, indem Sie CurrentDispatcher speichern - aber dann könnte die nichtgrafische Komponente nur in WPF-Anwendungen und nicht in Windows Forms- oder ASP.NET-Programmen verwendet werden.

Die DispatcherSynchronizationContext-Klasse behandeln diesen Bedarf - denken Sie sich eine vereinfachte Version von Dispatcher, die auch mit anderen UI-Frameworks funktionieren.

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();
}
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

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 die OK-Schaltfläche anklickt. 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 that shows a MessageBox with an OK button

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 sich vorstellen könnte; 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:

Diagram that shows threading reentrancy.

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