TOC

This article has been localized into German by the community.

Sonstiges (miscellaneous):

Multithreading mit dem BackgroundWorker

Standardmäßig wird jedes Mal, wenn deine Applikation (Anwendung) ein Stück Code abarbeitet, dieser Code auf demselben Thread (sequentiellen Abarbeitungsstrang) wie die Applikation selbst ausgeführt. Das bedeutet, dass in der Applikation währenddessen nichts anderes passiert und auch kein Update deiner Benutzeroberfläche (UI) vorgenommen wird.

Dies mag eine ziemliche Überraschung sein für Leute, die neu in der Windows-Programmierung sind, wenn sie das erste Mal etwas starten, was länger als eine Sekunde andauert und sie feststellen, dass ihre Anwendung in der Zwischenzeit hängt. Das Ergebnis sind eine Menge frustrierte Forenbeiträge von Leuten, die versuchen, den Fortschritt eines länger andauernden Prozesses mit der Hilfe eines ProgressBar Steuerelementes sichtbar zu machen, nur um zu erkennen, dass der Fortschrittsbalken nicht aktualisiert wird, solange der Prozess andauert.

Die Lösung für dieses Problem ist der Gebrauch von mehreren Threads( multiple threads) und C# macht das dem Programmierer auch relativ einfach, aber es sind dabei eine Menge Fallstricke zu beachten. Hier kommt jetzt der BackgroundWorker ins Spiel. Er erlaubt es dir, schnell und einfach mit einem zusätzlichen Thread in deiner Applikation zu arbeiten.

Wie der BackgroundWorker arbeitet

Das schwierigste Konzept in einer Windows-Anwendung in Bezug auf Multi-Threading ist die Tatsache, dass es dir nicht erlaubt ist, Änderungen an der UI von einem anderen Thread vornehmen zu lassen. Wenn du das versuchst, wird die Anwendung auf der Stelle abstürzen. Stattdessen musst du eine Methode auf dem UI-(Haupt-) Thread aufrufen, die dann die gewünschten Änderungen vornimmt. Dies ist ein wenig umständlich, aber nicht, wenn du den BackgroundWorker benutzt.

Wenn du eine Aufgabe (task) auf einem anderen Thread ausführst, musst du üblicherweise in zwei Situationen mit dem Rest der Anwendung kommunizieren : Wenn du mit einem Update anzeigen willst, wie weit der Prozess fortgeschritten ist und dann , natürlich, wenn der Prozess beendet ist und du das Ergebnis anzeigen willst. Der BackgroundWorker basiert auf dieser Idee und stellt hierfür zwei Ereignisse ( events) zur Verfügung : ProgressChanged undRunWorkerCompleted.

Das dritte Ereignis heißt DoWork und die allgemeine Regel besagt, dass du nichts in dem UI von diesem Ereignis aus ändern darfst. Stattdessen rufst du die ReportProgress() Methode auf, die wiederum das Ereignis ProgressChanged auslöst, von dem aus du das UI aktualisieren kannst. Ist der Prozess beendet, weist du dem worker ( Der Instanz der Klasse BackgroundWorker) ein Endergebnis zu worauf das Ereignis RunWorkerCompleted ausgelöst wird.

So, um es zusammenzufassen, das DoWork Ereignis kümmert sich um all die "harte Arbeit". Der gesamte Code dort wird auf einem anderen Thread ausgeführt und aus diesem Grund darfst du das UI von hier aus nicht anfassen. Stattdessen bringst du Daten( von dem UI oder von woanders) in das DoWork-Ereignis, indem du das Argument der RunWorkerAsync() Methode benutzt, und die resultierenden Daten aus dem Ereignis hinaus, indem du sie der e.Result Eigenschaft (property) zuweist.

Die ProgressChanged und RunWorkerCompleted Ereignisse, auf der anderen Seite, werden auf demselben Thread ausgeführt, auf dem auch der BackgroundWorker erzeugt wird, welches üblicherweise der Haupt-(UI-) Thread sein wird und aus diesem Grund ist es dir erlaubt, das UI von hier aus auf den aktuellen Stand zu bringen. Die einzige Kommunikation zwischen dem laufenden Hintergrund-Task und der Benutzeroberfläche ist nur über die Methode ReportProgress() möglich.

Das war eine Menge Theorie, aber obwohl der BackgroundWorker einfach zu benutzen ist, ist es wichtig zu verstehen was er und wie er es tut, so dass du nicht aus Versehen etwas falsch machst - wie bereits erwähnt, Fehler beim Multithreading können zu unangenehmen Problemen führen.

