Eine Heptalogie zu Mandelbrotmengen

1. Aufgabe

Der polnisch-französische Mathematiker Benoit B. Mandelbrot entdeckte im Jahre 1980 eine einfache Formel, mit der sich unendlich viele faszinierende Grafiken - auch Fraktale genannt - auf dem Computerbildschirm darstellen lassen. Das bekannteste Bild aus dieser Menge ist das so genannte Apfelmännchen, da es mehrere Figuren darstellt, die einem Apfel sehr ähnlich sehen, siehe zum Beispiel Abbildung 1:

Eines der bekanntesten Bilder aus der Familie der Mandelbrot-Fraktale.

Abbildung 1. Eines der bekanntesten Bilder aus der Familie der Mandelbrot-Fraktale.


Komplexe Zahlen eignen sich gut für die Darstellung fraktaler Computergrafiken: Jeder Bildschirmpunkt kann einem Punkt in der komplexen Zahlenebene zugeordnet werden. Die Berechnungsvorschrift für die Punkte einer Mandelbrotmenge lautet

zn+1 = zn2 + c

Dabei durchläuft n die natürlichen Zahlen 0, 1, 2, usw. z0 besitzt den Wert 0 und c ist eine beliebige komplexe Zahl. Der nächste Wert in dieser Zahlenfolge errechnet sich somit immer aus dem alten Wert zum Quadrat plus einem konstanten Wert c. Die ersten Werte dieser Folge lauten für ein beliebiges c:

z1 = c

z2 = z12 + c = c2 + c

z3 = z22 + c = (c2 + c)(c2 + c) + c = c4 + 2c3 + c2 +c

Man erkennt, dass die einzelnen Folgenelemente nur von der komplexen Zahl c abhängen. Betrachtet man zu den einzelnen Elementen einer Mandelbrot-Folge den Betrag, so kann man zwei Beobachtungen feststellen: Entweder der Betrag der Zahlen läuft gegen plus unendlich zu, oder aber er befindet sich stets unterhalb eines bestimmten Wertes, in unseren Beispielen liegt diese Schranke bei 2. Um diese Beobachtung grafisch auf dem Bildschirm zum Ausdruck zu bringen, legt man fest, all diejenigen Punkte aus der komplexen Zahlenebene, deren Betrag gegen unendlich strebt, mit einer bestimmten Farbe ungleich schwarz zu zeichnen, während man alle anderen Punkte auf dem Bildschirm in der Farbe schwarz belässt.

Um das Programm bzgl. der Entscheidung, ob der Betrag einer Mandelbrot-Folge gegen unendlich läuft oder nicht, nicht endlos arbeiten zu lassen, bricht man die Berechnung der Folgenelemente nach einer bestimmten Anzahl ab. Wir sprechen hier von der so genannten Iterationstiefe. Die Zuordnung eines Punktes zu einer Farbe erfolgt nun anhand der Geschwindigkeit, mit der der Betrag der Mandelbrot-Folgenelemente gegen unendlich strebt. Legen wir beispielsweise für die maximale Iterationstiefe einen Wert von 100 fest, so benötigen wir für die grafische Darstellung 100 Farben. Der Index n des Folgenelements zn, ab dem die Schranke übertroffen wird, entscheidet, welche Farbe zu benutzen ist.

Um nun konkret ein Bild mit Mandelbrot-Zahlenfolgen darzustellen, müssen wir noch festlegen, welchen Ausschnitt aus der komplexen Zahlenebene das Bild darstellen soll, sprich wir müssen in der komplexen Zahlenebene die obere und untere Grenze an den beiden Achsen festlegen, siehe Abbildung 2:

Zuordnung einer komplexen Zahl zu einem Pixel.

Abbildung 2. Zuordnung einer komplexen Zahl zu einem Pixel.


In Abbildung 2 liegt folgender Wertebereich zu Grunde:

public const double XMIN = -2.0;   // minimum x-value (real part)
public const double XMAX = +0.75;  // maximum x-value (real part)
public const double YMIN = -1.25;  // minimum y-value (imaginary part)
public const double YMAX = +1.25;  // maximum y-value (imaginary part)

Natürlich ist die Menge aller komplexen Zahlen in einem beliebigen Ausschnitt der Zahlenebene unendlich, wir müssen daher den umgekehrten Weg beschreiten und für alle Pixel eines Windows-Fensterobjekts ausrechnen, welche komplexe Zahl dem Pixel zugeordnet ist. Zu dieser komplexen Zahl müssen wir dann die Folge der Mandelbrot-Zahlen berechnen und (anhand einer vorgegebenen Iterationstiefe) entscheiden, ob der Betrag gegen unendlich strebt oder nicht. In Abbildung 1 liegt für die Iterationstiefe der Wert 100 vor, die Schranke wurde mit dem Wert 2 festgelegt. Zugegeben, bei einem Windows-Fensterobjekt mit der Breite und Höhe von 400 Pixel bedeutet das, dass immerhin 160.000 Zahlenfolgen betrachtet werden müssen, es werden also weit mehr als eine Million komplexer Mandelbrot-Zahlen berechnet.

Lassen Sie mich zum Abschluss einen kurzen Blick auf die Vielfältigkeit von Mandelbrot-Grafiken werfen, bei unterschiedlichen Ausschnitten aus der komplexen Zahlenebene erhalten wir die Fraktale aus Abbildung 3 bzw. Abbildung 4.

Ein weiteres Beispiel für die Faszination von Mandelbrot-Grafiken.

Abbildung 3. Ein weiteres Beispiel für die Faszination von Mandelbrot-Grafiken.


Für die beiden Grafiken aus Abbildung 3 und Abbildung 4 liegen sehr kleine Ausschnitte aus der komplexen Zahlenebene vor, in Abbildung 3 ist es der Bereich:

public const double XMIN = -0.745468;  // minimum x-value (real part)
public const double XMAX = -0.745385;  // maximum x-value (real part)
public const double YMIN = +0.112975;  // minimum y-value (imaginary part)
public const double YMAX = +0.113044;  // maximum y-value (imaginary part)

und in Abbildung 4 der Bereich

public const double XMIN = -0.74581;   // minimum x-value (real part)
public const double XMAX = -0.74448;   // maximum x-value (real part)
public const double YMIN = +0.11196;   // minimum y-value (imaginary part)
public const double YMAX = +0.11339;   // maximum y-value (imaginary part)
Noch ein drittes Beispiel einer Mandelbrot-Grafik.

Abbildung 4. Noch ein drittes Beispiel einer Mandelbrot-Grafik.


Je kleiner der Ausschnitt aus der komplexen Zahlenebene ist, desto größer muss man die Iterationstiefe wählen, um ein interessantes Bild zu erhalten.

In der nun folgenden Heptalogie betrachten wir eine Reihe von Problemstellungen, die sich im Zusammenhang mit der Erstellung einfacher wie auch komplexerer WPF-Anwendungen zur Visualisierung von Mandelbrotmengen ergeben:

  • Variation 1: Basisversion der Mandelbrot-Anwendung.

    Erstellen Sie eine C#-Applikation, die das in Abbildung 1 gezeigte Bild erzeugt. Es genügt eine einfache Realisierung, die grafischen Ausgaben dürfen innerhalb des Click-Ereignishandlers einer Schaltfläche durchgeführt werden. Die Klassen Image und WriteableBitmap sind geeignete Kandidaten, um berechnete Pixel auch zur Anzeige zu bekommen. Für die beiden Objekte darf eine feste Größe zu Grunde gelegt werden.

  • Variation 2: Basisversion mit reaktionsfähigem Window-Objekt.

    Mit großer Wahrscheinlichkeit besitzt Ihre Lösung zu Teilschritt 1 eine unschöne Eigenschaft: Für die Dauer der Berechnungen und grafischen Ausgaben ist das Fensterobjekt der Anwendung nicht bedienbar oder wie man auch sagt, nicht reaktionsfähig: Es lässt sich nicht bewegen, die Schaltflächen zum Verkleinern, Vergrößern und Schließen des Fensters funktionieren nicht, das Fensterobjekt ignoriert schlichtweg alle Eingaben des Benutzers.

     

    Entwickeln Sie im zweiten Teilschritt eine Anwendung, die sich wie jede andere Windows-Anwendung durch eine adäquate Reaktionsfähigkeit auszeichnet. Verwenden Sie zu diesem Zweck die Klassen Task und Dispatcher aus den Namensräumen System.Threading.Tasks bzw. System.Windows.Threading, um alle langlaufenden Methoden zu entkoppeln und in verkleinerten Teilaufgaben in den Primärthread der Anwendung einzuschleusen.

  • Variation 3: Basisversion mit reaktionsfähigem Image-Objekt.

    In den ersten beiden Teilschritten war es nicht verlangt, bei Größenveränderungen des Fensters das vorhandene Image- (bzw. WriteableBitmap-)Objekt ebenfalls in seiner Größe zu verändern. In diesem Teilschritt wird die Forderung gestellt, dass Größenveränderungen des Fensters an das unterlagerte Image- und WriteableBitmap-Objekt weitergereicht werden. Da bei schnell aufeinander folgenden Größenveränderungen des Fensters die Anwendung noch mit den Berechnungen der letzen Visualisierung beschäftigt ist, lassen sich mit dieser zusätzlichen Forderung eine Reihe weiterer, höchst interessanter Features des Dispatcher-Objekts studieren.

  • Variation 4: Parallelisierung mit Tasks.

    Pixel-Operationen stellen selbst einen leistungsstarken Rechner vor größere Probleme, da für die Ansteuerung eines einzelnen Pixels viele Softwareschichten zu durchlaufen sind. Selbst auf einem schnellen Rechner werden Sie feststellen, dass die Ausführungen der bislang erstellen Programme viel Rechenzeit verbrauchen. Wir bringen nun die Features eines Mehrkernrechners ins Spiel: Ergänzen Sie Ihre Anwendung um eine Lastverteilung mit einer entsprechenden Aufteilung aller mathematischen Berechnungen und Zeichenvorgänge auf mehrere Kerne. Als Zulieferung von Ihrer Seite müssen Sie die Menge aller Rechenoperationen in noch zu definierende Teilmengen aufteilen und ihre Ausführung einzelnen Kernen zuteilen. Die Klasse Task aus dem Namensraum System.Threading.Tasks sollte hierbei ins Spiel kommen. Im Beispiel der Mandelbrot-Anwendung ist diese Aufteilung nicht weiter schwer. Was liegt näher, als die Oberfläche des Fensterobjekts einfach in gleich große Rechtecke zu unterteilen, wie in Abbildung 5 gezeigt wird:

    Unterteilung der Darstellung einer Mandelbrotmenge in mehrere Rechtecke.

    Abbildung 5. Unterteilung der Darstellung einer Mandelbrotmenge in mehrere Rechtecke.


    Jede einzelne Task ist nun für einen bestimmten Ausschnitt des Bilds verantwortlich und berechnet die Farbdarstellung der einzelnen Pixel unabhängig von den anderen Tasks. Schreiben Sie die Anwendung so um, dass die Berechnungen der einzelnen Mandelbrot-Folgen samt ihrer Ausgaben im Fensterobjekt auf mehrere Tasks verteilt werden.

  • Variation 5: Parallelisierung mit BackgroundWorker-Objekt.

    Speziell für .NET-Anwendung mit Oberflächenanteilen gibt es eine Klasse BackgroundWorker. Sie führt Aktivitäten, wie der Name zum Ausdruck bringt, im Hintergrund aus und bietet spezielle Rückrufmethoden an, die im Kontext des UI-Threads ausgeführt werden. Damit können Ausgaben auf der Oberfläche gemacht werden, ohne das Dispatcher-Objekt in Anspruch nehmen zu müssen.

  • Variation 6: Parallelisierung ausschließlich mit Dispatcher-Objekt.

    In dieser Variation zum Abschluss der Heptalogie stellen wir eine Lösung einer parallel arbeitenden Mandelbrot-Anwendung vor, die ohne Task- und BackgroundWorker-Objekte auskommt.

2. Lösung

Bevor wir im Detail auf die Realisierung der einzelnen Mandelbrot-Anwendungen eingehen, müssen wir einige Grundlagen zum Zeichnen von Pixeln schaffen. In der WPF finden wir Bitmaps in Gestalt der Klasse WriteableBitmap vor. WriteableBitmap-Objekte lassen sich beispielsweise in einem Bild anzeigen, wir benötigen folglich noch ein Image-Objekt. Die Klasse WriteableBitmap ist im Namensraum System.Windows.Media.Imaging untergebracht, zum Erzeugen von Objekten verwenden wir den Konstruktor aus Tabelle 1:

Konstruktor

Beschreibung

public WriteableBitmap
(
  int pixelWidth,
  int pixelHeight,
  double dpiX,
  double dpiY,
  PixelFormat pixelFormat,
  BitmapPalette palette
);

Die ersten beiden Parameter pixelWidth und pixelHeight legen die Größe des WriteableBitmap-Objekts fest. Zum Einstellen der Auflösung in Punkten pro Zoll (dots per inch, dpi) gibt es die zwei Parameter dpiX (horizontale Punkteanzahl) und dpiY (vertikale Punkteanzahl).

Das fünfte Argument pixelFormat ist ein PixelFormat-Objekt. Jedes Pixel in einer Bitmap wird durch ein oder mehrere Bits oder Bytes repräsentiert, um seine Farbe zu definieren.

 

Mit dem letzten Parameter palette kann man eine Farbpalette (BitmapPalette-Objekt) festlegen. Pixelformate wie Indexed2 benötigen eine Farbpalette.

Tabelle 1. Konstruktor der Klasse WriteableBitmap.


Um PixelFormat-Objekte zu definieren, greift man am besten auf vordefinierte Objekte zurück, die in einer Klasse PixelFormats zusammengestellt sind. Insgesamt stehen hier 26 Formate zur Auswahl, in der nachfolgenden Aufstellung finden Sie einige exemplarische Formate vor:

  • Bgr24 – Spezifiziert das Bgr24-Pixelformat. Bgr24 ist ein sRGB-Format, das zur Beschreibung eines Pixels 24 Bits benötigt. Jedem Farbkanal (rot, grün und blau) sind 8 Bits zugeordnet.

  • Bgr32 – Spezifiziert das Bgr32-Pixelformat. Bgr32 ist ein sRGB-Format, das zur Beschreibung eines Pixels 32 Bits benötigt. Jedem Farbkanal (rot, grün und blau) sind 8 Bits zugeordnet, das vierte Byte ist unbenutzt.

  • Bgra32 – Spezifiziert das Bgra32-Pixelformat. Bgra32 ist wie Bgr32 ein sRGB-Format, das zur Beschreibung eines Pixels 32 Bits benötigt. Im Gegensatz zum Bgr32-Format wird im letzten Byte der Alphakanal (Transparenz) abgelegt: 0 entspricht durchsichtig, 255 undurchsichtig.

  • Bgr555 – Spezifiziert das Bgr555-Pixelformat. Bgr555 ist ein sRGB-Format, das zur Beschreibung eines Pixels 16 Bits benötigt. Jedem Farbkanal (rot, grün und blau) sind 5 Bits zugeordnet.

  • Gray2 – Spezifiziert das Gray2-Pixelformat. Gray2 steht für ein Format mit Grauschattierung. Pro Pixel stehen 2 Bits zur Verfügung. Ein Pixel mit ausschließlich Nullen ist schwarz und eines mit ausschließlich Einsen ist weiß.

  • Gray4 – Spezifiziert das Gray4-Pixelformat. Gray4 ist wie Gray2 definiert. Pro Pixel stehen 4 Bits zur Verfügung, damit sind 16 unterschiedliche Grauschattierungen möglich.

  • Gray8 – Spezifiziert das Gray8-Pixelformat. Gray8 ist wie Gray2 definiert. Pro Pixel stehen 8 Bits zur Verfügung, damit sind 256 unterschiedliche Grauschattierungen möglich.

  • Indexed2 – Spezifiziert das Indexed2-Pixelformat. Formate, die mit dem Wort Indexed beginnen, benötigen zusätzlich eine Farbpalette. Jedes Pixel ist dann ein Index in diese Farbpalette. Beim Indexed2-Pixelformat stehen pro Pixel 2 Bits zur Verfügung, es können folglich 4 Farben in der Palette adressiert werden. Andere indizierte Pixelformate lauten Indexed1, Indexed4 und Indexed8, diesen Formaten sind (höchstens) 2, 16 bzw. 256 Farben zugeordnet.

Wir sind nun in der Lage, ein WriteableBitmap-Objekt zu erzeugen. Die nächste Hürde ist der Aufruf der zentralen Methode WritePixels dieser Klasse. Bevor wir ihre Definition betrachten, sind wiederum einige Grundlagen zu schaffen. Das Lesen und Schreiben von einem oder mehreren Pixeln erfolgt an einem WriteableBitmap-Objekt aussschließlich auf der Basis von (untypisierten) Byte-Arrays. Logischerweise muss der von uns erstellte C#-Code und das aktuell vorliegende WriteableBitmap-Objekt vom selben Pixelformat ausgehen, wenn wir Byte-Arrays bit- oder byteweise lesen bzw. schreiben. Auf der Basis des abgesprochenen Formats weiß das Objekt insbesondere, wie viele Bytes für ein einzelnes Pixel notwendig sind und an welcher Stelle diese intern zu einer bestimmten Koordinate abzulegen sind.

Wir steigen etwas mehr in die Details ein: Die Gesamtanzahl der Pixel eines WriteableBitmap-Objekts sind durch das Produkt der Eigenschaften PixelWidth und PixelHeight festgelegt (siehe Tabelle 1). Das Pixelformat selbst wird durch ein Objekt des Typs PixelFormat festgelegt. An diesem Objekt finden wir eine Eigenschaft BitsPerPixel vor:

public int BitsPerPixel { get; }   // number of bits-per-pixel (bpp)
                                   // for this pixel format

Die Werte dieser Eigenschaft können, je nach Pixelformat, zwischen 1 und 128 liegen! Auf diese Weise ist es also möglich, dass ein einzelnes Byte Daten für 8 fortlaufende Pixel enthalten kann (zum Beispiel bei einem Grauschattierungsformat mit nur zwei Grauwerten) oder ganze 16 Byte Daten für ein einzelnes Pixel erforderlich sind. Um ein oder mehrere Pixel in einer Bitmap anzusprechen, legt man in der Regel einen rechteckigen Ausschnitt mit Hilfe des Strukturtyps Int32Rect fest:

struct Int32Rect
{
    public Int32Rect (int x, int y, int width, int height);

    public int Height { get; set; } // height of the rectangle
    public int Width { get; set; }  // width of the rectangle
    public int X { get; set; }      // x-coordinate of the top-left
    public int Y { get; set; }      // y-coordinate of the top-left
    ...
}

Um nun alle Pixel, die durch eine Int32Rect-Strukturvariable festgelegt werden, lesend oder schreibend anzusprechen, muss man ein hinreichend großes Byte-Array zur Verfügung stellen. Hier kann es nun ein wenig kompliziert werden, da wie schon erwähnt verschiedene Pixelformate mehrere Pixel in einem einzelnen Byte speichern. Bei diesen Formaten müssen die Daten für eine einzelne Pixelzeile immer an einer Bytegrenze beginnen.

Wir machen dazu ein Beispiel: Angenommen, Sie arbeiten mit einer Bitmap mit einem Format von 4 Bits pro Pixel. Der rechteckige Bereich der Bitmap, auf den Sie lesend oder schreibend zugreifen, ist 5 Pixel breit und 12 Pixel hoch. Damit ist geklärt, welche Werte die Width- und Height-Eigenschaften des dazugehörigen Int32Rect-Objekts besitzen. Das erste Byte im Array enthält Daten für die ersten zwei Pixel, das zweite für die nächsten beiden Pixel, das dritte Byte jedoch enthält nur Daten für das fünfte Pixel in der ersten Zeile! Das nächste Byte entspricht den ersten beiden Pixeln der zweiten Zeile, im Byte zuvor sind also die letzten 4 Bits ungenutzt geblieben.

Jede Zeile Daten erfordert 3 Bytes, und der gesamte rechteckige Bereich erfordert 36 Bytes. Für diese Berechnung erfordert die WritePixels-Methode ein Argument namens stride. Dies ist die Anzahl der Bytes, die für jede einzelne Zeile an Pixeldaten notwendig sind. Allgemein gesehen kann man den stride-Wert so berechnen:

int stride = (Width * BitsPerPixel + 7) / 8;

Die Breite ist gleich der Width-Eigenschaft der Int32Rect-Struktur. Jetzt sind wir soweit, um die Definition der WritePixels-Methode in Tabelle 2 auch verstehen zu können:

Methode

Beschreibung

public void WritePixels
(
  Int32Rect sourceRect,
  Array pixels,
  int stride,
  int offset
);

Mit dem ersten Parameter sourceRect wird der Ausschnitt aus der Bitmap festgelegt, auf den man zugreifen möchte. Die Pixeldaten sind, konform zum Pixelformat, im Parameter pixels in einem ein- oder zweidimensionalen byte-Array abzulegen. Das implizit benutzte Pixelformat legt fest, wie viele Einträge in dem Array pro Pixel bereit zu stellen sind.

Der Parameter stride legt die Anzahl Bytes fest (inklusiver Leerbytes), die für die Daten einer Pixelreihe erforderlich sind. Mit dem stride-Parameter kann man im Parameter pixels korrekt von einer Pixelreihe zur nächsten umschalten.

Der letzte Parameter offset ist ein Offset in das pixels-Array. Typischerweise übergibt man hier den Wert 0. Verwendet man dasselbe pixels-Array für mehrere Aufrufe von WritePixels, so könnte man mit diesem Parameter einen anderen Wertebereich adressieren.

Tabelle 2. Methode WritePixels aus der Klasse WriteableBitmap.


Einige allgemeine Festlegungen, wie etwa die Bereiche der komplexen Zahlenebene, die wir visualisieren wollen, oder auch eine einfache Methode Compute zum Berechnen eines Mandelbrotfolgenwerts habe ich in einer Hilfsklasse Mandelbrot zusammengestellt (Listing 1):

01: public static class Mandelbrot
02: {
03:     public const double XMin = -2.0;   // minimum x-value (real part)
04:     public const double XMax = +0.75;  // maximum x-value (real part)
05:     public const double YMin = -1.25;  // minimum y-value (imaginary part)
06:     public const double YMax = +1.25;  // maximum y-value (imaginary part)
07: 
08:     public const double XWidth  = XMax - XMin;  // range of x-values
09:     public const double YHeight = YMax - YMin;  // range of y-values
10: 
11:     public static Color MapColor(int count)
12:     {
13:         Color C = new Color();
14:         C.A = 255;
15:         C.B = (byte)(count / 100 * 25);
16:         count = count % 100;
17:         C.G = (byte)(count / 10 * 25);
18:         C.R = (byte)(count % 10 * 25);
19:         return C;
20:     }
21: 
22:     public static int Compute(Complex c)
23:     {
24:         Complex z = Complex.Zero;
25:         int count = 0;
26:         while (count < 1000 && z.Magnitude < 4)
27:         {
28:             z = z * z + c;
29:             count++;
30:         }
31:         return count;
32:     }
33: }

Beispiel 1. Hilfsklasse Mandelbrot.


Die Implementierung der Compute-Methode (Zeile 22 bis 32 von Listing 1) sollte mit den vorangegangenen Erläuterungen verständlich sein. Nicht ganz klar dürfte sein, wie den Punkten in der Mandelbrotmenge jetzt eine Farbe zugewiesen wird. Die maximale Iterationstiefe (Zeile 26 in Methode Compute) ist hier mit dem konstanten Wert 1000 festgelegt. Entsprechend benötigen wir 1000 verschiedene Farben, die sich idealerweise durch einen harmonischen Farbübergang auszeichnen. Wir stellen uns den RGB-Farbenraum nun als einen Würfel mit 10*10*10 verschiedenen Farben vor. Da jeder Farbanteil (Rot, Grün und Blau) durch Werte von 0 bis 255 spezifiziert werden kann, finden wir in diesem Würfel sicherlich nicht alle existierenden RGB-Farben vor. In unserer konkreten Situation stellt dies aber keine Einschränkung dar, im Gegenteil: Wenn wir einen Wert im Bereich von 1 bis 1000 gleichmäßig auf die 10 Ebenen eines Würfels mit 10 Reihen und 10 Spalten gleichmäßig abbilden können, erhalten wir für die Mandelbrotbilder eine sinnvolle Gleichverteilung der Farben. Diesen Zweck erfüllt die Methode MapColor in den Zeilen 11 bis 20 von Listing 1. Die Berechnung der drei Koordinaten x, y und z mit Werten von 1 bis 10 (genauer: 0 bis einschließlich 9) bei einem Ausgangswert count geht so:

int x_axis = count / 100;
count = count % 100;
int y_axis = count / 10;
int z_axis = count % 10;

Multiplizieren wir nun die drei Koordinaten x_axis, y_axis und z_axis noch mit 25, erhalten wir ansprechend gleichverteilte Farben des RGB-Farbraums. Damit können wir uns der eigentlichen Aufgabe zuwenden, Mandelbrotbilder in einem C#-Programm zu zeichnen.

Variation 1: Basisversion der Mandelbrot-Anwendung

Wir müssen vorab ein Pixelformat festlegen, das wir unseren Berechnungen zu Grunde legen. Ein populärer, weil sehr häufig verwendeter Kandidat ist das Bgra32-Format. Sein Format wird von den meisten Methoden der WritePixels-Methode unterstützt. Außerdem ist die Struktur des Formats leicht verständlich: Pro Pixel sind hier 4 Bytes nötig, neben den Farbanteilen für Blau, Grün und Rot (jeweils ein Byte mit den Werten 0 bis 255) gibt es noch den Alphakanal, der die Transparenz festlegt (Wertebereich ebenfalls 0 bis 255). Diese vier Werte sind für ein Pixel in vier aufeinander folgenden Bytes abzulegen.

Da bei allen Anwendungen dieser Heptalogie viele Berechnungen im Vordergrund stehen, habe ich diese komplett imperativ erstellt, also auf jeglichen XAML-Beschreibungscode verzichtet. Das Hauptfenster der ersten Mandelbrot-Anwendung finden Sie in Listing 2 vor:

01: class MandelbrotWindow : Window
02: {
03:     private const int WindowWidth = 500;
04:     private const int WindowHeight = 500;
05: 
06:     private Button b;
07:     private MandelbrotImage img;
08: 
09:     public MandelbrotWindow()
10:     {
11:         // setup window
12:         this.Title = "Mandelbrot Heptalogy - Part 1: Basic Version";
13:         this.Width = WindowWidth;
14:         this.Height = WindowHeight;
15:         this.Background = Brushes.White;
16: 
17:         // arrange child elements into a docking panel
18:         DockPanel dp = new DockPanel();
19:         dp.LastChildFill = true;
20:         this.Content = dp;
21: 
22:         // add button
23:         this.b = new Button();
24:         this.b.Content = "Paint Mandelbrot Picture ...";
25:         this.b.Click += new RoutedEventHandler(this.Button_Click);
26:         DockPanel.SetDock(this.b, Dock.Top);
27:         dp.Children.Add(this.b);
28: 
29:         // add image
30:         this.img = new MandelbrotImage(400, 400);
31:         this.img.XMin = Mandelbrot.XMin;
32:         this.img.XMax = Mandelbrot.XMax;
33:         this.img.YMin = Mandelbrot.YMin;
34:         this.img.YMax = Mandelbrot.YMax;
35: 
36:         dp.Children.Add(this.img);
37:     }
38: 
39:     private void Button_Click(Object sender, RoutedEventArgs e)
40:     {
41:         this.img.DrawMandelbrotSet();
42:     }
43: }

Beispiel 2. Klasse MandelbrotWindow.


Der Vollständigkeit halber führen wir noch die Main-Methode für das Hauptprogramm auf:

01: class Program
02: {
03:     [STAThread]
04:     public static void Main()
05:     {
06:         Application app = new Application();
07:         MandelbrotWindow w = new MandelbrotWindow();
08:         app.Run(w);
09:     }
10: }

Beispiel 3. Klasse Program: Einsprungpunkt der Anwendung.


Das Fensterobjekt MandelbrotWindow besitzt neben einer Schaltfläche ein Steuerelement vom Typ Image, das durch eine Spezialisierung (Klasse MandelbrotImage) um das Wissen einer Mandelbrotmenge ergänzt wird. Details dieser Klasse entnehmen Sie Listing 4. Vorab machen wir darauf aufmerksam, dass die Größe des MandelbrotImage-Steuerelements beim Aufruf des zugehörigen Konstruktors in Zeile 30 von Listing 2 mit konstanten Werten festgelegt wird. Aus diesem Grund besitzen die Mandelbrotbilder fürs Erste immer eine fixe Größe.

01: class MandelbrotImage : Image
02: {
03:     private WriteableBitmap bmp;
04: 
05:     // properties (excerpt from complex area)
06:     public double XMin { get; set; }
07:     public double XMax { get; set; }
08:     public double YMin { get; set; }
09:     public double YMax { get; set; }
10: 
11:     public MandelbrotImage(int width, int height)
12:     {  
13:         this.Width = width;
14:         this.Height = height;
15: 
16:         this.bmp = new WriteableBitmap(
17:             width,    // desired width of bitmap
18:             height,   // desired height of bitmap
19:             96,       // horizontal dots per inch (dpi) of the bitmap
20:             96,       // vertical dots per inch (dpi) of the bitmap
21:             PixelFormats.Bgra32,     // pixel format of bitmap
22:             null);    // bitmap palette, if any
23: 
24:         this.Source = this.bmp;
25:     }
26: 
27:     public void DrawMandelbrotSet()
28:     {
29:         int pixelHeight = this.bmp.PixelHeight;
30:         int pixelWidth = this.bmp.PixelWidth;
31: 
32:         int bytesPerPixel = this.bmp.Format.BitsPerPixel / 8;
33:         int stride = pixelWidth * bytesPerPixel;
34: 
35:         double xScale = (this.XMax - this.XMin) / pixelWidth;
36:         double yScale = (this.YMax - this.YMin) / pixelHeight;
37: 
38:         byte[,] pixels = new byte[pixelHeight, stride];
39:         for (int i = 0; i < pixelHeight; i++)
40:         {
41:             for (int j = 0; j < stride; j += bytesPerPixel)
42:             {
43:                 int dx = j / bytesPerPixel;
44: 
45:                 double x = this.XMin + dx * xScale;
46:                 double y = this.YMin + i * yScale;
47: 
48:                 Complex z = new Complex(x, y);
49:                 int count = Mandelbrot.Compute(z);
50:                 Color c = Mandelbrot.MapColor(count);
51: 
52:                 pixels[i, j] = c.B;
53:                 pixels[i, j + 1] = c.G;
54:                 pixels[i, j + 2] = c.R;
55:                 pixels[i, j + 3] = c.A;
56:             }
57:         }
58: 
59:         Int32Rect rect = new Int32Rect(0, 0, pixelWidth, pixelHeight);
60:         this.bmp.WritePixels(rect, pixels, stride, 0);
61:     }
62: }

Beispiel 4. Klasse MandelbrotImage.


Die Methode WritePixels (Zeile 60 von Listing 4) kann interessanterweise sowohl mit ein- wie auch zwei-dimensionalen Byte-Arrays aufgerufen werden. Aus diesem Grund ist die Schnittstelle dieser Methode so definiert:

public void WritePixels (
    Int32Rect sourceRect,  // rectangle of the bitmap to update
    Array pixels,          // pixel array used to update the bitmap
    int stride,            // stride of the update region in pixels
    int offset             // input buffer offset
);

Der abstrakte Klassentyp System.Array des Parameters pixels fungiert im Common Type System des .NET-Frameworks als universelle Basisklasse für Felder jeglicher Dimension. Im konkret vorliegenden Fall der Methode WritePixels müssen die Felder allerdings ein- oder zweidimensional sein, andernfalls wirft die Methode eine ArgumentException-Ausnahme.

In der Methode DrawMandelbrotSet aus Listing 4 (Zeile 27 bis 61) werden alle Pixel nach ihrer Berechnung mit einem einzigen Aufruf von WritePixels in das WriteableBitmap-Objekt geschrieben. Alternativ könnte man auch jedes berechnete Pixel sofort in das WriteableBitmap-Objekt schreiben. Wie Sie schon richtig vermuten, ist dieser Ansatz mit Sicherheit nicht schneller. Allerdings können wir auf diese Weise die Konzepte des strides wie auch die flexible Versorgung der WritePixels-Methode mit byte-Arrays noch einmal vertiefter studieren:

01: public void DrawMandelbrotSet()
02: {
03:     int pixelHeight = this.bmp.PixelHeight;
04:     int pixelWidth = this.bmp.PixelWidth;
05: 
06:     int bytesPerPixel = this.bmp.Format.BitsPerPixel / 8;
07:     int stride = bytesPerPixel;
08: 
09:     double xScale = (this.XMax - this.XMin) / pixelWidth;
10:     double yScale = (this.YMax - this.YMin) / pixelHeight;
11: 
12:     byte[] pixels = new byte[bytesPerPixel];
13:     for (int i = 0; i < pixelWidth; i++)
14:     {
15:         for (int j = 0; j < pixelHeight; j++)
16:         {
17:             double x = this.XMin + i * xScale;
18:             double y = this.YMin + j * yScale;
19: 
20:             Complex z = new Complex(x, y);
21:             int count = Mandelbrot.Compute(z);
22:             Color c = Mandelbrot.MapColor(count);
23: 
24:             pixels[0] = c.B;
25:             pixels[1] = c.G;
26:             pixels[2] = c.R;
27:             pixels[3] = c.A;
28: 
29:             Int32Rect rect = new Int32Rect(i, j, 1, 1);
30:             this.bmp.WritePixels(rect, pixels, stride, 0);
31:         }
32:     }
33: }

Beispiel 5. Alternative Realisierung der Methode DrawMandelbrotSet.


Wenn Sie das Programm aus Listing 4 (bzw. mit der Variation aus Listing 5) ausführen, werden Sie die Beobachtung machen, dass einige Sekunden vergehen, bis die berechneten Pixel sichtbar werden. Das wäre prinzipiell nicht so schlimm, immerhin sind ja eine ganze Menge an mathematischen Berechnungen zu erledigen. Unangenehmer ist allerdings der Umstand, dass das Hauptfenster der Anwendung für die Zeitdauer der Mandelbrotberechnungen eingefroren ist. Es ist weder die Fensterposition veränderbar, noch werden Aktionen des Benutzers wie Minimieren oder Maximieren des Fensters zur Kenntnis genommen. Positiv können wir zumindest vermerken, dass die Anwendung nicht abstürzt. Wenn Sie die Anwendung sorgfältig bedienen, werden Sie die Beobachtung machen, dass Ihre Bedienhandlungen nur zeitverzögert nach dem Aufblenden des Mandelbrotbilds zur Ausführung gelangen. Damit haben wir die Motivation für den nächsten Teilschritt gegeben.

Variation 2: Basisversion mit reaktionsfähigem Window-Objekt.

Für die Reaktionsfähigkeit einer Windows-Anwendung spielt der so genannte UI-Thread (engl. User Interface-Thread) eine besondere Rolle. Wir verstehen darunter den Primärthread einer oberflächenbehafteten .NET-Anwendung. In seinem Kontext werden alle ereignisspezifischen Methodenaufrufe wie etwa OnMouseDown, Loaded, OnClosing und OnClosed usw. einer nach dem anderen abgearbeitet. Wir erkennen bereits an dieser Stelle, das sich eine .NET-Anwendung, die Wert auf das Attribut bedienerfreundlich legt, nicht allzu viel Zeit für die Bearbeitung dieser einzelnen Methoden nehmen darf, da sonst eben das gewohnte Verhalten der Anwendung verloren geht bzw. sich erst mit einer entsprechenden zeitlichen Verzögerung einstellt.

Für den Fall, dass die Ausführung einer derartigen Methode dennoch extrem viel Zeit verlangt (und dies ist genau der Fall in der Implementierung der Berechnungen einer Mandelbrot-Anwendung), müssen wir auf das Instrumentarium des Multithreadings zurückgreifen. Durch die nebenläufige Ausführung aller Threads auf einem Rechner erreichen wir, dass sowohl die ereignisspezifischen Methoden der Anwendung zur Ausführung gelangen (und die Anwendung damit bedienbar bleibt) als auch parallel dazu Langläufer-Methoden abgearbeitet werden.

Konkret: Zuerst müssen wir die Schwachstelle der Button_Click-Methode aus Listing 2 beseitigen und alle Berechnungen innerhalb dieser Methode in einen separaten Thread auslagern. Mit dieser Verlagerung ist die Anwendung sofort reaktionsfähig geworden, da die Button_Click-Methode die Abarbeitung der Nachrichtenwarteschlange des UI-Threads nicht mehr unnötig blockiert. Ganz auf die Schnelle sind wir jetzt aber mit einem zweiten Problem konfrontiert, nämlich dass im Kontext des Sekundärthreads keinerlei Zugriffe auf die Anwendungsoberfläche möglich sind. An dieser Stelle kommt das Dispatcher-Objekt zum Tragen. Alle berechneten Pixel bzw. deren Visualisierung müssen wieder in den UI-Thread transferiert werden. In der Tat entsteht auf diese Weise ein gewisser Overhead in der Anwendung, der sich möglicherweise optimieren, aber nicht in Gänze umgehen lässt. Betrachten wir zunächst das Redesign des Hauptfensters der Mandelbrotanwendung in Listing 6. Die Arbeit, die zum Berechnen der Mandelbrotmenge erforderlich ist, wird in Zeile 8 komplett in eine Hintergrundaufgabe ausgelagert, die Button_Click-Methode wird danach unverzüglich verlassen:

01: class MandelbrotWindow : Window
02: {
03:     private MandelbrotImage img;
04:     ...
05: 
06:     public void Button_Click(Object sender, RoutedEventArgs e)
07:     {
08:         if (this.img.IsActive)
09:             return;
10: 
11:         this.img.ClearBitmap();
12: 
13:         this.img.IsActive = true;
14: 
15:         this.task =
16:             Task.Factory.StartNew(() => { this.img.DrawMandelbrotSet(); });
17:     }
18: }

Beispiel 6. Überarbeitung der Methode Button_Click aus der Klasse MandelbrotWindow.


Die weitaus größeren Umstrukturierungstätigkeiten sind in der Methode DrawMandelbrotSet der Klasse MandelbrotImage zu erbringen, siehe ihr Redesign in Listing 7:

001: class MandelbrotImage : Image
002: {
003:     private WriteableBitmap bmp;
004: 
005:     // properties (excerpt from complex area)
006:     public double XMin { get; set; }
007:     public double XMax { get; set; }
008:     public double YMin { get; set; }
009:     public double YMax { get; set; }
010: 
011:     public bool IsActive { get; set; }
012: 
013:     public MandelbrotImage(int width, int height)
014:     {
015:         this.Width = width;
016:         this.Height = height;
017: 
018:         this.bmp = new WriteableBitmap(
019:             width,    // desired width of bitmap
020:             height,   // desired height of bitmap
021:             96,       // horizontal dots per inch (dpi) of the bitmap
022:             96,       // vertical dots per inch (dpi) of the bitmap
023:             PixelFormats.Bgra32,     // pixel format of bitmap
024:             null);    // bitmap palette, if any
025: 
026:         this.Source = this.bmp;
027:     }
028: 
029:     // public interface
030:     public void DrawMandelbrotSet()
031:     {
032:         int pixelHeight = 0;
033:         int pixelWidth = 0;
034:         int bytesPerPixel = 0;
035: 
036:         Dispatcher.Invoke(
037:             new Action(delegate() { pixelHeight = (int)this.bmp.PixelHeight; }),
038:             null);
039:         Dispatcher.Invoke(
040:             new Action(delegate() { pixelWidth = (int)this.bmp.PixelWidth; }),
041:             null);
042:         Dispatcher.Invoke(
043:             new Action(delegate() { bytesPerPixel = bmp.Format.BitsPerPixel / 8; }),
044:             null);
045: 
046:         int stride = pixelWidth * bytesPerPixel;
047:         double xScale = (this.XMax - this.XMin) / pixelWidth;
048:         double yScale = (this.YMax - this.YMin) / pixelHeight;
049: 
050:         byte[,] pixels = new byte[pixelHeight, stride];
051:         for (int i = 0; i < pixelHeight; i++)
052:         {
053:             for (int j = 0; j < stride; j += bytesPerPixel)
054:             {
055:                 if (!this.IsActive)
056:                     return;
057: 
058:                 int dx = j / bytesPerPixel;
059: 
060:                 double x = this.XMin + dx * xScale;
061:                 double y = this.YMin+ i * yScale;
062: 
063:                 Complex z = new Complex(x, y);
064:                 int count = Mandelbrot.Compute(z);
065:                 Color c = Mandelbrot.MapColor(count);
066: 
067:                 pixels[i, j] = c.B;
068:                 pixels[i, j + 1] = c.G;
069:                 pixels[i, j + 2] = c.R;
070:                 pixels[i, j + 3] = c.A;
071:             }
072:         }
073: 
074:         SetPixelHandler_TwoDimArray h = new SetPixelHandler_TwoDimArray(this.SetPixels);
075:         Object[] args = new Object[] { pixelWidth, pixelHeight, stride, pixels };
076:         Dispatcher.BeginInvoke(h, DispatcherPriority.SystemIdle, args);
077: 
078:         this.IsActive = false;
079:     }
080: 
081:     private delegate void SetPixelHandler_TwoDimArray
082:         (int pixelWidth, int pixelHeight, int stride, byte[,] pixels);
083: 
084:     private void SetPixels(int pixelWidth, int pixelHeight, int stride, byte[,] pixels)
085:     {
086:         Int32Rect rect = new Int32Rect(0, 0, pixelWidth, pixelHeight);
087:         this.bmp.WritePixels(rect, pixels, stride, 0);
088:     }
089: 
090:     public void ClearBitmap()
091:     {
092:         int pixelHeight = this.bmp.PixelHeight;
093:         int pixelWidth = this.bmp.PixelWidth;
094:         int bytesPerPixel = this.bmp.Format.BitsPerPixel / 8;
095:         int stride = pixelWidth * bytesPerPixel;
096: 
097:         byte[,] pixels = new byte[pixelHeight, stride];
098:         for (int i = 0; i < pixelHeight; i++)
099:         {
100:             for (int j = 0; j < stride; j += bytesPerPixel)
101:             {
102:                 pixels[i, j] = 255;
103:                 pixels[i, j + 1] = 255;
104:                 pixels[i, j + 2] = 255;
105:                 pixels[i, j + 3] = 255;
106:             }
107:         }
108: 
109:         Int32Rect rect = new Int32Rect(0, 0, pixelWidth, pixelHeight);
110:         this.bmp.WritePixels(rect, pixels, stride, 0);
111:     }
112: }

Beispiel 7. Redesign der Klasse MandelbrotImage.


In den Zeilen 36, 39 und 42 wird nun das Dispatcher-Objekt aktiv. Zuerst werden die drei Eigenschaften PixelHeight, PixelWidth und BitsPerPixel des WriteableBitmap-Objekts bestimmt. Bitte beachten Sie: Zugriffe auf ein WriteableBitmap-Objekt sind ausschließlich im Kontext des UI-Threads möglich! Ohne Verwendung des Dispatcher-Objekts würden Zugriffe auf diese Variablen eine Ausnahme des Typs InvalidOperationException (The calling thread cannot access this object because a different thread owns it) nach sich ziehen! Um nach den Berechnungen der Pixel diese darstellen zu können, müssen wir den Aufruf der WritePixels-Methode wieder in den UI-Thread verlagern. Dazu gibt es in den Zeilen 84 bis 88 eine Hilfsmethode SetPixels, deren Methodenschnittstelle in Zeile 81 durch den dazu passenden Delegate-Typ SetPixelHandler_TwoDimArray beschrieben wird. Der Kontextwechsel von der Sekundärtask in den UI-Thread findet in Zeile 76 mit dem Aufruf der BeginInvoke-Methode statt.

Wenn Sie Listing 7 aufmerksam studiert haben, müsste Ihnen aufgefallen sein, dass in den Zeilen 36, 39 und 42 die Methode Invoke am Dispatcher-Objekt aufgerufen wurde, in Zeile 76 hingegen BeginInvoke. Die Invoke-Methodenaufrufe sind zeitaufwändiger. Es wird die rufende Task solange blockiert, bis das in den UI-Thread delegierte Codefragment ausgeführt wurde! Dies ist in den Zeilen 36, 39 und 42 zwingend notwendig, da die gesuchten Werte vorliegen müssen, bevor in der aktuellen Methode weitergearbeitet werden kann. In Zeile 76 hingegen kann die Sekundärtask durchaus weiterlaufen, es genügt zu wissen, dass in absehbarer Zeit Pixel gezeichnet werden.

Achtung

Beachten Sie in Zeile 76 von Listing 7 den Aktualparameter vom Typ DispatcherPriority. Wenn Sie hier nicht defensiv mit der Priorität SystemIdle arbeiten, wird ihr Programm ein weitaus schlechteres Laufzeitverhalten aufweisen!

Für die Zeilen 74 bis 76 gibt es vielfältige Möglichkeiten einer programmiersprachlichen Ausdrucksweise. Etwas kompakter, aber nicht mehr ganz so übersichtlich, ist die folgende Formulierung des BeginInvoke-Methodenaufrufs:

Dispatcher.BeginInvoke(
    new Action<int, int, int, byte[,]>
    (delegate(int n, int m, int s, byte[,] p)
    {
        this.SetPixels(n, m, s, pixels);
    }),
    DispatcherPriority.SystemIdle,
    new Object[] { pixelWidth, pixelHeight, stride, pixels }
);

Nicht ganz so kompakt, dafür ohne explizite Definition eines Delegate-Typs ginge es auch so:

Action<int, int, int, byte[,]> action = new Action<int, int, int, byte[,]>
    (delegate (int n, int m, int s, byte[,] p) { this.SetPixels(n, m, s, p); });
Object[] args = new Object[] { pixelWidth, pixelHeight, stride, pixels };
Dispatcher.BeginInvoke(action, DispatcherPriority.SystemIdle, args);

Die Einbeziehung einer Task (eines Threads) in die Anwendung erfordert, dass wir auch deren Beendigung in unsere Überlegungen mit einbeziehen. Prinzipiell ist eine Task immer dann beendet, wenn die in ihrem Kontext aufgerufene Methode verlassen wurde. Im Regelfall ist dies in unserem Beispiel immer dann der Fall, wenn das Mandelbrot-Bild komplett gezeichnet wurde. Es wäre dann auch nichts weiter zu tun. Da unsere Anwendung jetzt aber reaktionsfähig ist, könnten wir sie auch vorzeitig beenden. Ein initiierter Thread würde dann mit den Berechnungen der einzelnen Pixel weiterarbeiten.

Dieser Aspekt ist etwas mühsam in der Anwendung zu behandeln. Das Beenden einer Anwendung (egal, ob vorzeitig oder nicht) bekommt diese durch den Aufruf der (zu überschreibenden) Methode OnClosing mitgeteilt. Hier kommt nun eine bool-Variable (bzw. Eigenschaft) ins Spiel, die wir an der Klasse MandelbrotImage ergänzen, wir nennen sie IsActive (Zeile 11 in Listing 7). Wollen wir zum Ausdruck bringen, dass ein laufender Thread nicht mehr benötigt wird, setzen wir ihren Wert auf false. In der Thread-Prozedur wiederum ist diese Variable (Eigenschaft) an zentralen Stellen zu überprüfen und die Thread-Prozedur ggf. vorzeitig zu verlassen.

Sowohl die Hauptfensterklasse MandelbrotWindow wie auch die Klasse MandelbrotImage aus Listing 7 sollten Sie nun in Bezug auf die Variable IsActive eingehend studieren. Das kontrollierte, vorzeitige Verlassen von Thread-Prozeduren ist ein wichtiges Thema, dem wir immer wieder begegnen werden. Die Erweiterungen an der MandelbrotWindow-Klasse entnehmen Sie bitte Listing 8:

01: class MandelbrotWindow : Window
02: {
03:     ...
04: 
05:     private Task task;
06: 
07:     public MandelbrotWindow()
08:     {
09:         // setup window
10:         this.Title = "Mandelbrot Heptalogy - Part 2: Responsive Window";
11:         ...
12: 
13:         // add image
14:         this.img = new MandelbrotImage(400, 400);
15:         this.img.XMin = Mandelbrot.XMin;
16:         this.img.XMax = Mandelbrot.XMax;
17:         this.img.YMin = Mandelbrot.YMin;
18:         this.img.YMax = Mandelbrot.YMax;
19: 
20:         this.img.IsActive = false;
21:         
22:         dp.Children.Add(this.img);
23:     }
24: 
25:     public void Button_Click(Object sender, RoutedEventArgs e)
26:     {
27:         if (this.img.IsActive)
28:             return;
29: 
30:         this.img.ClearBitmap();
31: 
32:         this.img.IsActive = true;
33: 
34:         this.task =
35:             Task.Factory.StartNew(() => { this.img.DrawMandelbrotSet(); });
36:     }
37:     
38:     protected override void OnClosing(CancelEventArgs e)
39:     {
40:         base.OnClosing(e);
41: 
42:         // cancel task, if any 
43:         this.img.IsActive = false;
44: 
45:         if (this.task != null)
46:             this.task.Wait();
47:     }
48: }

Beispiel 8. Redesign der Klasse MandelbrotWindow – Erweiterungen.

Variation 3: Basisversion mit reaktionsfähigem Image-Objekt.

Vereinfachend zu den bisherigen Lösungen lassen wir die Schaltfläche zum Starten eines Zeichenvorgangs von nun an weg. Das Fensterobjekt beherbergt jetzt nur noch ein Image-Objekt, dessen Größe gemäß unserer Zielsetzung bei jeder Größenveränderung des umgebenden Fensterobjekts anzupassen ist. Um Problemen mit der aktuellen Größe des inneren Bereichs des Hauptfensters (client area) aus dem Weg zu gehen, betten wir das Image-Objekt in einen Canvas-Container ein. In Listing 9 finden Sie die überarbeitete Spezialisierung der Basisklasse Window vor, die für SizeChanged-Ereignisse eine Ereignishandlermethode MandelbrotWindow_SizeChanged bereitstellt:

01: class MandelbrotWindow : Window
02: {
03:     private const int WindowWidth = 500;
04:     private const int WindowHeight = 500;
05: 
06:     private MandelbrotImage img;
07:     private Canvas canvas;
08: 
09:     public MandelbrotWindow()
10:     {
11:         // setup window
12:         this.Title = "Mandelbrot Heptalogy - Part 3: Responsive Picture";
13:         this.Width = WindowWidth;
14:         this.Height = WindowHeight;
15:         this.Background = Brushes.White;
16:         this.SizeChanged +=
17:             new SizeChangedEventHandler(this.MandelbrotWindow_SizeChanged);
18: 
19:         // arrange sinngle child element into a canvas panel
20:         this.canvas = new Canvas();
21:         this.Content = this.canvas;
22: 
23:         // add image
24:         this.img = new MandelbrotImage(0, 0);
25:         this.img.XMin = Mandelbrot.XMin;
26:         this.img.XMax = Mandelbrot.XMax;
27:         this.img.YMin = Mandelbrot.YMin;
28:         this.img.YMax = Mandelbrot.YMax;
29:         this.canvas.Children.Add(this.img);
30:     }
31: 
32:     private void MandelbrotWindow_SizeChanged(Object sender, SizeChangedEventArgs e)
33:     {
34:         this.img.UpdateSize(this.canvas.ActualWidth, this.canvas.ActualHeight); 
35:     }
36: }

Beispiel 9. Klasse MandelbrotWindow – in der dritten Variation.


Die Hauptarbeit des Beispiels ist wieder in der Klasse MandelbrotImage verborgen. Diese besitzt wie schon zuvor zur Darstellung des Bildes ein Steuerelement des Typs WriteableBitmap. Das zeitnahe Weiterreichen der aktuellen Fenstergröße an das MandelbrotImage- und damit an das WriteableBitmap-Objekt ist kein ganz einfaches Unterfangen. Wir sind mit den folgenden drei Aufgaben konfrontiert:

  • Aktuellen Zeichnenvorgang beenden, sofern dieser noch tätig ist.

  • Das aktuell sichtbare Image-Objekt in seiner Größe verändern, das unterlagerte WriteableBitmap-Objekt neu anlegen, da dessen Größe unveränderbar ist.

  • Neuen Zeichnenvorgang auf Basis der aktualisierten Abmessungen starten.

Diese drei Aufgaben studieren wir nun in Listing 10, die Namen der Methoden StopDrawing, ResizeImage sowie StartDrawingLinePerLine und DrawNextMandelbrotPixelLine stellen den Zusammenhang her:

001: class MandelbrotImage : Image
002: {
003:     private WriteableBitmap bmp;
004:     private int nextRow;
005: 
006:     private double xScale;
007:     private double yScale;
008: 
009:     private DispatcherOperation lastOperation;
010: 
011:     // properties (excerpt from complex area)
012:     public double XMin { get; set; }
013:     public double XMax { get; set; }
014:     public double YMin { get; set; }
015:     public double YMax { get; set; }
016:     
017:     public MandelbrotImage(double width, double height)
018:     {
019:         this.Width = width;
020:         this.Height = height;
021:     }
022: 
023:     // public methods
024:     public void UpdateSize(double width, double height)
025:     {
026:         this.StopDrawing();
027:         this.ResizeImage(width, height);
028:         this.StartDrawingLinePerLine((int)width, (int)height);
029:     }
030: 
031:     // private helper methods
032:     private void StopDrawing()
033:     {
034:         if (this.lastOperation != null)
035:         {
036:             if (this.lastOperation.Status != DispatcherOperationStatus.Completed)
037:             {
038:                 this.lastOperation.Abort();
039:                 this.lastOperation.Wait();
040:                 this.lastOperation = null;
041:             }
042:         }
043:     }
044: 
045:     private void ResizeImage(double width, double height)
046:     {
047:         this.Width = width;
048:         this.Height = height;
049: 
050:         this.bmp = new WriteableBitmap(
051:             (int) width,   // desired width of bitmap
052:             (int) height,  // desired height of bitmap
053:             96,            // horizontal dots per inch (dpi) of the bitmap
054:             96,            // vertical dots per inch (dpi) of the bitmap
055:             PixelFormats.Bgra32,     // pixel format of bitmap
056:             null);         // bitmap palette, if any
057: 
058:         this.Source = this.bmp;
059:     }
060: 
061:     private void StartDrawingLinePerLine(int width, int height)
062:     {
063:         Console.WriteLine("StartDrawingLinePerLine [{0}, {1}].", width, height); 
064:         
065:         this.nextRow = 0;
066: 
067:         this.xScale = (this.XMax - this.XMin) / width;
068:         this.yScale = (this.YMax - this.YMin) / height;
069: 
070:         int bytesPerPixel = this.bmp.Format.BitsPerPixel / 8;
071: 
072:         // initiate drawing pixels
073:         this.lastOperation = this.Dispatcher.BeginInvoke(
074:             (Action)
075:             (() => { this.DrawNextMandelbrotPixelLine(width, height, bytesPerPixel); }),
076:             DispatcherPriority.SystemIdle, null);
077:     }
078: 
079:     private void DrawNextMandelbrotPixelLine(int width, int height, int bytesPerPixel)
080:     {
081:         int stride = width * bytesPerPixel;
082: 
083:         // calculate pixel
084:         byte[] pixels = new byte[stride];
085:         for (int j = 0; j < stride; j += bytesPerPixel)
086:         {
087:             int dx = j / bytesPerPixel;
088:             double x = this.XMin + dx * this.xScale;
089:             double y = this.YMin + this.nextRow * this.yScale;
090: 
091:             Complex z = new Complex(x, y);
092:             int count = Mandelbrot.Compute(z);
093:             Color c = Mandelbrot.MapColor(count);
094: 
095:             pixels[j] = c.B;
096:             pixels[j + 1] = c.G;
097:             pixels[j + 2] = c.R;
098:             pixels[j + 3] = c.A;
099:         }
100: 
101:         // draw pixels into bitmap
102:         Int32Rect rect = new Int32Rect(0, this.nextRow, width, 1);
103:         this.bmp.WritePixels(rect, pixels, stride, 0);
104: 
105:         // update row count
106:         this.nextRow++;
107: 
108:         // schedule same routine for next line
109:         if (this.nextRow < height)
110:         {
111:             this.lastOperation = this.Dispatcher.BeginInvoke(
112:                 (Action)
113:                 (() => { this.DrawNextMandelbrotPixelLine(width, height, bytesPerPixel); }),
114:                 DispatcherPriority.SystemIdle, null);
115:         }
116:     }
117: }

Beispiel 10. Klasse MandelbrotImage – in der dritten Variation.


Wir steigen in Listing 9 in Zeile 32 mit Betrachtung der MandelbrotWindow_SizeChanged-Methode ein. Diese wird prinzipiell bei Größenveränderungen des MandelbrotWindow-Fensterobjekts aufgerufen, da sie in Zeile 17 am SizeChanged-Ereignis angemeldet ist. Folgende Tätigkeiten sind bei einer Größenveränderung des Fensters auszuführen: Beenden des aktuellen Zeichnens eines Mandelbrotbilds, Größe des vorliegenden Image-Objekts und des unterlagerten WriteableBitmap-Objekts anpassen und danach, in Bezug auf die neuen Größenverhältnisse, ein neues Mandelbrotbild zeichnen. Diese drei Aktionen sind auf die drei Methoden StopDrawing, ResizeImage und StartDrawingLinePerLine aufgeteilt (Listing 10). Die Realisierung der ResizeImage-Methode ist selbsterklärend, bzgl. des Pixelformats des zugeordneten WriteableBitmap-Objekts ist die Wahl wieder auf das Bgra32-Format gefallen.

Nun zur StartDrawingLinePerLine-Methode. Wie der Name schon ausdrückt, zeichnen wir das Mandelbrotbild Zeile für Zeile. In welchem Threadkontext? Ganz einfach, im Kontext des UI-Threads, der für das Zeichnen zuständig ist. Dafür gibt es eine Hilfsmethode DrawNextMandelbrotPixelLine in den Zeilen 79 bis 116. Diese benötigt einige korrekt belegte Instanzvariablen wie zum Beispiel die aktuelle Größe des WriteableBitmap-Objekts oder auch Skalierungskonstanten, um die Mandelbrotwerte in Abhängigkeit von der Position im Fenster zu berechnen. Diese werden in den Zeilen 65 bis 70 belegt, danach wird mit einem Aufruf von BeginInvoke ein Aufruf von DrawNextMandelbrotPixelLine in den UI-Thread der Anwendung eingeschleust (Zeile 73 bis 76). Das Zeichnen einer Pixelzeile enthält qualitativ nichts Neues, viel interessanter ist die Frage, wie es danach weiter geht. Um einerseits höherpriore Threads bzw. nebenläufige Aktivitäten nicht unnötig von der Arbeit abzuhalten und andererseits das Zeichnen von Pixeln im Kontext des UI-Thread zu belassen, initiieren wir einfach mit BeginInvoke einen weiteren Aufruf der DrawNextMandelbrotPixelLine-Methode, der folglich wieder im Kontext des UI-Threads stattfindet. Dies wird in den Zeilen 111 bis 114 veranlasst. Nun gilt es nur noch das Problem zu betrachten, dass eine bereits gemalte Pixelzeile nicht wiederholt gezeichnet wird. Zu diesem Zweck gibt es eine Instanzvariable nextRow, sie verwaltet die nächste zu zeichnende Zeile. Auf diese Weise erfolgt das zeilenweise Zeichnen eines Mandelbrotbilds quasi selbstorganisierend im UI-Thread der Anwendung! Andere Tasks bzw. Threads werden auf diese Weise nicht gestört, da nur dann gemalt wird, wenn sich das System im Zustand DispatcherPriority.SystemIdle befindet.

Jetzt ist nur noch ein Problem zu beachten: Wie kann dieses selbstorganisierte Zeichnen rechtzeitig abgebrochen werden, wenn sich zwischenzeitlich die beteiligten Image-und WriteableBitmap-Objekte in ihrer Größe verändert haben? An dieser Stelle sollte man erwähnen, dass beim Eintreten eines SizeChanged-Ereignisses zwei Einträge in der Warteschlange des Dispatcher-Objekts vorliegen. Ein Eintrag für das SizeChanged-Ereignis selbst und ein zweiter Eintrag für einen Aufruf der DrawNextMandelbrotPixelLine-Methode. Vermutlich werden in der Warteschlange noch weitere Einträge vorhanden sein, sie spielen nur für diese für diese Überlegungen keine Rolle. Da der DrawNextMandelbrotPixelLine-Methodenaufruf mit niedrigster Priorität zur Abarbeitung gelangt, wird das SizeChanged-Ereignis vorrangig behandelt. Oder anders herum ausgedrückt: Wenn das SizeChanged-Ereignis in Bearbeitung ist, steht noch ein mit BeginInvoke eingeschleuster DrawNextMandelbrotPixelLine-Methodenaufruf in der Warteschlange zur Ausführung an. Genau dieser Aufruf darf nicht mehr stattfinden, da in der Zwischenzeit ein anderes WriteableBitmap-Objekt mit einer anderen Größe angelegt wurde. Würde der Aufruf von DrawNextMandelbrotPixelLine noch stattfinden, würde eine Pixelzeile an einer falschen Stelle gezeichnet werden, was Sie möglicherweise als nicht ganz so dramatisch ansehen, sofern wir dies überhaupt optisch wahrnehmen würden. Weitaus empfindlicher schmerzt uns eher der Absturz der gesamten Anwendung, denn wenn das neue WriteableBitmap-Objekt kleiner ist als das vorhergehende, produzieren wir mit den Längenangaben einer Pixelzeile eine System.ArgumentException-Ausnahme (WriteableBitmap.WritePixels: Value does not fall within the expected range).

An dieser Stelle kommt der Rückgabewert von BeginInvoke ins Spiel. Das durch BeginInvoke in die Warteschlange eingereihte Element wird im Laufe seiner Bearbeitung durch ein DispatcherOperation-Objekt beobachtet. An diesem Objekt können wir zum einen den Zustand des in Bearbeitung befindlichen Ereignisses abfragen. Sollte dieser vom Wert ungleich DispatcherOperationStatus.Completed sein, wurde das Ereignis noch nicht ausgeführt. Mit der Abort-Methode entfernen wir das Element aus der Dispatcher-Warteschlange, es kann in Folge dessen nicht zu einem Absturz der Anwendung kommen. In den Zeilen 34 bis 42 von Listing 10 finden Sie die Umsetzung dieser Erläuterungen vor.

Sie sehen, dass Beispiel aus Listing 10 hatte es in sich. Wenn Sie die Anwendung ausführen und die Größe des Fensters mit dem Mauszeiger verändern, machen Sie die Beobachtung, dass es die Mühe wert war. Im Ruhezustand berechnet und zeichnet die Anwendung vergleichsweise schnell ein Mandelbrotbild. Im anderen Fall werden neue Image- und WriteableBitmap-Objekte angelegt und nicht durch obsolete WritePixels-Aufrufe zum Absturz gebracht!

Noch ein letzter Hinweis: In der dritten Variante müssen wir uns mit dem vorzeitigen Ende von Threads nicht auseinandersetzen. Der UI-Thread läuft unter der Obhut des Hauptfensters bzw. der Laufzeitumgebung, er wird beim Schließen der Anwendung kontrolliert beendet.

Variation 4: Parallelisierung mit Tasks

In den ersten drei Variationen stand der Entwurf einer WPF-Anwendung mit einem Image- bzw. WriteableBitmap-Objekt in Verbindung mit einem hohen Aufwand an Rechenleistung im Vordergrund. In den nun folgenden Variationen fügen wir diesem Anforderungsprofil nun noch den Aspekt der Parallelisierung hinzu. Auf modernen Rechnern, wie etwa einem Mehrkernsystem, bietet es sich an, die immensen Anforderungen an die Rechenleistung (in unserem Beispiel: die Berechnung der Mandelbrotfolgen) auf mehrere Kerne zu verteilen. Da die berechneten Resultate auch grafisch angezeigt werden, ist dieser Umstieg zusätzlich mit der einen oder anderen Hürde verbunden, die wir im Folgenden näher betrachten wollen.

Um das Hauptfenster der WPF-Anwendung in mehrere rechteckige Bereiche zu untergliedern, fügen wir auf der obersten Ebene ein UniformGrid-Steuerelement ein. Dieses wiederum wird gemäß seiner Zeilen- und Spaltenanzahl mit entsprechend vielen MandelbrotImage-Steuerelementen aufgefüllt. Die Klasse MandelbrotImage ist in dieser Variation natürlich wieder einem Redesign zu unterwerfen, wir verweilen zunächst bei der Klasse MandelbrotWindow des Hauptfensters. Verändert dieses seine Größe, sind alle unterlagerten MandelbrotImage-Objekte, so wie bereits in der dritten Variation betrachtet, ebenfalls davon in Kenntnis zu setzen. Neben dem Konstruktor, der den grundlegenden Aufbau des WPF-Fensterobjekts durchführt, benötigen wir noch eine Handlermethode für das SizeChanged-Ereignis in der Klasse MandelbrotWindow (Listing 11):

01: class MandelbrotWindow : Window
02: {
03:     private const int WindowWidth = 500;
04:     private const int WindowHeight = 500;
05: 
06:     private const int GridCols = 2;
07:     private const int GridRows = 2;
08:     private const int GapSize = 4;
09: 
10:     private UniformGrid grid;
11: 
12:     public MandelbrotWindow()
13:     {
14:         // setup window
15:         this.Title = "Mandelbrot Heptalogy - Part 4: Using Tasks";
16:         this.Width = WindowWidth;
17:         this.Height = WindowHeight;
18:         this.Background = Brushes.White;
19: 
20:         // add a uniform grid into the window
21:         this.grid = new UniformGrid();
22:         this.grid.Columns = GridCols;
23:         this.grid.Rows = GridRows;
24:         this.Content = this.grid;
25: 
26:         // calculate excerpts from Mandelbrot for picture
27:         double xDist = (Mandelbrot.XMax - Mandelbrot.XMin) / GridCols;
28:         double yDist = (Mandelbrot.YMax - Mandelbrot.YMin) / GridRows; 
29:         for (int i = 0; i < GridRows; i++)
30:         {
31:             for (int j = 0; j < GridCols; j++)
32:             {
33:                 MandelbrotImage img = new MandelbrotImage(0, 0);
34: 
35:                 img.XMin = Mandelbrot.XMin + j * xDist;
36:                 img.XMax = Mandelbrot.XMin + (j + 1) * xDist;
37:                 img.YMin = Mandelbrot.YMin + i * yDist;
38:                 img.YMax = Mandelbrot.YMin + (i + 1) * yDist;
39: 
40:                 this.grid.Children.Add(img);
41:             }
42:         }
43: 
44:         this.SizeChanged +=
45:             new SizeChangedEventHandler(this.MandelbrotWindow_SizeChanged);
46:     }
47: 
48:     private void MandelbrotWindow_SizeChanged(Object sender, SizeChangedEventArgs e)
49:     {
50:         double currentWidth = (this.grid.ActualWidth / GridCols) - GapSize;
51:         double currentHeight = (this.grid.ActualHeight / GridRows) - GapSize;
52: 
53:         for (int i = 0; i < this.grid.Children.Count; i++)
54:         {
55:             MandelbrotImage img = (MandelbrotImage)this.grid.Children[i];
56:             img.UpdateSize(currentWidth, currentHeight);
57:         }
58:     }
59: 
60:     protected override void OnClosing(CancelEventArgs e)
61:     {
62:         base.OnClosing(e);
63: 
64:         for (int i = 0; i < this.grid.Children.Count; i++)
65:         {
66:             MandelbrotImage img = (MandelbrotImage)this.grid.Children[i];
67:             img.StopDrawing();
68:         }
69:     }
70: }

Beispiel 11. Klasse MandelbrotWindow der vierten Variation.


Die Zusammenführung einer Größenveränderung des Hauptfensters mit seinen unterlagerten MandelbrotImage-Objekten finden wir in Listing 11 in den Zeilen 53 bis 56 vor: Der Aufruf der UpdateSize-Methode schleust die neuen Ausmaße an alle im Fenster enthaltenen Steuerelemente durch. Da in allen unterlagerten MandelbrotImage-Objekten Tasks am arbeiten sind, fängt die eigentliche Arbeit erst jetzt richtig an. Folgende Tätigkeiten sind im Einzelnen auszuführen:

  • Die aktuell laufenden Tasks müssen vorzeitig beendet werden, sofern sie noch am Laufen sind.

  • Die aktuell sichtbaren Image- und WriteableBitmap-Objekte müssen entweder in ihrer Größe verändert werden (Klasse Image) oder aber neu erzeugt werden, wenn sie unveränderbar sind (Klasse WriteableBitmap).

  • Neue Tasks zum Berechnen der Mandelbrotbilder auf Basis der neuen Abmessungen sind zu starten.

Mit diesen drei Anforderungen vor Augen können wir jetzt eine adäquate Realisierung einer Klasse MandelbrotImage in Listing 12 betrachten:

001: class MandelbrotImage : Image
002: {
003:     private WriteableBitmap bmp;
004: 
005:     private Task task;
006:     private CancellationTokenSource cancelSource;
007: 
008:     private long activePicture;
009: 
010:     // properties (excerpt from complex area)
011:     public double XMin { get; set;}
012:     public double XMax { get; set; }
013:     public double YMin { get; set; }
014:     public double YMax { get; set; }
015: 
016:     // c'tor
017:     public MandelbrotImage(double width, double height)
018:     {
019:         this.Width = width;
020:         this.Height = height;
021: 
022:         this.activePicture = 0;
023:     }
024: 
025:     // public methods
026:     public void UpdateSize(double width, double height)
027:     {
028:         this.StopDrawing();
029:         this.ResizeImage(width, height);
030:         this.StartDrawingTask((int)width, (int)height);
031:     }
032: 
033:     public void StopDrawing()
034:     {
035:         // cancel task
036:         if (this.task != null)
037:         {
038:             this.cancelSource.Cancel();
039:             this.task.Wait();
040:         }
041:     }
042: 
043:     // private helper methods
044:     private void ResizeImage(double width, double height)
045:     {
046:         this.Width = width;
047:         this.Height = height;
048:         this.bmp =  new WriteableBitmap((int)width, (int)height, 96, 96, PixelFormats.Bgra32, null);
049:         this.Source = this.bmp;
050:     }
051: 
052:     private void StartDrawingTask(int pixelWidth, int pixelHeight)
053:     {
054:         Interlocked.Increment(ref this.activePicture);
055:         
056:         TaskData data = new TaskData();
057:         data.PixelWidth = pixelWidth;
058:         data.PixelHeight = pixelHeight;
059:         data.BytesPerPixel = this.bmp.Format.BitsPerPixel / 8;
060:         data.ActivePicture = this.activePicture;
061: 
062:         this.task = new Task(this.DrawingTask, data);
063:         this.cancelSource = new CancellationTokenSource();
064:         this.task.Start();
065:     }
066: 
067:     private delegate void DrawPixelLineHandler(
068:         int line, int pixelWidth, int stride, byte[] pixels, long active);
069: 
070:     private void DrawPixelLine(int line, int pixelWidth,
071:         int stride, byte[] pixels, long active)
072:     {
073:         long globalActive = Interlocked.Read(ref this.activePicture);
074:         if (globalActive == active)
075:         {
076:             // draw pixel line into bitmap
077:             Int32Rect rect = new Int32Rect(0, line, pixelWidth, 1);
078:             this.bmp.WritePixels(rect, pixels, stride, 0);
079:         }
080:     }
081: 
082:     private void DrawingTask(Object o)
083:     {
084:         TaskData data = (TaskData)o;
085: 
086:         double xscale = (XMax - XMin) / data.PixelWidth;
087:         double yscale = (YMax - YMin) / data.PixelHeight;
088: 
089:         int bytesPerPixel = data.BytesPerPixel;
090:         int stride = data.PixelWidth * bytesPerPixel;
091: 
092:         // calculate pixel
093:         CancellationToken token = this.cancelSource.Token;
094:         DrawPixelLineHandler h = new DrawPixelLineHandler(this.DrawPixelLine);
095:         for (int i = 0; i < data.PixelHeight; i++)
096:         {
097:             if (token.IsCancellationRequested)
098:                 return;
099: 
100:             byte[] pixels = new byte[stride];
101:             for (int j = 0; j < stride; j += bytesPerPixel)
102:             {
103:                 // calculate pixel
104:                 int dx = j / bytesPerPixel;
105:                 double x = XMin + dx * xscale;
106:                 double y = YMin + i * yscale;
107: 
108:                 Complex z = new Complex(x, y);
109:                 int count = Mandelbrot.Compute(z);
110:                 Color c = Mandelbrot.MapColor(count);
111: 
112:                 pixels[j] = c.B;
113:                 pixels[j + 1] = c.G;
114:                 pixels[j + 2] = c.R;
115:                 pixels[j + 3] = c.A;
116:             }
117: 
118:             // draw line of pixels into bitmap - using Dispatcher
119:             Object[] args = new Object[] {
120:                 i, data.PixelWidth, stride, pixels, data.ActivePicture
121:             };
122:             Dispatcher.BeginInvoke(h, DispatcherPriority.SystemIdle, args);
123:         }
124: 
125:         this.task = null;
126:     }
127: }

Beispiel 12. Klasse MandelbrotImage für die vierte Variation.


Das vorzeitige Abbrechen laufender Aufgaben können wir in der Methode StopDrawing nachvollziehen (Zeile 33 bis 41). Es kommen die Standardwerkzeuge wie etwa eine Klasse CancellationTokenSource, ihre Methode Cancel (Zeile 38) und der Zugriff auf die Eigenschaft IsCancellationRequested (Zeile 97) zum Zuge. Das Ändern der Größenabmessungen finden wir in Methode ResizeImage vor und bedarf keiner weiteren Erläuterungen. Eine neue Task wird in den Zeilen 56 bis 64 aufgesetzt. Die aufgabenspezifischen Parameter werden in einer Hilfsstruktur TaskData zusammengestellt und dem Konstruktor der Klasse Task in Zeile 62 übergeben.

struct TaskData
{
   public int PixelWidth { get; set; }
   public int PixelHeight { get; set; }
   public int BytesPerPixel { get; set; }
   public long ActivePicture { get; set; }
}

Noch sind wir nicht am Ziel angekommen, wieder einmal sind wir mit den Widrigkeiten unterschiedlich schnell arbeitender Aufgaben konfrontiert. Es geht konkret um den Umbau von Tasks mit veralteten Größenangaben hin zu Tasks mit aktualisierten, korrekten Bildgrößen. Die Tasks der veralteten Größenangaben sind mit den bisherigen Betrachtungen korrekt vorzeitig abgebrochen worden, nur: Da diese nicht direkt in die WriteableBitmap-Objekte zeichnen dürfen, sondern den Umweg über das Dispatcher-Objekt gehen müssen, liegen in dessen Warteschlange noch zahlreiche Nachrichten vor, die auf veralteten Größenangaben fußen bzw. die sich auf WriteableBitmap-Objekte beziehen, die nicht mehr aktuell sind. Die hat nicht nur unschöne Nebeneffekte bei den grafischen Ausgaben zur Folge, die WritePixels-Methode quittiert ihren Dienst bei falschen Aktualparametern zusätzlich mit einer System.ArgumentException-Ausnahme!

Dieses Problem ist nicht ganz trivial lösbar, da es – leider – so einfache Methoden wie das Löschen der Warteschlange des Dispatcher-Objekts nicht gibt. Wir behelfen uns an dieser Stelle mit einem recht simplen Trick: Jedem zeitlichen Abschnitt im Ablauf des Programms, das Mandelbrotbilder einer bestimmten Größe berechnet, ordnen wir eine fortlaufende, ganzzahlige ID zu. Werden Aufträge in die Warteschlange des Dispatcher-Objekts eingereiht, wird eine Kopie dieser ID als Parameter mit übergeben. Nach einer gewissen zeitlichen Verzögerung wird – im Kontext des UI-Threads – der Auftrag ausgeführt. Stimmt die als Parameter übergebene ID mit der aktuellen ID im laufenden Programm überein, können wir die Aussage fällen, dass die Größenmaße der Bitmap-Objekte noch aktuell sind. Hätten sich die Größenmaße zwischenzeitlich verändert, hätten wir die ID inkrementiert und würden dann eine Abweichung beobachten. Durch den Vergleich zweier ganzzahliger Werte (ID in der laufenden Berechnungstask und kopierte ID in der Warteschlange des Dispatchers) lässt sich ein einfacher Plausibilitätscheck durchführen, ob Aufträge in der Warteschlange noch aktuell oder schon veraltet sind.

Das Schreiben und Lesen dieser ID-Werte ist threadsicher zu gestalten, wie wir in den Ausführungen zur Synchronisation von Threads ausführlich erläutert haben. Die ID-Variable trägt den Bezeichner activePicture und ist in Zeile 8 definiert. Das Inkrementieren dieser Variablen findet statt, wenn ein neuer Satz von Aufgaben mit seinen Berechnungen startet, es kommt die threadsicherere Methode Interlocked.Increment zum Einsatz (Zeile 54 in Listing 12). Für den lesenden Zugriff gibt es die Methode Interlocked.Read, sie wird im Dispatcher-Thread in Zeile 73 benutzt.

Das war es jetzt aber! Wenn Sie das Programm starten, können Sie, je nachdem, welche Rechenleistung ihr Computer besitzt, die Parallelität im Aufbau der einzelnen Teilrechtecke verfolgen. Sie können zu diesem Zweck die Anzahl der Rechtecke nach Belieben verändern. Je mehr Spalten und Zeilen Sie vergeben, umso mehr sind die einzelnen Aufgaben vermutlich gezwungen, der Reihe nach und damit sequentiell zu arbeiten, da für die vielen Aufgaben nicht mehr verfügbare Kerne vorhanden sind.

Parallele Berechnung eines Mandelbrotbilds.

Abbildung 6. Parallele Berechnung eines Mandelbrotbilds.

Variation 5: Parallelisierung mit BackgroundWorker-Objekt

In der letzten Variation erfolgten grafische Ausgaben durch ein Zusammenspiel von nebenläufigen Aufgaben und einem Dispatcher-Objekt. Bereits seit den Tagen der ersten .NET-Grafikbibliothek namens Windows Forms gibt es eine Klasse BackgroundWorker (Namensraum System.ComponentModel), die ebenfalls für grafische Ausgaben konzipiert ist und auch im Umfeld der WPF benutzt werden kann. Das Interessante an dieser Klasse ist ihre Arbeitsweise. Im Prinzip sind die nebenläufigen Tätigkeiten hier in einer Reihe von Ereignishandlermethoden anzusiedeln. Mit dem Aufruf der Startmethode RunWorkerAsync wird diese Tätigkeiten quasi nur angestoßen (Tabelle 3):

Element

Beschreibung

Methode RunWorkerAsync

public void RunWorkerAsync();
public void RunWorkerAsync(Object argument);

Mit dem Aufruf von RunWorkerAsync wird die Ausführung einer nebenläufigen Aufgabe eingeleitet. Optional kann mit der zweiten Überladung von RunWorkerAsync ein Objekt mit Initialisierungswerten übergeben werden (Parameter argument). Die nebenläufige Aktivität kann zu einem Zeitpunkt nur einmal aktiv sein.

Tabelle 3. Methode RunWorkerAsync der Klasse BackgroundWorker.


Ist eine nebenläufige Aufgabe mittels RunWorkerAsync ins Leben gerufen worden, kann man mit einer Reihe von Lebenszeichen ihren Fortschritt beobachten. Dazu werden am BackgroundWorker-Objekt mehrere Ereignisse ausgelöst, die wir in Tabelle 4 näher beschreiben:

Element

Beschreibung

Ereignis DoWork

public event DoWorkEventHandler DoWork;

Das DoWork-Ereignis wird ausgelöst, wenn die nebenläufige Aufgabe des BackgroundWorker-Objekts mit RunWorkerAsync gestartet wurde. Im Rumpf des Ereignishandlers ist der nebenläufige Vorgang zu realisieren. Dies kann entweder im Ganzen erfolgen oder auch nur in einzelnen Teilschritten. Entscheidet man sich für das Zerlegen des Vorgangs in Teilschritte, so sind diese programmiertechnisch in der Handlermethode des Ereignisses ProgressChanged zu platzieren.

Ereignis ProgressChanged

public event ProgressChangedEventHandler ProgressChanged;

Das ProgressChanged-Ereignis tritt immer dann ein, wenn am BackgroundWorker-Objekt die ReportProgress-Methode aufgerufen wurde (siehe Tabelle 5). Im Prinzip signalisiert das ProgressChanged-Ereignis entweder die erfolgreiche Bearbeitung eines Teilschritts oder man kann darunter auch den Anstoß verstehen, den nächsten Teilschritt auszuführen.

Ereignis RunWorkerCompleted

public event RunWorkerCompletedEventHandler RunWorkerCompleted;

Das RunWorkerCompleted tritt dann ein, wenn der Hintergrundvorgang abgeschlossen wurde. Im Regelfall wird dieses Ereignis beim Verlassen der DoWork-Ereignishandlermethode ausgelöst.

Tabelle 4. Wichtige Ereignisse der Klasse BackgroundWorker.


Die letzten zwei Ereignisse ProgressChanged und RunWorkerCompleted spielen für Anwendungen mit einem Oberflächenanteil eine besondere Rolle. Ihre Handlermethoden werden stets im Kontext des UI-Threads der Anwendung ausgeführt. Der Einsatz des Dispatcher-Objekts ist damit nicht notwendig. Das Ereignis ProgressChanged kann darüberhinaus beliebig häufig durch das BackgroundWorker-Objekt ausgelöst werden. Eine größere nebenläufige Aktivität kann somit in beliebig viele Teilschritte zerlegen. Man muss dazu die Methode ReportProgress typischerweise innerhalb des DoWork-Ereignishandlers aufrufen (Tabelle 5):

Methode

Beschreibung

Methode ReportProgress

public void ReportProgress (int percentProgress);
public void ReportProgress (int percentProgress, Object state);

Mit dieser Methode wird ein ProgressChanged-Ereignis ausgelöst. Mit dem Parameter percentProgress kann man einen Wert im Bereich von 0 bis 100 übermitteln, um den bereits erzielten Fortschritt der Aktivität zu beschreiben. Prinzipiell ließe sich natürlich ein beliebiger int-Wert übergeben. Die zweite Überladung von ReportProgress besitzt einen zusätzlichen Parameter state vom Typ Object, um weitere Informationen an die Handlermethoden des ProgressChanged-Ereignisses zu übermitteln.

Eigenschaft WorkerReportsProgress

public bool WorkerReportsProgress { get; set; };

Um mit ReportProgress Fortschrittsmeldungen an das BackgroundWorker-Objekt übermitteln zu dürfen, muss die WorkerReportsProgress-Eigenschaft auf true gesetzt sein. Andernfalls werden keine ProgressChanged-Ereignisse ausgelöst.

Tabelle 5. Elemente der Klasse BackgroundWorker zur Verwaltung von Teilschritten.


Eine mit RunWorkerAsync gestartete Aktivität kann auch vor Erreichen ihres regulären Endes abgebrochen werden. Dazu gibt es einige spezielle programmiersprachliche Elemente in der Klasse BackgroundWorker, die wir in Tabelle 6 zusammengestellt haben:

Element

Beschreibung

Methode CancelAsync

public void CancelAsync();

Dient zum vorzeitigen Abbrechen einer laufenden Hintergrundaktivität.

Eigenschaft CancellationPending

public bool CancellationPending { get; };

Die Eigenschaft CancellationPending besitzt dann den Wert true, wenn zuvor mit einem Aufruf von CancelAsync der Abbruch der Aktivität eingeleitet wurde. Folglich sollte eine Hintergrundaktivität diese Eigenschaft periodisch auslesen, um sich bei Bedarf vorzeitig zu beenden. Die Hintergrundaktivität muss also durch entsprechende Anweisungen selbst für ihre Beendigung sorgen.

Eigenschaft WorkerSupportsCancellation

public bool WorkerSupportsCancellation { get; set; };

Nicht jede Hintergrundaktivität muss prinzipiell vorzeitig abbrechbar sein. Setzt man WorkerSupportsCancellation auf den Wert true, lässt sich die Aktivität (mit CancelAsync) vorzeitig abbrechen, andernfalls nicht.

Eigenschaft IsBusy

public bool IsBusy { get; };

Liefert true zurück, wenn die Aktivität des BackgroundWorker-Objekts am Laufen ist.

Tabelle 6. Elemente der Klasse BackgroundWorker zum vorzeitigen Abbrechen einer Aktivität.


Achtung: In Abhängigkeit von der vorliegenden Implementierung kann es sein, dass die IsBusy-Eigenschaft, trotz vorangegangenen CancelAsync-Aufrufs, niemals den Wert false annimmt. Gemeint ist zum Beispiel eine Anweisungsfolge der Gestalt

while(backgroundWorker.IsBusy)
{
    Thread.Sleep(100);
    // doing some other things ...
}

Ist dieses Code-Fragment im UI-Thread der Anwendung platziert, gelangen alle anderen Ereignisse der Anwendung nicht zur Abarbeitung, da diese ausschließlich (sequentiell) in diesem Thread ausgeführt werden. Dadurch wird insbesondere das RunWorkerCompleted-Ereignis des BackgroundWorker-Objekt nicht bearbeitet, folglich ist die nebenläufige Aktivität ewig busy.

Genug der Vorreden und theoretischen Betrachtungen, in Listing 13 finden Sie die Klasse MandelbrotImage in ihrer Ausprägung der fünften Variation vor, die gleichzeitig Mandelbrotbilder zeichnen kann als auch auf Unterbrechungen reagieren kann, um den Bildaufbau bei einer veränderten Fenstergröße anzupassen. Die Reaktionsfähigkeit der Klasse MandelbrotImage wird mit Hilfe eines BackgroundWorker-Objekts erzielt:

