www.rolandk.de
- Aktuelle Themen zu .Net -
Achtung: Hier handelt es sich um meine alte Seite.
Die aktuelle ist unter folgendem Link erreichbar: www.rolandk.de/wp/
Home Tutorials Threading Chapter 2 - ThreadPool und einfache Synchronisierung




















































Chapter 2 - ThreadPool und einfache Synchronisierung
Dienstag, den 16. November 2010 um 20:24 Uhr

 

 

Allgemeines

Im letzten Artikel dieses Tutorials bin ich kurz auf die Klasse BackgroundWorker eingegangen. Diese Klasse ist sehr nützlich, wenn man in einer grafischen Oberfläche bestimmte Aufgaben in den Hintergrund verlagern möchte. Doch was kann man machen, wenn man nicht in der Oberfläche entwickelt, sondern eine Ebene darunter (z. B. Business-Logik in einem MVP ähnlichen Muster)? Eine weitere Möglichkeit, ein längere Aufgabe in einen Hintergrundthread auszulagern, bildet der ThreadPool. In diesem Artikel beschreibe ich daher, was der ThreadPool in .Net macht und wie man ihn verwenden kann.

 

Grundlagen

Zunächst einmal grundsätzlich die Frage: Was ist der ThreadPool? Der ThreadPool ist nichts anderes als eine zentral verwaltete Liste von Threads. Jeder Thread kann dabei Aufgaben von dem Entwickler erhalten und diese dann asynchron ausführen. Wie viele Threads genau in diesem ThreadPool stecken, ist für den Entwickler erst einmal egal. Wichtig ist nur: Man kann dem ThreadPool eine oder mehrere Aufgaben zuweisen, die anschließend von den Threads asynchron ausgeführt werden. Im Prinzip also nichts anderes, als der BackgroundWorker im letzten Beispiel, allerdings gibt es einen Haken: Während der BackgroundWorker die Synchronisierung zwischen dem Haupt-Thread und dem Hintergrund-Thread automatisch vornimmt (Ereignis RunWorkerCompleted wird im Haupt-Thread ausgelöst), muss das mit dem ThreadPool selbst gemacht werden - natürlich nur, sofern man so etwas auch benötigt. Ein ganz einfaches Beispiel, ohne Synchronisierung, sieht etwa folgendermaßen aus:

  1. ThreadPool.QueueUserWorkItem(new WaitCallback(OnDoBackgroundWork), null);
  2.  
  3. ...
  4.  
  5. private void OnDoBackgroundWork(object argument)
  6. {
  7. //Perform background work here
  8. ...
  9. }

Selbstverständlich kann auch wie im folgenden Beispiel die modernere Schreibweise mit Lambdas verwendet werden. Welche Variante verständlicher ist, hängt dabei stark von den Gewohnheiten des Entwicklers ab, die Funktion ist aber die Gleiche:

  1. ThreadPool.QueueUserWorkItem(new WaitCallback((argument) =>
  2. {
  3. //Perform background work here
  4. ...
  5. }), null);

Was hier passiert ist also ähnlich wie beim BackgroundWorker kein Hexenwerk. Mit ein paar Zeilen Code können problemlos auch größere Blöcke Programmcode auf andere Threads ausgelagert werden. Allerdings haben wir bei den obigen Beispiel noch den Nachteil, dass wir im Hauptthread nicht wissen, wann die Arbeit im Hintergrund abgeschlossen ist.

 

Beispiel

Zur besseren Veranschaulichung ein kurzes Beispiel: Nehmen wir an, wir haben ein Programm das zu einem bestimmten Zeitpunkt mehrere voneinander unabhängige Dateien laden soll. Das Laden jeder Datei kann zwischen 1 bis 2 Sekunden dauern. Würde man hierzu nur einen Thread verwenden, würden alle Dateien nacheinander geladen werden und der gesamte Vorgang würde - rein rechnerisch - etwa bei 5 Dateien insgesammt 5 - 10 Sekunden benötigen. Die Alternative mit dem ThreadPool würde so aussehen, dass für jede zu ladende Datei ThreadPool.QueueUserWorkItem aufgerufen werden würde, um so alle Ladevorgänge jeweils in Threads auszulagern. Mit diesem Vorgehen kann die Ladezeit deutlich verringert werden. Die beiden nachfolgenden Codeausschnitte verdeutlichen dieses Beispiel (ersteres ohne und letzteres mit ThreadPool):

  1. //Loading 5 files
  2. LoadFile("file1.xml");
  3. LoadFile("file2.xml");
  4. LoadFile("file3.xml");
  5. LoadFile("file4.xml");
  6. LoadFile("file5.xml");
  1. //Loading 5 files
  2. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file1.xml");
  3. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file2.xml");
  4. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file3.xml");
  5. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file4.xml");
  6. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file5.xml");

Wenn wir in diesem Beispiel die Methode LoadFile wie nachfolgend programmieren, sehen wir in einem Consolenfenster (siehe Screenshot ganz oben auf dieser Seite) wann die Methode mit welchen Parametern aufgerufen wird:

  1. private static void LoadFile(object argument)
  2. {
  3. Console.WriteLine(
  4. "[" + DateTime.Now.ToLongTimeString() + "] " +
  5. "Starting loading of file " + argument.ToString());
  6.  
  7. //Simulate loading
  8. Thread.Sleep(1000);
  9.  
  10. Console.WriteLine(
  11. "[" + DateTime.Now.ToLongTimeString() + "] " +
  12. "Finished loading of file " + argument.ToString());
  13. }