Ein BackgroundWorker Beispiel

Genug mit der Theorie - lasst uns sehen, worum es geht. In diesem ersten Beispiel wollen wir einen einfachen, aber viel Zeit verbrauchenden Job ausgeführt haben. Jede Zahl zwischen 0 und 10000 soll daraufhin getestet werden, ob sie durch die Zahl 7 teilbar ist. Dies ist für die schnellen Computer von heute natürlich ein Kinderspiel, und so habe ich , um den Zeitverbrauch zu erhöhen und unseren Punkt klarer zumachen, in jede Iteration eine Verzögerung von einer Millisekunde eingebaut.

Unsere Beispiel-Applikation hat zwei Buttons : Einer, der unsere Aufgabe synchron ( auf dem gleichen Thread) und einer, der unsere Aufgabe mit einem BackgroundWorker und damit auf einem anderen Thread ausführen wird. Dies sollte es sehr einfach machen , die Notwendigkeit für einen extra Thread bei zeitintensiven Aufgaben zu erkennen. Der Code sieht so aus :

<Window x:Class="WpfTutorialSamples.Misc.BackgroundWorkerSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="BackgroundWorkerSample" Height="300" Width="375">
    <DockPanel Margin="10">
        <DockPanel DockPanel.Dock="Top">
            <Button Name="btnDoSynchronousCalculation" Click="btnDoSynchronousCalculation_Click" DockPanel.Dock="Left" HorizontalAlignment="Left">Synchronous (same thread)</Button>
            <Button Name="btnDoAsynchronousCalculation" Click="btnDoAsynchronousCalculation_Click" DockPanel.Dock="Right" HorizontalAlignment="Right">Asynchronous (worker thread)</Button>
        </DockPanel>
        <ProgressBar DockPanel.Dock="Bottom" Height="18" Name="pbCalculationProgress" />

        <ListBox Name="lbResults" Margin="0,10" />

    </DockPanel>
</Window>
using System;
using System.ComponentModel;
using System.Windows;

namespace WpfTutorialSamples.Misc
{
	public partial class BackgroundWorkerSample : Window
	{
		public BackgroundWorkerSample()
		{
			InitializeComponent();
		}

		private void btnDoSynchronousCalculation_Click(object sender, RoutedEventArgs e)
		{
			int max = 10000;
			pbCalculationProgress.Value = 0;
			lbResults.Items.Clear();

			int result = 0;
			for(int i = 0; i < max; i++)
			{
				if(i % 42 == 0)
				{
					lbResults.Items.Add(i);
					result++;
				}
				System.Threading.Thread.Sleep(1);
				pbCalculationProgress.Value = Convert.ToInt32(((double)i / max) * 100);
			}
			MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + result);
		}

		private void btnDoAsynchronousCalculation_Click(object sender, RoutedEventArgs e)
		{
			pbCalculationProgress.Value = 0;
			lbResults.Items.Clear();

			BackgroundWorker worker = new BackgroundWorker();
			worker.WorkerReportsProgress = true;
			worker.DoWork += worker_DoWork;
			worker.ProgressChanged += worker_ProgressChanged;
			worker.RunWorkerCompleted += worker_RunWorkerCompleted;
			worker.RunWorkerAsync(10000);
		}

		void worker_DoWork(object sender, DoWorkEventArgs e)
		{
			int max = (int)e.Argument;
			int result = 0;
			for(int i = 0; i < max; i++)
			{
				int progressPercentage = Convert.ToInt32(((double)i / max) * 100);
				if(i % 42 == 0)
				{
					result++;
					(sender as BackgroundWorker).ReportProgress(progressPercentage, i);
				}
				else
					(sender as BackgroundWorker).ReportProgress(progressPercentage);
				System.Threading.Thread.Sleep(1);

			}
			e.Result = result;
		}

		void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
		{
			pbCalculationProgress.Value = e.ProgressPercentage;
			if(e.UserState != null)
				lbResults.Items.Add(e.UserState);
		}

		void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + e.Result);
		}

	}
}

Der XAML-Teil besteht aus einem Paar Buttons, einer für die synchrone Ausführung des Prozesses ( auf dem UI Thread) und einer für die asynchrone Ausführung ( auf einem Hintergrund Thread), dazu einem ListBox-Kontrollelement, um die ausgerechneten Zahlen zu zeigen und ein ProgressBar Kontrollelement ganz am unteren Ende des Fensters, um den Fortschritt anzuzeigen.

