Multithreading in C#, Teil 4: Individualisierter BackgroundWorker

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.

04_picture_cbw_app1

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.

04_picture_cbw_app2

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

Advertisements
Dieser Beitrag wurde unter .NET, C#, Multithreading abgelegt und mit verschlagwortet. Setze ein Lesezeichen auf den Permalink.

3 Antworten zu Multithreading in C#, Teil 4: Individualisierter BackgroundWorker

  1. A. L. Diehm schreibt:

    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.

    • oliverfunke schreibt:

      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.

  2. Joe T. schreibt:

    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?

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s