001: class MandelbrotImage : Image
002: {
003:     // bitmap specific data
004:     private WriteableBitmap bmp;
005:     private int pixelHeight = 0;
006:     private int pixelWidth = 0;
007:     private int bytesPerPixel;
008: 
009:     // worker thread specific data
010:     private BackgroundWorker worker;
011:     private AutoResetEvent doneEvent;
012:     private bool launchWorker = false;
013:     private bool isFirstResize = true;
014: 
015:     // c'tor
016:     public MandelbrotImage(double width, double height)
017:     {
018:         this.Width = width;
019:         this.Height = height;
020: 
021:         // setup background worker
022:         this.worker = new BackgroundWorker();
023:         this.worker.WorkerReportsProgress = true;
024:         this.worker.WorkerSupportsCancellation = true;
025: 
026:         this.worker.DoWork +=
027:             new DoWorkEventHandler(this.Worker_DoWork);
028:         this.worker.RunWorkerCompleted +=
029:             new RunWorkerCompletedEventHandler(this.Worker_RunWorkerCompleted);
030:         this.worker.ProgressChanged +=
031:             new ProgressChangedEventHandler(this.Worker_ProgressChanged);
032: 
033:         this.doneEvent = new AutoResetEvent(false); 
034:     }
035: 
036:     // properties (excerpt from complex area)
037:     public double XMin { get; set; }
038:     public double XMax { get; set; }
039:     public double YMin { get; set; }
040:     public double YMax { get; set; }
041: 
042:     // event handling
043:     private void Worker_DoWork(Object sender, DoWorkEventArgs e)
044:     {
045:         double xScale = (XMax - XMin) / this.pixelWidth;
046:         double yScale = (YMax - YMin) / this.pixelHeight;
047:         int stride = this.pixelWidth * this.bytesPerPixel;
048: 
049:         int nextRow = 0;
050:         for (int i = 0; i < this.pixelHeight; i++)
051:         {
052:             byte[] pixels = new byte[stride];
053:             for (int j = 0; j < stride; j += this.bytesPerPixel)
054:             {
055:                 int dx = j / this.bytesPerPixel;
056:                 double x = XMin + dx * xScale;
057:                 double y = YMin + nextRow * yScale;
058: 
059:                 Complex z = new Complex(x, y);
060:                 int count = Mandelbrot.Compute(z);
061:                 Color c = Mandelbrot.MapColor(count);
062: 
063:                 pixels[j] = c.B;
064:                 pixels[j + 1] = c.G;
065:                 pixels[j + 2] = c.R;
066:                 pixels[j + 3] = c.A;
067:             }
068: 
069:             PixelsPerLine line = new PixelsPerLine();
070:             line.Y = i;
071:             line.Width = this.pixelWidth;
072:             line.Stride = stride;
073:             line.Bits = pixels;
074: 
075:             if (this.worker.CancellationPending)
076:             {
077:                 this.doneEvent.Set();
078:                 e.Cancel = true;
079:                 break;
080:             }
081:             else
082:             {
083:                 this.worker.ReportProgress(i, line);
084:             }
085: 
086:             // update row count
087:             nextRow++;
088:         }
089: 
090:         this.doneEvent.Set(); 
091:     }
092: 
093:     private void Worker_ProgressChanged(Object sender, ProgressChangedEventArgs e)
094:     {
095:         Int32Rect rect = new Int32Rect();
096:         try
097:         {
098:             PixelsPerLine line = (PixelsPerLine) e.UserState;
099:             rect = new Int32Rect(0, line.Y, line.Width, 1);
100:             this.bmp.WritePixels(rect, line.Bits, line.Stride, 0);
101:         }
102:         catch (ArgumentException)
103:         {
104:             Console.WriteLine("Rect: {0} -- Bitmap: Width={1}, Height={2}",
105:                 rect, this.bmp.Width, this.bmp.Height);
106:         }
107:     }
108: 
109:     private void Worker_RunWorkerCompleted(Object sender, RunWorkerCompletedEventArgs e)
110:     {
111:         if (this.launchWorker)
112:         {
113:             this.launchWorker = false;
114:             this.worker.RunWorkerAsync();
115:         }
116:     }
117: 
118:     // public methods
119:     public void UpdateSize(double width, double height)
120:     {
121:         this.StopDrawing();
122:         this.ResizeImage(width, height);
123: 
124:         // extract current bitmap data in correct thread context
125:         this.pixelWidth = this.bmp.PixelWidth;
126:         this.pixelHeight = this.bmp.PixelHeight;
127:         this.bytesPerPixel = this.bmp.Format.BitsPerPixel / 8;
128: 
129:         if (this.isFirstResize)
130:         {
131:             this.isFirstResize = false;
132:             this.worker.RunWorkerAsync();
133:         }
134:         else
135:         {
136:             if (this.worker.IsBusy)
137:                 this.launchWorker = true;
138:             else
139:                 this.worker.RunWorkerAsync();
140:         }
141:     }
142: 
143:     public void StopDrawing()
144:     {
145:         if (this.worker.IsBusy)
146:         {
147:             this.worker.CancelAsync();
148:             this.doneEvent.WaitOne();
149:         }
150:     }
151: 
152:     // private helper methods
153:     private void ResizeImage(double width, double height)
154:     {
155:         this.Width = width;
156:         this.Height = height;
157:         this.bmp = new WriteableBitmap(
158:             (int)width, (int)height, 96, 96, PixelFormats.Bgra32, null);
159:         this.Source = this.bmp;
160:     }
161: }

Beispiel 13. Klasse MandelbrotImagein der fünften Variation.


Einige Passagen in Listing 13 bedürfen einer vertieften Betrachtung. In Zeile 43 beginnt die Methode Worker_DoWork: Sie repräsentiert die nebenläufige Aktivität und wird in einem Threadkontext ausgeführt, der keinen Zugriff auf das User-Interface hat. Berechnete Pixelzeilen werden in Zeile 83 mit einem Trick an die Oberfläche gebracht: Da jede berechnete Zeile einen Fortschritt in der Berechnung des Bildes darstellt, schleusen wir alle relevanten Pixelinformationen zu einer Zeile mit einem Aufruf der ReportProgress-Methode zum BackgroundWorker-Objekt durch. Dieses löst in Folge dessen ein ProgressChanged-Ereignis aus, die angemeldete Methode Worker_ProgressChanged (Zeilen 93 bis 107) bekommt die Daten weitergereicht und kann diese – im richtigen Threadkontext – an der Oberfläche zeichnen. Für das Weiterreichen aller Pixelinformationen zu einer bestimmten Klasse bedient sich die Klasse MandelbrotImage einer Hilfsklasse PixelsPerLine:

class PixelsPerLine
{
    public int Y { get; set; }
    public int Width { get; set; }
    public int Stride { get; set; }
    public byte[] Bits { get; set; }

    public override String ToString()
    {
        return String.Format("{0}: -> {1}", this.Y, this.Width);
    }
}

Wichtig für Ihr Verständnis ist noch, wie wir auf Größenanderungen des umgebenden Fensters reagieren. Die aktuell in Ausführung befindliche Hintergrundaktivität ist natürlich als Erstes zu beenden. Das ist im Falle eines einzigen BackgroundWorker-Objekts gar nicht so einfach, vor allem, wenn wir dasselbe Objekt für die nächste Hintergrundaktivität wieder verwenden wollen. Besitzt dieses nämlich den Status Busy (also IsBusy-Eigenschaft gleich true), scheitert jeder Versuch mit dem Starten einer weiteren Hintergrundaktivität. Wie gehen wir deshalb vor?

Mit dem Aufruf von CancelAsync in Zeile 147 wird das vorzeitige Ende der Aktivität eingeläutet. Die Worker_DoWork-Methode kann darauf in Zeile 75 reagieren, die CancellationPending-Eigenschaft am BackgroundWorker-Objekt liefert den Wert true zurück und die Methode wird verlassen. Allerdings wissen wir nicht, inwieweit das Aufrufen der CancelAsync-Methode zum Einen und das Auslesen der CancellationPending-Eigenschaft zum anderen in der von uns gewünschten Reihenfolge stattfinden, da beide Aktionen in unterschiedlichen Threads stattfinden. Aus diesem Grund bediene ich mich eines zusätzlichen Hilfsmittels auf dem Werkzeugkasten der Parallelprogrammierung und setze ein AutoResetEvent-Objekt ein. Mit dem Aufruf von WaitOne in Zeile 148 warte ich im UI-Thread deshalb solange, bis die DoWork-Methode im Kontext des Sekundärthreads auch tatsächlich verlassen worden ist. In den Zeilen 136 bis 139 gehe ich nochmals defensiv vor: Ist das BackgroundWorker-Objekt immer noch Busy, setze ich ein boolesches Flag und verlagere das Starten der nächsten Hintergrundaktivität in die Ereignishandlermethode Worker_RunWorkerCompleted (Zeilen 109 bis 116). Andernfalls kann man die RunWorkerAsync-Methode direkt aufrufen.

Wir sind selbst mit diesen Anstrengungen noch immer nicht ganz am Ziel angekommen. Wie bereits in der letzten Variation gesehen, ist es trotzdem möglich, dass – auf Grund der nebenläufigen Ausführung von UI- und Hintergrundthread – Zeichenaufträge an das WriteableBitmap-Objekt gestellt werden, die unzulässige Koordinaten enthalten. An diesem Punkt bleibt uns nun trotz sorgfältigster Programmierung nichts anderes übrig, als mit Unterstützung eines try-/catch-Blocks obsolete WritePixels-Aufrufe zu ignorieren (Zeilen 95 bis 106).

Ein letzter Hinweis: Die Klasse MandelbrotWindow müssen wir in dieser Variation nicht betrachten. Die Realisierung aus der letzten Variation kann unverändert auch in diesem Teilschritt übernommen werden.

Variation 6: Parallelisierung ausschließlich mit Dispatcher-Objekt

Geht es nur darum, ein Mandelbrotbild in viele Rechtecke zu zerlegen und diese möglichst gleichzeitig ihren Bildinhalt zeichnen zu lassen, gelangen wir auch ohne Unterstützung von Task- und BackgroundWorker-Objekte ans Ziel. Die Klasse MandelbrotImage aus Variation 3 ist sehr wohl dazu geeignet, auch mehrfach in einem Container-Objekt instanziiert zu werden. Softwaretechnisch gibt es in dieser Variation nichts mehr zu tun. Als Fensterklasse wählen wir die Implementierung von MandelbrotWindow aus den Variationen 4 oder 5. Beide Realisierungen sind völlig identisch. Die MandelbrotImage-Klasse wiederum entnehmen wir Variation 3. In diesem Teilschritt hatten wir eine Spezialisierung der Image-Klasse entwickelt, deren Bildaufbau unterbrechbar war und die keine Hilfestellungen von zwei Klassen Task oder BackgroundWorker in Anspruch genommen hatte.

Vor dem Hintergrund der reinen Lehre ist diese Variation etwas angreifbar. Alle Berechungen werden letzten Endes um UI-Thread durchgeführt, was man typischerweise vermeiden sollte. Nichtsdestotrotz habe ich diese Variation an das Ende unserer Heptalogie gestellt. Wenn Sie das Beispiel wie vorgeschlagen ausführen, werden Sie von der Regelmäßigkeit des selbstorganisierenden Aufbaus einer Warteschlange fasziniert sein. Zu Beginn des Programms wird zunächst für alle Rechtecke sequentiell die erste Pixelzeile berechnet und eine entsprechende Nachricht in die Dispatcher-Warteschlange eingereiht. Diese Nachrichten werden nun streng der Reihe nach abgearbeitet – und damit wird für alle Rechtecke die zweite Pixelzeile berechnet. Entsprechende Nachrichten werden – dieses Mal im Kontext des UI-Threads – wieder in die Dispatcher-Warteschlange eingefügt, dieses Spiel setzt sich von nun an Pixelzeile für Pixelzeile fort. Bei genauer Betrachtung sollten wir dieses Mal von einer Quasi-Parallelisierung der Mandelbrotanwendung sprechen, da diese größtenteils in einem Thread abläuft (dem UI-Thread).

Es gibt in dieser Variation keinerlei Abhängigkeiten zu irgendwelchen Rechenzeiten von Tasks oder Hintergrundaktivitäten, die dieses regelmäßige Schema durcheinander bringen könnten. In Abbildung 7 finden Sie einen Schnappschuss der sechsten Variation vor. Das Hauptfenster wurde in 64 Rechtecke unterteilt, die durch eine Dispatcher-Warteschlange aufgebaut werden:

Quasiparallele Ausführung der Mandelbrotanwendung

Abbildung 7. Quasiparallele Ausführung der Mandelbrotanwendung


Wir sind am Ende der Heptalogie angekommen. Ich hoffe, dass Ziel der Betrachtung unterschiedlicher Lösungsansätze für rechenintensive Anwendungen mit grafischen Ausgaben ist bei den vielen Pixeln, die wir verschoben haben, nicht verloren gegangen. Die Entwicklung robuster und korrekter Multithreading-Anwendungen ist nicht immer ganz einfach. Die Beispiele aus diesem Kapitel sollten eine Motivation gegeben haben, dass es die Mühe trotzdem Wert ist.