Home > System.Threading > System.Threading Teil 2 – Anwendung

System.Threading Teil 2 – Anwendung

Nachdem der erste Teil die Grundlagen vorstellte, soll es im zweiten Teil um die Anwendung von Threads gehen.

Windows Forms und Threads

Bei der Entwicklung einer grafische Oberfläche ist es oftmals sinnvoll, verschiedene Arbeiten in mehrere Threads auszulagern. Stellen sie sich eine Anwendung mit einer grafischen Bedieneroberfläche vor, die eine größere Menge an Daten bearbeiten soll. Ohne Multithreading wäre das nur schwer möglich. Die Oberfläche wäre für die Dauer der Datenbearbeitung blockiert und der Anwender müsste sich mit der Sanduhr zufrieden geben. Erstrebenswert ist es, dem Anwender eine Applikation auszuliefern, die ein verzögerungsfreies Arbeiten ermöglicht. Die Entgegennahme der Benutzereingaben und das Bearbeiten der Daten könnten in verschiedene Threads aufgeteilt werden. Somit behält der Anwender während der Bearbeitung die Kontrolle über das Programm. Hierzu ein Beispiel, welche folgende Aktionen durchführen soll:

1. Daten aus einer Datei laden.
2. Jeden geladenen Datensatz so konvertieren, dass dieser dargestellt werden kann.
3. Die Daten auf der Bedieneroberfläche darstellen.

Da das Laden und Konvertieren der Daten eine lange Zeit benötigen kann, sollte dieses in einem eigenen Thread ausgelagert werden. Dieser Thread soll mit der Bedieneroberfläche kommunizieren, um den Benutzer über den Stand der Arbeit zu informieren (Beispielsweise mit einem ProgressBar). Hier kommen wir auch schon zum eigentlichen Problem:

Threads dürfen nicht auf die Daten jenes Threads zugreifen, welcher die Bedieneroberfläche erstellt hat und diese bedient. Der Grund liegt darin, dass es durch das Event-Modell von Windows zu Speicherinkonsistenzen kommen kann (und wird). Um nun dennoch von Threads auf die Bedieneroberfläche zugreifen zu können, gibt es das Invoke-Modell. Sehen wir uns folgenden Codeabschnitt an:

using System;
using System.Threading;
using System.Windows.Forms;

namespace Threading
{
    public partial class WinForm : Form
    {
        private bool threadRunning = true;
        private delegate void SetLabelTextDelegate(string text);
        public WinForm()
        {
            InitializeComponent();
            Thread t = new Thread(new ThreadStart(delegate()
            {
                int count = 0;
                while (threadRunning)
                {
                    SetLabelText(string.Format("Wert: {0}", count++));
                    Thread.Sleep(250);
                }
            } ));
            t.Start();
        }
        private void SetLabelText(string text)
        {
            if (InvokeRequired)
                Invoke(new SetLabelTextDelegate(SetLabelText), new object [] { text });
            else
                label.Text = text;
        }
        private void WinForm_OnFormClosed(object sender, FormClosedEventArgs e)
        {
            threadRunning = false;
        }
    }
}

Das Beispiel erstellt ein Fenster mit einem Label. Ein separater Thread zählt eine Variable alle 250ms hoch. Der Wert der Variablen soll in dem Label auf der Form angezeigt werden. Interessant für uns ist dabei folgendes:

Der Thread greift nicht direkt auf das Label zu, sondern verwendet eine eigene Methode für das Setzen des Wertes. Diese Methode überprüft, ob sich der Aufrufer im gleichen Thread wie die Bedieneroberfläche befindet. Ist dieses nicht der Fall, so ruft sie sich selbst mithilfe eines Delegaten und der Methode Invoke() auf. Danach befinden wir uns im gleichen Thread wie die Bedieneroberfläche und können dann auf die Controls zugreifen. Das komplizierte an der Sache ist, dass für jede dieser Methoden ein eigener Delegate definiert werden muß. Da die Invoke-Methode nur einen Parameter vom Typ eines Objekt-Arrays kennt, müssen wir unsere Parameter in ein solches Array umwandeln.

Die BackgroundWorker-Komponente

Wer sich nicht mit den Details von Threads herumschlagen will und dennoch den Vorteil von Threads nutzen möchte, für den stellt das .NET Framework seit der Version 2.0 die BackgroundWorker-Komponente bereit. Mit dieser ist es möglich, eine Methode asynchron in einem Thread auszuführen. Die Komponente stellt drei Ereignisse zur Verfügung:

Dass die Ausführung begonnen wird (DoWork).
Dass die Ausführung abgeschlossen ist (RunWorkerCompleted).
Dass sich der Fortschritt der Arbeit geändert hat (ProgressChanged).

Dabei ist die Benutzung der Komponente relativ einfach:

1. Erstellen des BackgroundWorker-Objekts.
2. Setzen der notwendigen Delegaten für den Callback.
3. Starten der Arbeit.

Sehen wir uns aber am besten ein Beispiel an, welches PI mithilfe eines Background-Workers berechnet:

using System;
using System.ComponentModel;
using System.Threading;

namespace Threading
{
    class BackgroundWorkerSample
    {
        private double workerResult;
        static void Main(string[] args)
        {
            new BackgroundWorkerSample();
        }
        public BackgroundWorkerSample()
        {
            BackgroundWorker bgWorker = new BackgroundWorker();
            bgWorker.DoWork += new DoWorkEventHandler(this.DoWork);
            bgWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(this.WorkCompleted);
            bgWorker.ProgressChanged += new ProgressChangedEventHandler(this.ProgressChanged);
            bgWorker.RunWorkerAsync("Beginne Berechnung von PI!");
            bgWorker.WorkerReportsProgress = true;
            while (bgWorker.IsBusy)
                Thread.Sleep(50);
            Console.WriteLine("Ergebnis = " + workerResult);
            Console.ReadLine();
        }
        private void DoWork(object sender, DoWorkEventArgs e)
        {
            Console.WriteLine(e.Argument);
            e.Result = CalculatePi((BackgroundWorker)sender);
        }
        private void WorkCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            Console.WriteLine("Beendet!");
            workerResult = (double)e.Result;
        }
        private void ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            Console.WriteLine(string.Format("Fertig {0}% ", e.ProgressPercentage));
        }
        private double CalculatePi(BackgroundWorker worker)
        {
            double radius = 100;
            double kreistreffer = 0;
            long perc = (long)(((radius * 2) * Math.Pow(radius, 2)) + Math.Pow(radius, 2) / 100) / 100;
            long iter = 0;
            int percCompleted = 0;
            for (double y = radius * (-1); y <= radius; y++)
            {
                double end = Math.Pow(radius, 2);
                for (double x = radius * (-1); x <= end; x ++)
                {
                    iter ++;
                    if ((Math.Pow(x, 2) + Math.Pow(y, 2)) <= Math.Pow(radius, 2))
                        kreistreffer++;
                    if (iter == perc)
                    {
                        worker.ReportProgress(percCompleted++);
                        iter = 0;
                    }
                }
            }
            return kreistreffer / Math.Pow(radius, 2);
        }
    }
}

Im Konstruktor werden die entsprechenden Delegaten zugewiesen und anschliessend wird die Ausführung begonnen. Zur Ausführung kann ein Parameter übergeben werden, den wir in unserem Beispiel aber nur dafür nutzen, um eine Statusmeldung auszugeben. Anschliessend berechnen wir PI und rufen bei jedem Fortschritt im %-Bereich die Methode ReportProgress() mit dem entsprechenden Prozentsatz auf. Diese löst das Ereignis ProgressChanged aus, welches eine Meldung über den Fortschritt ausgibt. Ist PI berechnet, so wird das Ereignis RunWorkerCompleted ausgelöst, in der das Ergebnis in eine Instanzvariable geschrieben wird. Hilfreich ist der BackgroundWorker insbesonders bei Programmen mit Bedieneroberfläche. Die Ereignisse RunWorkerCompleted und ProgressChanged werden im gleichen Thread ausgeführt wie der Hauptthread. Somit kann direkt aus dem Methoden auf Controls der Oberfläche zugegriffen werden.

Der ThreadPool

Auch wenn Threads relativ ressourcenschonend in der Erzeugung sind, kann es von Vorteil sein, wenn Threads für andere Aufgaben wiederbenutzt werden können. Genau diese Funktionalität stellt der ThreadPool zur Verfügung. Pro Prozess existiert ein ThreadPool, der per Default 25 Threads beinhaltet, welche Aufgaben übernehmen können.

Wird mittels QueueUserWorkItem() eine Methode zur Ausführung angegeben, so wird diese in eine interne Liste eingetragen. Ist zur Zeit kein Thread aus dem Pool frei, so wird mit der Ausführung so lange gewartet, bis ein Thread frei ist. Mittels der statischen Methode SetMaxThreads() der Klasse ThreadPool kann festgelegt werden, wieviele Threads der Pool zur Verfügung stellen soll.

Asynchrone Methodenaufrufe

Normalerweise werden Methoden synchron aufgerufen, d.h. der Aufrufer wird so lange blockiert bis die Methode abgearbeitet wurde. Anschliessend nimmt dieser einen eventuell zurückgelieferten Rückgabewert entgegen. Es kann aber auch durchaus sinnvoll sein, Methoden asynchron aufzurufen. Man könnte dies beispielsweise implementieren, indem man für die aufzurufende Methode einen Thread erzeugt und diesen dann startet. .NET bietet allerdings für diesen Zweck schon ein vorgefertigtes Mittel an, nämlich die Methoden BeginInvoke() und EndInvoke() der Klasse Delegate. Mit dieser ist es möglich, eine Methode auf einfache Weise asynchron aufzurufen. Über verschiedene Methoden kann über das Beenden des asynchronen Methodenaufrufs informiert werden. Sehen wir uns dazu gleich einmal ein Beispiel an:

using System;
using System.Threading;

namespace Threading
{
    class AsynchroneMethodenaufrufe
    {
        private delegate string AsyncMyMethodCaller(int number, string message);
        private bool myMethodFinished = false;
        static void Main(string[] args)
        {
            new AsynchroneMethodenaufrufe();
        }
        public AsynchroneMethodenaufrufe()
        {
            AsyncMyMethodCaller caller = new AsyncMyMethodCaller(this.MyMethod);

            // Methode 1 - Warten auf Beendigung durch Aufruf von EndInvoke()
            IAsyncResult result = caller.BeginInvoke(5, "Methode 1", null, null);
            Console.WriteLine(caller.EndInvoke(result));

            // Methode 2 - Für das Beenden auf das Handle warten
            result = caller.BeginInvoke(3, "Methode 2", null, null);
            result.AsyncWaitHandle.WaitOne();
            Console.WriteLine(caller.EndInvoke(result));

            // Methode 3 - zyklisches Abfragen
            result = caller.BeginInvoke(4, "Methode 3", null, null);
            while (!result.IsCompleted)
                Thread.Sleep(50);
            Console.WriteLine(caller.EndInvoke(result));

            // Methode 4 - callback benutzen
            result = caller.BeginInvoke(2, "Methode 4", new AsyncCallback(this.MyMethodFinishedHandler), caller);
            while (!myMethodFinished)
                Thread.Sleep(50);
            Console.WriteLine(caller.EndInvoke(result));
            Console.ReadLine();
        }
        private void MyMethodFinishedHandler(IAsyncResult result)
        {
            myMethodFinished = true;
            Console.WriteLine ("asynchronen Aufruf beendet");
        }
        private string MyMethod(int number, string message)
        {
            Console.WriteLine("AsyncDemo.MyMethod() aufgerufen");
            for (int i = 0; i < number; i ++)
            {
                Console.WriteLine(string.Format(" {0}:{1} ", i, message));
                Thread.Sleep(250);
            }
            return "Rückgabewert!";
        }
    }
}

Das Erzeugen des asynchronen Methodenaufrufs ist dabei immer gleich und gliedert sich in folgende Schritte:

1. Definieren eines Delegaten, welcher die gleiche Signatur wie die asynchron auszuführende Methode hat.
2. Deklarieren des Delegaten und Zuweisen der Methode an diesen.

Anschliessend wird die Methode BeginInvoke() des Delegaten aufgerufen, um die asynchrone Ausführung zu beginnen. Als Parameter werden alle Eingangsparameter unserer asynchronen Methode, sowie zwei zusätzliche, übergeben. Diese können neben NULL noch folgende Werte haben:

Eine Callback-Methode vom Typen AsyncCallback.
Den Delegaten, der für die asynchrone Ausführung zuständig ist. Dieser wird anschliessend an das Callback übergeben, sobald die Ausführung abgeschlossen ist und dieser aufgerufen wird.

Wie man an dem Beispiel erkennen kann, gibt es unterschiedliche Methoden, um sich über die abgeschlossene Ausführung der Methode informieren zu lassen:

Direktes Warten mittes der Methode EndInvoke() des Delegaten.
Warten bis das WaitHandle signalisiert wurde.
Die Eigenschaft IsCompleted des Delegaten zyklisch abfragen.
Informierung über einen Callback.

Wird kein Rückgabewert benötigt, so ist die Methode mittels des Callbacks wahrscheinlich am sinnvollsten. Es können somit Aktionen ausführen werden, ohne das sich der ursprüngliche Aufrufer darum kümmern muss.

Es ist zu beachten, dass die Methode EndInvoke() des Delegaten als Parameter das IAsyncResult benötigt, welches von BeginInvoke() zurückgegeben wurde.

Der abschließende dritte Teil beschäftigt sich mit den möglichen Problemen, die bei der Programmierung von Threads auftreten können.

Beispiel (Visual Studio 2010) auf GitHub

Advertisements
  1. UweThomas
    March 22, 2011 at 8:45 pm

    Hallo Stefan,

    toller Artikel! Was zu der asynchronen Programmierung noch gut passen würde, sind die neuen Möglichkeiten mit async und await in C# 5.

    Viele Grüße aus Oerlinghausen
    Uwe

    • Markus Schaber
      March 28, 2011 at 8:26 am

      Da C# 5 noch nicht mal angekündigt ist, und bisher nur eine CTP dazu existiert, wäre das vielleicht etwas früh geschossen. 🙂

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: