Threading-Modell

Aktualisiert: November 2007

Windows Presentation Foundation (WPF) dient dazu, Entwicklern die Schwierigkeiten des Threadings zu ersparen. Daher müssen die meisten WPF-Entwickler keine Schnittstelle schreiben, die mehr als einen Thread verwendet. Da Multithread-Programme komplex und schwierig zu debuggen sind, sollten sie vermieden werden, wenn Singlethread-Lösungen zur Verfügung stehen.

Aber kein Benutzeroberfläche-Framework, unabhängig wie gut dessen Architektur ist, kann eine Singlethread-Lösung für jedes Problem bieten. WPF kommt dem Ideal zwar nahe, dennoch gibt es immer noch Situationen, in denen mehrere Threads die Reaktionsgeschwindigkeit von Benutzeroberfläche (user interface, UI) oder die Leistung der Anwendung verbessern. Nach der Erläuterung von Hintergrundmaterial werden in diesem Beitrag einige der 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

Normalerweise beginnen WPF-Anwendungen mit zwei Threads: einem für die Behandlung des Renderings und einem weiteren für die Verwaltung der Benutzeroberfläche. Der Rendering-Thread wird effektiv verborgen im Hintergrund ausgeführt, während der Benutzeroberfläche-Thread Eingaben empfängt, Ereignisse behandelt, den Bildschirm zeichnet und Anwendungscode ausführt. Die meisten Anwendungen verwenden einen einzelnen Benutzeroberfläche-Thread, obwohl es in einigen Situationen am besten ist, mehrere Threads zu verwenden. Dies wird später anhand eines Beispiels verdeutlicht.

Der Benutzeroberfläche-Thread stellt Arbeitsaufgaben in einem Objekt namens Dispatcher in die Warteschlange. Der Dispatcher wählt Arbeitsaufgaben nach Priorität aus und führt jede vollständig aus.  Jeder Benutzeroberfläche-Thread 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 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? Üblicherweise wird in einem solchen Fall der umfangreiche Vorgang in einen separaten Thread ausgelagert, sodass der Benutzeroberfläche-Thread für Elemente in der Dispatcher-Warteschlange zur Verfügung steht. Nachdem der umfangreiche Vorgang abgeschlossen ist, kann er sein Ergebnis an den Benutzeroberfläche-Thread zur Anzeige zurückübermitteln.

Traditionell ermöglicht Windows nur dem Thread den Zugriff auf die Benutzeroberfläche-Elemente, der sie erstellt hat. Das bedeutet, dass ein Hintergrundthread, der für eine Aufgabe mit langer Laufzeit verantwortlich ist, ein Textfeld nicht aktualisieren kann, wenn es vollständig ist. Diese Aufgabe übernimmt Windows, um die Integrität von Benutzeroberfläche-Komponenten sicherzustellen. 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. Bei der Konstruktion speichert ein DispatcherObject einen Verweis auf den Dispatcher, der mit dem gerade ausgeführten Thread verknüpft ist. Praktisch wird das DispatcherObject dem Thread zugeordnet, der es erstellt. Während der Programmausführung kann ein DispatcherObject seine öffentliche VerifyAccess-Methode aufrufen. VerifyAccess untersucht den Dispatcher, der dem aktuellen Thread zugewiesen ist und vergleicht ihn mit dem Dispatcher-Verweis, der während der Konstruktion gespeichert wurde. Wenn sie nicht übereinstimmen, löst VerifyAccess eine Ausnahme aus. VerifyAccess soll am Anfang jeder Methode aufgerufen werden, die zu einem DispatcherObject gehört.

Wenn nur ein Thread die Benutzeroberfläche ändern kann, wie interagieren dann Hintergrundthreads mit dem Benutzer? Ein Hintergrundthread kann den Benutzeroberfläche-Thread bitten, einen Vorgang in seinem Namen auszuführen. Dies wird erreicht, indem eine Arbeitsaufgabe beim Dispatcher des Benutzeroberfläche-Threads registriert wird. 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 wird nicht zurückgegeben, bis der Benutzeroberfläche-Thread tatsächlich die Ausführung des Delegaten beendet hat. BeginInvoke ist asynchron und wird sofort zurückgegeben.

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 graphische Benutzeroberflächen (graphical user interfaces, GUIs) verbringen einen Großteil ihrer Zeit im Leerlauf, während sie auf Ereignisse warten, die als Reaktion auf Benutzerinteraktionen generiert werden. Bei sorgfältiger Programmierung lässt sich diese Leerlaufzeit konstruktiv verwenden, ohne die Reaktionsgeschwindigkeit von Benutzeroberfläche zu beeinflussen. Das WPF-Threadmodell lässt nicht zu, dass Eingaben einen Vorgang im Benutzeroberfläche-Thread unterbrechen. Sie müssen also darauf achten, regelmäßig zum Dispatcher zurückzukehren, um ausstehende Eingabeereignisse zu verarbeiten, bevor sie veralten.

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 Click-Ereignishandlers der Schaltfläche behandelt würde, erhielte der Benutzeroberfläche-Thread nie die Möglichkeit, andere Ereignisse zu behandeln. Die Benutzeroberfläche könnte nicht auf Eingaben reagieren oder Meldungen verarbeiten. Sie würde nie neu gezeichnet werden und nie auf Klicks auf Schaltflächen 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 Primzahlenüberprüfungen in derselben Warteschlange planen, aus der Benutzeroberfläche-Ereignisse stammen. Im Beispiel wird jeweils nur eine einzelne Primzahlenüberprüfung geplant. Nach Abschluss der Primzahlenüberprüfung wird sofort die nächste Überprüfung geplant. Diese Prüfung wird nur ausgeführt, nachdem ausstehende Benutzeroberfläche-Ereignisse 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 Benutzeroberfläche-Threads 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>

Das folgende Beispiel zeigt den Code-Behind.

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 ture.
                    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 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 Benutzeroberfläche 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 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 ture.
            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 Benutzeroberfläche-Thread 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 Benutzeroberfläche-Thread erfolgen, da die GUI nicht direkt vom Workerthread aus geändert werden kann. Mithilfe von Invoke oder von BeginInvoke können Delegaten in den Dispatcher des Benutzeroberfläche-Threads eingefügt werden. Letztlich werden diese Delegaten mit der Berechtigung ausgeführt, Benutzeroberfläche-Elemente zu ändern.

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

Bildschirmabbildung der Wetter-Benutzeroberfläche

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 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 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 Benutzeroberfläche-Thread weiter ausgeführt und reagiert auf Ereignisse. Um dies zu verdeutlichen, wird eine Animation weiter ausgeführt, und die Schaltflächen zum Minimieren und Maximieren funktionieren auch nach wie vor.

  • Aktualisieren der Benutzeroberfläche

    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 Benutzeroberfläche-Thread 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" wiederhergestellt.

Den vollständigen Quellcode für dieses Beispiel finden Sie unter Beispiel zu Wetterdienstsimulation über einen Verteiler.

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, aber mitunter eignen sich mehrere Threads besser. Das gilt umso mehr, wenn die Möglichkeit besteht, dass eines der Fenster den Thread für sich allein beansprucht.

Der Windows-Explorer arbeitet auf diese Weise. Jedes neue Explorer-Fenster gehört zum ursprünglichen Prozess, doch dessen Erstellung wird 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 ein wichtiges Feature: 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>
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 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 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.

Den vollständigen Quellcode für dieses Beispiel finden Sie unter Beispiel für einen Multithreading-Webbrowser.

Technische Details und Stolpersteine

Schreiben von Komponenten mithilfe von Threading

Im Microsoft .NET Framework-Entwicklerhandbuch wird ein Muster beschrieben, wie 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 folgendermaßen 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);

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 Benutzeroberfläche-Frameworks funktioniert.

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 Benutzeroberfläche-Thread 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 haben 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, zeichnet es sich neu. 

MessageBox mit einer Schaltfläche "OK"

Es muss einen Thread geben, der das Meldungsfenster steuert. WPF kann einen neuen Thread nur für das Meldungsfenster erstellen, aber dieser Thread kann die deaktivierten Elemente im ursprünglichen Fenster nicht zeichnen (denken Sie an die Erläuterung zum gegenseitigen Ausschluss weiter oben). Stattdessen verwendet WPF ein geschachteltes System zur Meldungsverarbeitung. Die Dispatcher-Klasse enthält eine besondere 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 von common language runtime (CLR) verhält sich nicht genau so wie anzunehmen wäre: Bei der Anforderung einer Sperre sollte ein Thread eigentlich alle Tätigkeiten vollständig einstellen. Tatsächlich aber empfängt der Thread weiterhin Meldungen mit hoher Priorität und verarbeitet sie. Dies hilft, Deadlocks zu vermeiden und eine minimale Reaktionsfähigkeit von Schnittstellen zu erhalten, allerdings können sich dadurch auch unscheinbare Fehler einschleichen.  Meist spielt dieses Verhalten keine Rolle, aber unter seltenen Umständen (wobei in der Regel Win32-Fenstermeldungen oder COM-STA-Komponenten beteiligt sind) kann es nützlich sein, dies zu wissen.

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 Benutzeroberfläche zugreift. In diesem Fall führt dieser einzelne Thread vielleicht zu unerwarteten Zeitpunkten Änderungen an der Umgebung aus und verursacht damit die unschönen Effekte, die der gegenseitige Ausschlussmechanismus von DispatcherObject beheben soll. Betrachten Sie den folgenden Pseudocode:

Diagramm des Verkettungswiedereintritts

Meistens ist dies genau richtig, aber manchmal kann es in WPF vorkommen, dass solche unerwartete Reentranz richtige Probleme verursacht. Es kann also unter Umständen dazu kommen, dass DisableProcessing von WPF aufgerufen wird, wodurch die Sperranweisung für diesen Thread so geändert wird, dass statt 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 dessen Finalize-Methode im dedizierten Finalizerthread ausgeführt, nicht im Benutzeroberfläche-Thread. Und hierin liegt das Problem, da ein COM-STA-Objekt, das im Benutzeroberfläche-Thread erstellt wurde, nur im Benutzeroberfläche-Thread verworfen werden kann. CLR leistet eine Entsprechung von BeginInvoke (in diesem Fall die Verwendung von Win32-SendMessage). Aber wenn der Benutzeroberfläche-Thread 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 dafür eingesetzt, wozu sie vorgesehen sind.  

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

Aufgaben

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

Beispiel zu Wetterdienstsimulation über einen Verteiler

Beispiel für einen Multithreading-Webbrowser