Im Code-behind fangen wir an mit der synchronen Ereignisbehandlung. Wie erwähnt, geht die Schleife von 0-10000 mit einer kleinen Verzögerung in jeder Iteration, und wenn eine Zahl durch die Zahl 7 teilbar ist, fügen wir sie der Liste hinzu. In jeder Iteration machen wir auch einen Update der ProgressBar, und wenn wir fertig sind, zeigen wir dem User in einer Nachricht, wie viele Zahlen gefunden wurden.

Wenn du die Applikation startest und den ersten Knopf drückst, wird es so wie unten im Bild aussehen, egal wie weit der Prozess fortgeschritten ist.

Keine Einträge in der Liste, keine Anzeige des Fortschritts auf der ProgressBar, und der Button wurde noch nicht einmal entsperrt, was beweist, dass es nicht ein einziges Update bei der UI gab , seit die Maus über dem Button gedrückt wurde.

Wenn stattdessen der zweite Button gedrückt wird, wird die Herangehensweise mit dem BackgroundWorker gewählt. Wie wir aus dem Code ersehen können, machen wir so ziemlich das Gleiche, nur auf eine leicht unterschiedliche Art. All die harte(zeitaufwendige) Arbeit ist nun in das DoWork -Ereignis gepackt, welches der Worker aufruft, nachdem du die RunWorkerAsync() Methode startest. Diese Methode übernimmt Input von deiner Applikation, welche von dem Worker genutzt werden kann, worauf wir später noch eingehen werden.

Wie bereits erwähnt, ist es uns nicht gestattet, die UI vom DoWork-Ereignis aus zu updaten. Stattdessen rufen wir die ReportProgress Methode des worker auf. Wenn die laufende Zahl durch 7 teilbar ist, planen wir sie ein, um sie der Liste hinzuzufügen - im anderen Falle übermitteln wir nur den laufenden Fortschritt des Prozesses in Prozenten, damit die ProgressBar aktualisiert werden kann.

Sind erst einmal alle Zahlen getestet, weisen wir das Endergebnis der e.Result Eigenschaft zu. Dieses wird weitergegeben an das RunWorkerCompleted Ereignis, wo wir es dem Benutzer zeigen. Dies mag etwas umständlich aussehen, statt es sofort dem User zu zeigen, sobald der Prozess beendet ist, aber noch einmal, so ist es sicher, dass wir nicht vom DoWork -Ereignis aus mit der Benutzeroberfläche kommunizieren, was uns nicht erlaubt ist.

Das Ergebnis ist, wie man sieht, wesentlich benutzerfreundlicher.

Das Fenster "hängt" nicht länger, der Button ist gedrückt aber nicht unterdrückt, die Liste der ermittelten Zahlen wird zeitnah aktualisiert - das Interface gibt dem Benutzer jetzt eine ganze Menge Informationen über den Fortgang des Prozesses.

Eingabe ( Input) und Ausgabe ( Output)

Beachte, dass sowohl der Input in Gestalt des an die Methode RunWorkerAsync() übergebenen Arguments, als auch der Output in Gestalt des Wertes, den wir der e.Result Eigenschaft zuweisen, vom Type object sind. Das bedeutet, dass wir jede beliebige Art von Wert ( value) zuweisen können. Unser Beispiel war elementar, da sowohl Input als auch Output mit einem einzigen Integer - Wert dargestellt werden konnten , aber es ist normal, komplexeren Input und Output zu haben.

Dies wird erreicht durch die Verwendung eines komplexeren Typs , in vielen Fällen ein struct oder sogar eine Klasse , die du selbst erstellst und weitergibst. Dadurch sind die Möglichkeiten nahezu unbegrenzt und du kannst so beliebig viele und beliebig komplexe Daten zwischen deinem BackgroundWorker und deiner Applikation/UI transportieren.

Dasselbe gilt auch für die ReportProgress Methode. Das zweites Argument dieser Methode heißt userState und hat den Type object, was bedeutet, dass du der ProgressChanged Methode übergeben kannst, was immer du willst.

Zusammenfassung

Der BackgroundWorker ist ein ausgezeichnetes Werkzeug, wenn du Multi-Threading in deiner Applikation haben willst, hauptsächlich, weil es so einfach anzuwenden ist. In diesem Kapitel haben wir uns eine Sache angeschaut, die mit dem BackgroundWorker sehr einfach zu machen ist, und zwar die Fortschrittsmeldung (progress reporting), aber auch die Unterstützung für den Abbruch (cancelling) eines laufenden Tasks ist sehr praktisch. Das untersuchen wir im nächsten Kapitel.


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!