Überraschend einfache Thread-Programmierung in C# mittels der BackgroundWorker-Komponente
Der Artikel beschreibt eine einfache Möglichkeit, wie laufzeitintensive Prozesse im Hintergrund ausgeführt werden können. Mittels C# und der BackgroundWorker-Komponente aus dem .NET-2.0-Framework gelingt es, den Vorgang der Thread-Programmierung zu entzaubern und damit gewissermaßen zu standardisieren.
Trotz der enormen Entwicklungen im Bereich der Hardware sind
gelegentlich laufzeitintensive Algorithmen durch eine Software
auszuführen. Hierzu gehören beispielsweise
aufwändige
Berechnungen, Suchoperationen, Downloads, Datenbankoperationen oder
Optimierungsprozesse. Insbesondere in Anwendungen, wo mathematische
Näherungsverfahren zum Einsatz gelangen, ist eine Vielzahl von
sich wiederholenden Verarbeitungsschritten (z.B. durch rekursive
Algorithmen) auszuführen. Startet man ohne weitere
Vorkehrungen
derartige Algorithmen, so ist das Anwendungsprogramm nicht mehr
für weitere Benutzeranforderungen reaktionsfähig.
Dieser
Zustand hält dann so lange an, bis die betreffende Prozedur
vollständig abgearbeitet wurde und diese die Kontrolle an das
aufrufende Programm zurückgegeben hat. Misslich ist die
beschriebene Situation auch dahingehend, dass ein vorzeitiges Abbrechen
des Vorgangs im Regelfall nicht möglich ist, da keine
Interaktion
über die Benutzeroberfläche zwischen Programm und
Benutzer
erfolgen kann. So mancher (ungeduldiger) Nutzer scheint in dieser
Situation geneigt, das Programm über den Taskmanager gewaltsam
abzubrechen, da er einen Softwarefehler (Programmabsturz) vermutet.
Eine verbesserte Lösung für das beschriebene Szenario
sieht
beispielsweise wie folgt aus: Nachdem der betreffende Prozess durch den
Anwender (bzw. durch die Software selbst) angestoßen wurde,
erhält der Benutzer darüber eine Information
(Meldung,
Statuszeile usw.). Darüber hinaus wird jederzeit die
Möglichkeit geboten, den Vorgang abzubrechen (Abbrechen-Button).
Letzteres sollte aus zwei Gründen realisiert werden: Zum einen
wird dem Anwender vermittelt, jederzeit die vollständige
Kontrolle
über die Vorgänge zu haben, und zum anderen ist auch
nur so
ein Beenden des Programms auf legale Weise möglich. Der
eigentliche Prozess wird dann scheinbar im Hintergrund
ausgeführt,
d.h. die Interaktionsfähigkeit wird an das aufrufende Programm
(und somit auch an die Benutzerschnittstelle) zurückgegeben.
Ist
es gewünscht, kann in Intervallen der Bearbeitungsstand der
Hintergrundaktivität an das Hauptprogramm gemeldet werden. Als
Darstellungsform bietet sich beispielsweise ein Fortschrittsbalken an.
Diese Vorgehensweise ist vom theoretischen Ansatz her nicht neu und
wird auch in professioneller Software angewendet. Das Stichwort zur
Thematik lautet Thread-Programmierung
und meint die Realisierung mehrerer nebenläufiger Prozesse.
Deren
programmiertechnische Umsetzung ist – unabhängig von
der
gewählten Programmiersprache – oftmals nicht das
Lieblingsthema des Softwareentwicklers. Es erfordert einiges an
Spezialwissen über das Betriebssystem und dessen
Schnittstellen.
Einige Hintergrundinformationen zu Threads und Prozessen und zwei
Literaturverweise liefert die Textbox „Threads und
Prozesse“.
Getreu dem Motto „Neu und besser“ findet sich im
.NET 2.0
Framework ein komfortabler Lösungsvorschlag für das
Problem.
Gemeint ist die (nichtvisuelle) Komponente BackgroundWorker.
Der Einsatz dieses Bausteins mittels der Programmiersprache C# wird
nachfolgend dargestellt. Verwendet man Microsofts Visual Studio, so
kann aus der Toolpalette das Symbol BackgroundWorker
ausgewählt und auf das Formular gezogen werden.
Selbstverständlich kann eine Instanz der Komponente auch
manuell
über den Quellcode erzeugt werden. Mit diesem unscheinbaren
Schritt hat man sozusagen im Vorbeigehen einen Großteil der
Arbeit erledigt. Der Vorteil wieder verwendbarer Software besteht
darin, dass jetzt nur noch die Anpassung an die programmindividuellen
Gegebenheiten stattfinden muss. Man muss sich keine Gedanken
über
die Anmeldung eines neuen Threads im Betriebssystem, über
dessen
Verwaltung und Synchronisation oder Beendigung machen.
Sämtliche
Funktionalität ist als „Blackbox“
verfügbar.
Neben einer deutlich vereinfachten Implementierbarkeit, lassen sich
hierüber die Laufzeitsicherheit und damit die
Qualität der
Software erhöhen. Der Entwickler sollte als Erstes denjenigen
(laufzeitintensiven) Prozess ermitteln, welcher nebenläufig
ausgeführt werden soll. An dieser Stelle sei ein Tipp aus der
Praxis erlaubt: Der betreffende Quellcode des künftigen
Hintergrundprozesses sollte vor der Integration in die Komponente
ausführlich getestet werden. Grund dafür ist, dass
das
Debuggen eines Hintergrundprozesses zwar grundsätzlich
möglich ist, jedoch fällt es dabei deutlich schwerer
den
Überblick zu behalten. Die Schnittstelle des
Hintergrundprozesses
ist nach dessen Implementierung und Test an die besonderen
Erfordernisse der BackgroundWorker-Komponente
anzupassen. Hierzu sind nur geringfügige Modifikationen
notwendig.
… etwas Theorie
Nachfolgend wird auf die Einzelheiten zum Einsatz der Komponente
eingegangen. Ein Studium der Online-Dokumentation [3] und speziell der Member
(Eigenschaften, Methoden, Ereignisse) liefert die relevanten
Informationen. Tabelle 1 gibt einen Überblick und eine
Erklärung der wesentlichen Member der BackgroundWorker-Komponente.

Tabelle 1: Überblick über die wesentlichen Member der BackgroundWorker-Komponente
… ganz konkret
Nach der Vorstellung der Theorie soll die konkrete Vorgehensweise an
einem Beispiel erläutert werden. Das Projekt der
Beispielapplikation ist als Download von der Webseite des Verlages
verfügbar [4]. Um die Angelegenheit einfach und
nachvollziehbar zu
gestalten, wurde folgende Vorgehensweise gewählt: Als
laufzeitintensive Aktivität (hier als Hintergrundprozess
bezeichnet) wurde lediglich eine Schleife erstellt, deren alleinige
Aufgabe darin besteht, eine Pause von jeweils einer Sekunde
durchzuführen. Diese Schleife wird zehnmal durchlaufen, sodass
eine Gesamtverzögerung von 10 Sekunden entsteht. An der Stelle
dieses (hier sinnlosen Vorganges) tritt dann später die
konkrete
laufzeitintensive Prozedur.
private void AufwaendigerVorgang1()
{
for (int i = 0; i < 10; i++)
Thread.Sleep(1000);
}
}
Wird dieser Hintergrundprozess aus der Anwendung ohne weitere
Vorkehrungen aufgerufen, so kann die Anwendung 10 Sekunden auf keine
weiteren Ereignisse reagieren. Diesen Zustand gilt es entsprechend zu
verbessern. Im
DoWork-Ereignis
der
BackgroundWorker-Komponente
wird der Aufruf der Hintergrundaktivität vorgenommen. Dazu
wird eine neue Instanz der Klasse
BackgroundWorker
erzeugt und diese als Objekt an den Hintergrundprozess mit
übergeben. Diese wurde entsprechend den Erfordernissen
erweitert
(Listing 1).
Listing 1
private void AufwaendigerVorgang2(
BackgroundWorker worker, DoWorkEventArgs e)
{
for (int i = 0; i < 10; i++)
{
if (worker.CancellationPending==true)
{
e.Cancel = true;
break;
}
worker.ReportProgress(i * 10);
Thread.Sleep(1000);
}
}
Der Aufruf des Hintergrundprozesses erfolgt jetzt nicht mehr direkt,
sondern über die
RunWorkerAsync()-Methode.
Abgebrochen wird mittels der
CancelAsync()-Methode,
d.h. hierdurch wird die Abbruchanforderung durch die
BackgroundWorker-Komponente
an den Hintergrundprozess übermittelt. In diesem selbst muss
immer
wieder auf das Vorhandensein einer Abbruchanforderung geprüft
werden. Gleiches gilt für die Übermittlung des
Bearbeitungsstandes. Den Unterschied zwischen der sequenziellen und der
alternativen nebenläufigen Ausführung des
Hintergrundprozesses zeigt Abbildung 1. Hier wird jeweils angenommen,
dass sowohl der Hauptprozess als auch der laufzeitintensive Prozess
jeweils eine Methode separater Klassen (
Main und
Background) ist. Der
obere Teil der Abbildung zeigt, dass bei sequenzieller Verarbeitung die
Instanz der Klasse
Main auf die Antwort der Klasse
Background warten
muss. Im unteren Teil ist ersichtlich, dass die Klasse
Main weiterhin
„reaktionsfähig“ ist und
regelmäßig eine
Botschaft über den Verarbeitungsstand von
Background
empfängt.

Abb. 1: Vergleich von sequenzieller und nebenläufiger Ausführung
Mittels der Testapplikation (Abb. 2) kann das Vorgehen leicht
nachvollzogen werden. Der Quellcode (Auszüge) der
vorgestellten Methoden und Ereignisse ist in Listing 3 zusammengefasst.
Das Beispiel wurde bewusst schlank gehalten und auf das Wesentliche
beschränkt.

Abb. 2: Formular der Testanwendung
Listing 2
private void buttonStartNormal_Click(…)
{
...
AufwaendigerVorgang1();
...
}
-------------------------
private void backgroundWorker_DoWork(…)
{
BackgroundWorker worker = sender as BackgroundWorker;
AufwaendigerVorgang2(worker, e);
}
-------------------------
private void buttonStartBackground_Click(…)
{
...
backgroundWorker1.RunWorkerAsync();
}
-------------------------
private void backgroundWorker1_RunWorkerCompleted(…)
{
if (e.Cancelled == true)
{
lblStatus.Text="Abbruch des Vorgangs";
}
else
{
lblStatus.Text = "Erfolgreich beendet";
}
...
}
-------------------------
private void buttonCancel_Click(...)
{
backgroundWorker1.CancelAsync();
...
}
-------------------------
private void backgroundWorker1_ProgressChanged(…)
{
progressBar1.Value = e.ProgressPercentage;
}
Zum Schluss
Mit C# und der Komponente
BackgroundWorker
gelingt es recht unkompliziert, laufzeitintensive Vorgänge im
Hintergrund des Hauptprogramms ausführen zu lassen. Die
einfache Integration und Konfiguration des Bausteins macht dieses
– sonst als komplex eingestufte – Thema
alltagstauglich für den Softwareentwickler. Der Einbau, auch
in bestehenden Quellcode, ist unkritisch. Es muss lediglich die Methode
zum Aufruf des Hintergrundprozesses entsprechend modifiziert werden und
eine regelmäßige Überprüfung auf
eine Abbruchanforderung stattfinden. Zum leichteren
Verständnis bietet es sich an, ein eigenes Beispiel zu
konstruieren. Dazu kann die Beispielapplikation modifiziert werden,
indem ein konkreter zeitintensiver Vorgang (statt der Sinnlosschleife)
eingebaut wird.
Veikko Krypczyk hat
Betriebswirtschaftslehre u.a. in der Fachrichtung Wirtschaftsinformatik
studiert. Nebenberuflich ist er als Softwareentwickler und Fachautor
für Themen der IT tätig. Er beschäftigt sich
seit längerem mit der Softwareentwicklung, u.a. mit C# und dem
.NET Framework. Lob und Kritik senden Sie per E-Mail an
veikko2000@yahoo.de.