Die BackgroundWorker Klasse ist eine Hilfsklasse zur Verwaltung eines Background Threads. Sie eignet sich besonders gut wenn es darum geht langwierige Aufgaben in einer WPF oder Windows Forms Anwendung auszuführen, ohne dabei die GUI zu blockieren. Details zur BackgroundThread Klasse und ein Template zur Implementierung finden sie hier.
In diesem Artikel möchte ich Ihnen zeigen wie Sie eine eigene Klasse von BackgroundWorker ableiten können. Mit diesem Vorgehen können Sie relativ einfach Klassen erzeugen welche langfristige Aufgaben parallel in einem Background Thread ausführen und dabei mit der GUI kommunizieren können ohne diese zu blockieren.
Um eine eigene BackgroundWorker Klasse zu erstellen, müssen Sie nur von der Original Klasse ableiten. Anschliessend können Sie die OnDoWork Methode überschreiben und mit Leben füllen. Der nachfolgende Quellcode enthält ein Template für eine eigene Klasse abgeleitet von BackgroundWorker.
1 public class CustomizedBackgroundWorkerTemplate : BackgroundWorker
2 {
3 public CustomizedBackgroundWorkerTemplate()
4 {
5 WorkerReportsProgress = true;
6 WorkerSupportsCancellation = true;
7 }
8
9 protected override void OnDoWork (DoWorkEventArgs e)
10 {
11 //execute
12 ReportProgress(0);
13
14 for (int i=0; i < 100; i++)
15 {
16 if (CancellationPending)
17 {
18 e.Cancel = true;
19 return;
20 }
21
22 //…do work…
23
24 ReportProgress(i);
25 }
26
27 ReportProgress(100);
28
29 e.Result = „my result“;
30 }
31 }
Beispielanwendung
Eine kleine Beispielanwendung soll Ihnen das Vorgehen bei der Erstellung und Verwendung eines eigenen BackgroundWorkers verdeutlichen. Die Beispielanwendung berechnet Primzahlen. Die eigentliche Berechnungsfunktion soll dabei in einem parallelen Thread ausgeführt werden. Für die Berechnung wird ein Start- und Endwert angegeben. Somit können mehrere Threads parallel gestartet werden, um Primzahlen in unterschiedlichen Bereichen zu ermitteln.
BackgroundWorker zur Primzahlenberechnung.
Die nachfolgende Klasse zeigt die fertig implementierte Primzahlenberechnung in einer eigenen BackgroundWorker Klasse.
1 public class PrimeNumberBackgroundWorker : BackgroundWorker
2 {
3 public PrimeNumberBackgroundWorker()
4 {
5 WorkerReportsProgress = true;
6 WorkerSupportsCancellation = true;
7 }
8
9 public PrimeNumberBackgroundWorker(
10 int identifier,
11 int first,
12 int last)
13 : this()
14 {
15 _identifier = identifier;
16 _first = first;
17 _last = last;
18 }
19
20 protected override void OnDoWork (DoWorkEventArgs e)
21 {
22 int actual;
23 int total;
24
25 //init
26 total = _last – _first + 1;
27
28 _primeNumbers = new List<int>();
29
30 //execute
31 ReportProgress(0, _identifier);
32
33 for(actual = _first; actual <= _last; actual++ )
34 {
35 if (CancellationPending)
36 {
37 e.Cancel = true;
38 return;
39 }
40
41 if (IsPrimeNumber(actual) == true)
42 {
43 _primeNumbers.Add(actual);
44 }
45
46 Thread.Sleep(2);
47
48 if (actual % 10 == 0)
49 {
50 ReportProgress((actual-_first) * 100 / total, _identifier);
51 }
52 }
53
54 ReportProgress(100, _identifier);
55
56 e.Result = _primeNumbers;
57 }
58
59 private static bool IsPrimeNumber(int number)
60 {
61 if(number % 2 == 0)
62 {
63 return false;
64 }
65
66 int half = (int)(number / 2.0) + 1;
67
68 for(int i = 3; i <= half; i+=2)
69 {
70 if(number % i == 0)
71 {
72 return false;
73 }
74 }
75
76 return true;
77 }
78
79 private int _identifier;
80 private int _first;
81 private int _last;
82
83 private List<int> _primeNumbers;
84 }
Zeile 46 enthält eine Thread.Sleep Anweisung. Diese Anweisung hat keinen funktionalen Grund sondern dient lediglich dazu die Berechnung zu verlangsamen um damit die parallele Ausführung der Threads besser in der GUI visualisieren zu können. Des Weiteren wurde nur ein simpler Berechnungsalgorithmus implementiert, da der Fokus dieses Beispielprogramm auf dem BackgroundWorker liegt.
Verwendung in GUI
In einer WPF Anwendung wird die Primzahlenberechnung in acht parallelen Threads ausgeführt. Die GUI enthält acht Progress Bars zur Fortschrittsanzeige. Der nachfolgende Screenshot zeigt die Applikation während der Durchführung der Primzahlenberechnung. Da die Threads parallel ausgeführt werden ist die GUI währenddessen bedienbar. Es kann zum Beispiel jederzeit der Button geklickt werden um die Berechnung neu zu starten.
Wenn ein Thread fertig prozessiert ist meldet er die Ergebnisse an die GUI und die Primzahlen werden in einer ListBox angezeigt. Der nachfolgende Screenshot zeigt die Anwendung mit den berechneten Primzahlen.
Die Verwaltung der BackgroundWorker Threads inklusive Ereignisbehandlung ist in der Klasse des Hauptfensters implementiert. Der nachfolgende Quellcode zeigt diese Klasse.
1 ///<summary>
2 /// Interaction logic for MainWindow.xaml
3 ///</summary>
4 public partial class MainWindow : Window
5 {
6 public MainWindow()
7 {
8 InitializeComponent();
9 }
10
11 private void Button_Click(
12 object sender,
13 RoutedEventArgs e)
14 {
15 //init
16 ProgressBarA.Value = 0;
17 ProgressBarB.Value = 0;
18 ProgressBarC.Value = 0;
19 ProgressBarD.Value = 0;
20 ProgressBarE.Value = 0;
21 ProgressBarF.Value = 0;
22 ProgressBarG.Value = 0;
23 ProgressBarH.Value = 0;
24
25 Results.Items.Clear();
26
27 //background worker
28 StopAllBackgroundWorker();
29 CreateAllBackgroundWorker();
30 StartAllBackgroundWorker();
31 }
32
33 private void PrimeNumberWorkerCompleted(
34 object sender,
35 RunWorkerCompletedEventArgs e)
36 {
37 if (e.Cancelled)
38 {
39 //canceled
40 }
41 else if (e.Error != null)
42 {
43 //error occured
44 }
45 else
46 {
47 List<int> results = e.Result as List<int>;
48
49 Results.Items.Add(„—————————–„);
50 foreach (int result in results)
51 {
52 Results.Items.Add(result);
53 }
54 }
55 }
56
57 private void PrimeNumberWorkerProgressChanged(
58 object sender,
59 ProgressChangedEventArgs e)
60 {
61 int identifier = (int)e.UserState;
62
63 switch(identifier)
64 {
65 case 1: ProgressBarA.Value = e.ProgressPercentage; break;
66 case 2: ProgressBarB.Value = e.ProgressPercentage; break;
67 case 3: ProgressBarC.Value = e.ProgressPercentage; break;
68 case 4: ProgressBarD.Value = e.ProgressPercentage; break;
69 case 5: ProgressBarE.Value = e.ProgressPercentage; break;
70 case 6: ProgressBarF.Value = e.ProgressPercentage; break;
71 case 7: ProgressBarG.Value = e.ProgressPercentage; break;
72 case 8: ProgressBarH.Value = e.ProgressPercentage; break;
73 }
74 }
75
76 private void CreateAllBackgroundWorker()
77 {
78 PrimeNumberBackgroundWorker backgroundWorker;
79
80 _backgroundWorkers = new List<PrimeNumberBackgroundWorker>();
81
82 for (int i = 0; i < 8; i++)
83 {
84 backgroundWorker = new PrimeNumberBackgroundWorker(
85 i + 1,
86 i * 1000 + 1,
87 i * 1000 + 1000);
88
89 backgroundWorker.ProgressChanged += PrimeNumberWorkerProgressChanged;
90 backgroundWorker.RunWorkerCompleted += PrimeNumberWorkerCompleted;
91
92 _backgroundWorkers.Add(backgroundWorker);
93 }
94 }
95
96 private void StartAllBackgroundWorker()
97 {
98 foreach (PrimeNumberBackgroundWorker worker in _backgroundWorkers)
99 {
100 worker.RunWorkerAsync();
101 }
102 }
103
104 private void StopAllBackgroundWorker()
105 {
106 if (_backgroundWorkers == null)
107 {
108 return;
109 }
110
111 foreach(PrimeNumberBackgroundWorker worker in _backgroundWorkers)
112 {
113 if(worker.IsBusy == true)
114 {
115 worker.CancelAsync();
116 }
117 }
118 }
119
120 private List<PrimeNumberBackgroundWorker> _backgroundWorkers;
121 }
Fazit
Lang laufende Hintergrundaufgaben einer GUI können sehr einfach in separate Threads ausgelagert werden. Dazu bietet sich die Implementierung einer eigenen Klasse an, welche von BackgroundWorker erbt. Dadurch kann die Logik zur Ausführung der Hintergrundaufgaben in den jeweiligen Klassen realisiert werden und diese werden zusätzlich um die umfangreichen Features der BackgroundWorker Klasse erweitert.
Quellcode
Der komplette Quellcode der Beispielanwendung steht Ihnen hier zur Verfügung. (Bitte entfernen Sie die zusätzliche Erweiterung „.pdf“ nach dem Download und aktivieren Sie die Dateiendung „.zip“ damit Sie die Zip-Datei entpacken können.)
Eine schöne Informationsserie, besonders für Quereinsteiger in C#. Allerdings ist die Routine ‚IsPrimeNumber‘ in dreierlei Hinsicht ungenau:
1) „Eine Primzahl ist eine natürliche Zahl, die genau zwei natürliche Zahlen als Teiler hat“ (Wikipedia), d.h. alle Zahlen < 2 sind keine Primzahlen – eine entsprechende Abfrage für die allgemeine Nutzung der Routine fehlt.
2) Da 2 eine Primzahl ist, liefert die Abfrage "if (2 % 2 == 0)" natürlich "true", was zum Überspringen der 2 führt.
3) Der Wert "half" ist für alle natürlichen Zahlen ein ganzzahliger Wert, der größer als die Hälfte der zu untersuchenden Zahl ist, d.h. in der for-Schleife kann die Abbruchbedingung auf "i < half" geändert werden.
Danke für die Richtigstellung der Berechnungsfunktion. Das die „2“ ignoriert wird ist natürlich ein Fehler. Des weiteren fehlt eine Prüfung des Wertebereiches. Wie richtig gesagt wurde müssten alle Zahlen kleiner als 2 ignoriert werden.
Hi, kann mich nur anschließen, dass es eine super Serie ist, besonders für Quereinsteiger in C#. Habe das obige Beispiel verwendet und ein wenig rumgespielt, wo ich dann auf folgende Probleme gestoßen bin:
1. Wenn einer der BackgroundWorker in einen Fehler läuft, wie bekomme ich dann mit, welcher der BackgroundWorker es war?
Ich habe beispielsweise in der Klasse PrimeNumberBackgroundWorker über der Zeile 61
die Anweisung „if(number == 3000 || number == 5000) throw new Exception(„Ein Fehler von mir!“);“ eingefügt. Da ich den Fehler in der Klasse nicht abfange, behandelt der BackgroundWorker sie und gibt sie an das Error.Property von RunWorkerCompletedEventArgs weiter. In der Zeile 41 von MainWindow wird auf e.Error geprüft, wodurch ich dann meine Exception sehe und im MainThread behandeln kann. Aber woher weiß ich, welcher Thread es war? Im Falle eines Fehlers kann man das e.Result nicht verwenden und das Property „UserState“ kann ich aus der doWork-Methode heraus nicht setzen.
Persönlich fällt mir nur ein, eine eigene Exception zu schreiben, die ich dann beispielsweise um das Property UserState/Identifier erweitere. Oder gibt es eine andere Möglichkeit?
2. Ich habe mich vor dem BackgroundWorker mit der Klasse Task beschäftigt und habe, wie im obigen Beispiel, mehrere Tasks gleichzeitig gestartet.
Task[] tasks = new Task[2];
tasks[0] = Task.Factory.StartNew(() => legePersonAn(„Müller“, „Titus“));
tasks[1] = Task.Factory.StartNew(() => legePersonAn(„Schmidt“, „Depp“));
Task.WaitAll();
Über die WaitAll()-Methode kann ich somit warten, bis alle Task beendet sind.
Gibt es so was auch für den BackgroundWorker bzw. oder gibt es eine andere Möglichkeit dies zu realisieren?