Was fällt dabei auf? Im ersten Block werden die Dateien nacheinander geladen, macht ja soweit auch Sinn. Im Zweiten Block dagegen werden zunächst die Dateien file2.xml, file4.xml, file3.xml und file1.xml geladen. Erst nachdem file1.xml fertig ist, startet file5.xml. Unterm Strich ergibt das eine völlig willkürliche Reihenfolge. Aber warum zuerst 4 Dateien? Das liegt daran, weil das Beispiel auf meinem Rechner lief und ich einen 4-Kern Prozessor habe. Standardmäßig erzeugt .Net so viele Threads im Threadpool wie es Prozessorkerne im Rechner gibt. Nachdem also einer der Threads mit dem Laden einer Datei fertig war, konnte er mit der file5.xml beginnen. Wichtig ist auch die willkürliche Reihenfolge. Man kann bei der Verwendung von ThreadPool nicht davon ausgehen, dass die Aufgaben auch in der Reihenfolge abgearbeitet werden, in der ThreadPool.QueueUserWorkItem aufgerufen wurde.

 

Synchronisierung mit EventWaitHandle

Obiges Beispiel hat gezeigt, wie einfach Aufgaben mit dem ThreadPool parallel ausgeführt werden können. Problem ist aber noch: Woher weiß das Hauptprogramm oder besser gesagt der Haupthread, wann alle Aufgaben abgearbeitet sind? An diesem Punkt die Synchronisierung ins Spiel, die ich in den nachfolgenden Artikeln noch tiefer erklären werde. Grundsätzlich gibt es zahlreiche Möglichkeiten, Threads zu synchronisieren. An dieser Stelle will ich einen kurzen Einblick in dieses Thema mit dem EventWaitHandle geben. Betrachtet man den Namen, erkennt man bereits den Sinn. Event heißt, dass irgendein Ereignis eine Rolle spielt. Dazu lässt Wait vermuten, dass irgendjemand wartet. Um nicht lange drum herum zu reden: Das Ereignis, dass uns interessiert, tritt auf, wenn alle Aufgaben erledigt sind, die wir dem ThreadPool übergeben haben. Warten muss selbstverständlich der Hauptthread. Obiges Beispiel modifiziert mit einem WaitHandle könnte dann in etwa so aussehen:

  1. //Loading 5 files
  2. s_eventWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
  3. s_fileCounter = 0;
  4. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file1.xml");
  5. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file2.xml");
  6. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file3.xml");
  7. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file4.xml");
  8. ThreadPool.QueueUserWorkItem(new WaitCallback(LoadFile), "file5.xml");
  9. s_eventWaitHandle.WaitOne();
  10.  
  11. ...
  12.  
  13. private static void LoadFile(object argument)
  14. {
  15. Console.WriteLine(
  16. "[" + DateTime.Now.ToLongTimeString() + "] " +
  17. "Starting loading of file " + argument.ToString());
  18.  
  19. //Simulate loading
  20. Thread.Sleep(1000);
  21.  
  22. Console.WriteLine(
  23. "[" + DateTime.Now.ToLongTimeString() + "] " +
  24. "Finished loading of file " + argument.ToString());
  25.  
  26. s_fileCounter++;
  27. if (s_fileCounter >= 5) { s_eventWaitHandle.Set(); }
  28. }

Was passiert hier? Bevor die Threads gestartet werden, werden ein EventWaitHandle und eine Zählervariable erzeugt. In jedem Aufruf von LoadFile, wird der Zähler hochgezählt und sobald die letzte Datei geladen wurde, wird die Set-Methode des EventWaitHandles aufgerufen. Set setzt das EventWaitHandle auf signalisiert, was heißt, dass intern eine Art Ereignis ausgelöst wird. Auf dieses Ereignis wiederum Wartet der Hauptthread beim Aufruf von WaitOne. Der Hauptthread macht also erst weiter, nachdem alle fünf Dateien geladen wurden. Zugegen, in diesem Beispiel ist die Codierung aufgrund der statischen Variablen (WaitHandle und Zähler) nicht gerade sauber, aber der Zweck dieser Objekte sollte so gut ersichtlich sein.

 

Zusammenfassung

Dieser Artikel hat in einem kurzen Beispiel gezeigt, wie der ThreadPool grundlegend funktioniert und wie man ihn verwenden kann. Zudem wurde eine einfache Möglichkeit zur Synchronisierung vorgestellt. Abschließend sollte man sich im Hinterkopf behalten, dass die Ausführung von Aufgaben in einem ThreadPool nicht zwingend nacheinander, sondern rein theoretisch in einer willkürlichen Reihenfolge stattfinden kann. Diese Eigenschaft hat auch etwa die Klasse Parallel in der in .Net 4 eingeführten TPL. Selbst bei PLinq ist das so.

 

Quellen

  • Msdn
    Beschreibung der Klasse ThreadPool auf Msdn.
  • Msdn
    Beschreibung der Klasse EventWaitHandle auf Msdn.

 

Siehe auch...

 

Kommentar hinzufügen

Ihr Name:
Kommentar: