Multithreading mit .NET Jetzt besonders günstig in allen Sprachen MSDN TechTalk – <<Monat JJJJ>> <<Thema>> 1 Multithreading mit .NET Jetzt besonders günstig in allen Sprachen Bernd Marquardt Software & Consulting berndm@go-sky.de
Agenda Einführung Erzeugung von Threads Steuerung von Threads Parameterübergabe, Performance Steuerung von Threads Priority, Suspend, Resume, Abort Debugging Synchronisierung Der Thread-Pool Threads und Lokalisierung Zusammenfassung
Einführung Multi-Threading kommt jetzt aus der .NET-Runtime Es ist kein Sprach-Feature (wie z.B. in C++) Jede .NET-Sprache kann Multi-Threading Im .NET-Framework gibt es mehrere Klassen, die das Arbeiten mit Threads sehr vereinfachen Siehe: Thread, ThreadStart , ThreadPool, Monitor, Mutex, Interlocked,…
Was ist ein Thread? Ein Programm besteht mindestens aus einem Thread, dem Haupt-Thread Wird vom Betriebssystem gestartet Ein Thread ist ein „Ausführungsweg“ im Programm Ein Thread führt ganz bestimmte Befehle nacheinander aus In einem Programm können mehrere unabhängige Threads existieren
Kontrollfluss Synchrone Abarbeitung Asynchrone Abarbeitung
Was ist ein Thread? Jeder Thread hat seinen eigenen Stack Threads können jedoch auch globale Resourcen des Programms gemeinsam nutzen Jeder Thread hat eine bestimmte Priorität Das Betriebssystem vergibt über die Priorität entsprechende Zeitscheiben für die Ausführung an den Thread
Threads & Prozessoren In einem Rechner mit mehreren Prozessoren können entsprechend viele Threads tatsächlich gleichzeitig laufen Ein Rechner mit zwei Prozessoren ist aber nicht doppelt so schnell, wie sein Ein-Prozessor-Kollege ACHTUNG: Win95, 98 und ME können nur mit einem Prozessor arbeiten
Einsatz mehrerer Threads Wann kann man mehrere Threads sinnvoll einsetzen? Bei intensiven, mathematischen Berechnungen Grafik-Algorithmen Drucker-Ausgabe Aufräumarbeiten im Hintergrund Übertragen von Daten im Hintergrund ......
Kleine Warnung Die Programmierung von Applikationen mit mehreren Threads ist kompliziert Debugging, Fehlersuche, Testen Synchronisierung Ordentliches Beenden von Threads Fehlerbehandlung Parameterübergabe Saubere Planung und Prioritätenvergabe
Vorab-Analyse Bevor man mehrere Threads programmiert, sollte man prüfen: Ist eine saubere Abtrennung der Aufgaben des Threads möglich? Ist eine vernünftige Datenübergabe möglich? Kann die Applikation überhaupt andere Aufgaben ausführen, wenn der zusätzliche Thread läuft? Muss im neuen Thread auf Resourcen zugegriffen werden, die sowieso schon blockiert sind?
Thread-Typen I Man kann grob zwei Typen von Threads unterscheiden: Worker-Threads... ...führen mathematische Berechnungen im Hintergrund aus User-Interface-Threads... ...sorgen für die flüssige Bedienung eines Windows-Programms Beide Typen sind sich sehr ähnlich UI-Threads erfordern mehr Planung
Thread-Typen II Andere Unterscheidung: Hintergrund-Threads Werden automatisch beendet, wenn der Haupt-Thread beendet wird Anwendungen: Aufräumarbeiten, langwierige Berechnungen Vordergrund-Threads Der Haupt-Thread wartet, bis alle anderen Vordergrund-Threads beendet sind Anwendungen: Eigener Thread für ein Fenster Steuerung mit Property „Thread.IsBackground“
Unser Beispiel Aus der numerischen Mathematik Bestimmung der Fläche unter einer Kurve Numerische Integration Hier: Wurzel-Funktion Bei kleiner Schrittweite hat man lange Rechenzeiten Berechnungen können leicht in einem oder mehreren Threads durchgeführt werden
Unser Beispiel
Erzeugung von Threads Worker-Thread mit statischen Methoden ThreadStart-Klasse zeigt auf die Thread-Funktion Thread-Klasse erlaubt die Steuerung des Threads Thread.Join wartet auf den Thread Thread.Join geht auch mit einem Timeout Abfrage, ob Timeout, ist möglich Ist der Thread gestartet mit „IsAlive“ Bedeutet nicht, dass der Thread auch läuft
MSDN TechTalk – <<Monat JJJJ>> <<Thema>> 16 Problem mit 2 Threads MSDN TechTalk – <<Monat JJJJ>> <<Thema>> 16 Ein typisches Sychronisierungs-problem: Zwei Threads sollen quasi-gleichzeitig die Fläche unter der Kurve berechnen Achtung: Es gibt nur eine Variable „dSum“ Statischer Kontext des Threads Falsches Rechenergebnis!!! Das Verhalten mit statischen Variablen kann aber auch erwünscht sein, z.B. Zählen von Threads
Erzeugung von Threads Thread-Methoden in instanzierten Klassen Vorteil: Datenübergabe ist kein Problem Jeder Thread hat seine eigene Instanz der Thread-Klasse Nachteil: Diese Vorgehensweise kostet etwas mehr Resourcen Performance und Speicher
Parameterübergabe Alle Variablen werden als „private“ in der Thread-Klasse deklariert Es gibt eine spezielle Funktion in der Thread-Klasse, welche... ...die Daten übernimmt ...das Thread-Objekt erzeugt und zurückgibt Das Rechenergebnis wird über ein Property der Thread-Klasse abgefragt
Performance Einprozessor- Zweiprozessor- Rechner Rechner Mit 1 Thread 119,078 sek 119,979 sek Mit 2 Threads 119,091 sek 59,875 sek Mit 3 Threads 119,088 sek 59,922 sek Mit 10 Threads 119,090 sek 59,859 sek Mit 1000 Threads --- 60,218 sek
Performance Rechner: Dual Pentium III, 500 MHz, 500 MB RAM Pentium III, 600 MHz, 200 MB RAM Thread-Erzeugung und Kontextwechsel scheinen sehr schnell zu sein Frage: Wieviele Kontextwechsel hat es gegeben??? Bei ca. 3000 Threads: OutOfMemoryException Es werden nicht immer sofort alle Threads gestartet
Steuerung von Threads Prioritäten-Vergabe mit Thread.Priority Suspend und Resume WIN32: n x Suspend = n x Resume .NET: n x Suspend = 1 x Resume Abort Man muss feststellen können, ob der Thread seine Arbeit beendet hat Möglichkeit: Bool‘sche Variable WANN genau der Thread beeinflußt wird, kann man nicht vorhersagen
Steuerung von Threads Wann hält der Thread an, wenn ein Abort-Befehl kommt? Im Normalfall wird der Thread NICHT sofort stehenbleiben Single-Prozessor-Maschine: Beendigung der Zeitscheibe des laufen den Threads Die Runtime steuert einen sog. „Safe-Point“ an Das gilt auch bei „Suspend“ Am „Safe-Point“ hat der Garbage-Collector die volle Kontrolle über das Thread-Objekt Wenn „unmanaged“ Code ausgeführt wird, muss dieser erst beendet werden
Steuerung von Threads Ein Thread kann sich aus seiner Thread-Methode auch selbst „suspenden“ ACHTUNG: Ein anderer Thread muß ihn dann wieder „resumen“ Gefahr von Deadlocks ist hier gegeben
Steuerung von Threads Bei „Abort“ wird immer eine ThreadAbortException geworfen In der Thread-Prozedur wird die Exception gefangen und es kann „aufgeräumt“ werden Problem: Ein Thread der „suspended“ ist, reagiert nicht auf „Abort“ und er kann dann auch nicht mehr „resumed“ werden!!! Lösung: Test des ThreadStates
Steuerung von Threads Es gibt leider noch ein Problem: Wenn eine Thread-Methode mit Abort unterbrochen wird, wenn sie gerade einen finally-Block ausführt, dann wird der finally-Block nicht bis zu Ende bearbeitet Es wird sofort (wenn vorhanden) der catch-Block der ThreadAbortException bearbeitet
Steuerung von Threads „ResetAbort“ in einer ThreadAbortException Der Thread kann dann weiterlaufen Frage: Wo läuft der Thread weiter? Nach dem catch-Zweig der Exception NICHT nach der Stelle, an der das Abort den Code unterbrochen hat!
Debugging Debugging mit VS .NET möglich Liste aller Threads über „Debug > Windows > Threads“ Einfrieren eines Threads möglich Bestimmten Thread weiterlaufen lassen Zeilenweise Debuggen Debugging von MT-Applikationen ist evtl. sehr mühselig Tests sind aufwändig, da man alle parallelen Zustände (durch Threads) irgendwie testen „sollte“
Debugging Zuerst möglichst viel ohne Multi-Threading testen Z.B.: mathematische Algorithmen Dann erst die Softwareteile in die MT-Applikation einfügen Nun kann man den Synchronisierungs-Code testen Verfahren ist nicht immer möglich Unbedingt mit Mehrprozessor-Rechnern testen!
Synchronisierung Die Synchronisierung soll den Zugriff auf begrenzte Resourcen des Rechners steuern, wenn mehrere Threads benutzt werden Variablen (RAM) Codeteile LPT-, COM- und USB-Schnittstellen
Synchronisierung Eine Synchronisierung beinhaltet immer die Gefahr eines „Deadlocks“ Thread A wartet auf die Resource Thread B hat die Resource in Benutzung Thread B wartet auf Thread A
Synchronisierung Objekte für die Synchronisierung Monitor Mutex Events Interlocked WaitHandle ReaderWriterLock Im .NET-Framework gibt es Klassen für diese Synchronisations-Objekte
Gleichzeit. Code-Zugriff Verhinderung des gleichzeitigen Code-Durchlaufs mit Monitor-Objekt Synchronisierung eines bestimmten Code-Bereiches, so dass nur ein Thread den Code benutzt Andere Threads müssen warten Das Monitor-Objekt kann nur innerhalb eines Prozesses verwendet werden Zugriff auf eine Variable kann mit einem Monitor-Objekt gesteuert werden
Gleichzeit. Code-Zugriff Bei Monitor: Angabe des Objektes, das geschützt werden soll ACHTUNG bei: Normalen Value-Typen (int, double, structs) Strings Hier funktioniert der Monitor nicht Wegen Boxing/Unboxing Wegen neuer String-Referenzen (wenn mit dem String etwas „gemacht“ wurde)
Synchron. mit Mutex Das Mutex-Objekt funktioniert in .NET wie in WIN32 Es ist prozessübergreifend einsetzbar Durch Festlegen eines Namens Darum ist das Mutex-Objekt langsamer, als ein Monitor-Objekt Bis zu 64 Mutex-Objekte sind verfügbar (plattformabhängig)
Synchron. mit Events Dienen der Koordinierung der Aktionen mehrerer Threads untereinander Im Gegensatz dazu: Monitor, Mutex verhindern den gleichzeitigen Zugriff auf Resourcen Sicherstellung, dass die Threads ihre Aufgaben in der richtigen Reihenfolge durchführen Beispiel: Ein Thread schreibt in einen Puffer, ein anderer Thread liest die Daten ManualResetEvent AutoResetEvent
„Atomare“ Anweisungen Viele Befehle dürfen nicht unterbrochen werden „Interlocked“-Klasse garantiert die vollständige Ausführung bestimmter Grundbefehle, wenn mehrere Threads auf die Variablen zugreifen können: Increment Decrement Exchange CompareExchange
WaitHandles Ähnliches Konzept zu Monitor Nicht auf Managed Code beschränkt WaitHandle kapselt Plattform-Ressource Ermöglicht Synchronisation mit Unmanaged Code Drei Typen: Mutex AutoResetEvent ManualResetEvent
Andere Sync.-Methoden Serialisierter Zugriff auf ArrayLists mit „Synchronized“ Benutzung eines thread-sicheren Wrappers Gibt es für viele .NET-Objekte (Queue, SortedList, Stack,…) Synchronisierung ganzer Methoden mit dem Attribut [MethodImpl(MethodImplOptions.Sychronized)] Funktion ähnlich wie Monitor Das Attribut wirkt immer auf die gesamte Methode
ReaderWriterLock Monitore unterscheiden nicht zwischen lesenden und schreibenden Threads Analogie: Datenbank-Sperren Objekt der Klasse ReaderWriterLock muss angelegt werden Z.B. als private member der fraglichen Threadklasse AcquireReaderLock / ReleaseReaderLock AcquireWriterLock / ReleaseWriterLock Geschachteltes Locking UpgradeToWriterLock
Timer System.Threading.Timer Periodische Ausführung von Funktionalität Status wird in übergebener Objekt-Instanz gehalten Deinitialisierung mit Dispose()
MT und Windows Forms Windows Forms laufen in einem STA-Kontext Nur der Thread, der das Fenster erzeugt hat, darf auf Methoden des Fensters und der Controls zugreifen „lock“ darf NICHT auf Fenster und Controls angewendet werden Gefahr von Deadlocks Benutzung von „Invoke“ und „BeginInvoke“ zur Umgehung der Probleme Ist langsamer, als ein direkter Aufruf Wichtig: Der Aufruf läuft NICHT in einem neuen, separaten Thread!
MT und Windows Forms Anwendung: Jedes Fenster (in einer MDI-Anwendung) läuft in einem separaten Thread Probleme: In den Threads können Exceptions auftreten Der Haupt-Thread kann dann ungültiger Referenzen auf nicht mehr existierende Fenster-Threads enthalten Aufwändige Programmierung
Threads & Lokalisierung Startet man einen neuen Thread, dann benutzt er die default-Resourcen ResourceManager kann normal benutzt werden Kultur (oder Objekt „CultureInfo“) muss in den Thread als Parameter übergeben und benutzt werden
Der .NET-Thread-Pool Ziel: Möglichst einfaches Arbeiten mit mehreren Threads Optimal für Threads, die lange Zeit auf etwas warten Optimal für Threads, die periodisch erweckt werden Der Thread-Pool stellt “fertige” Threads bereit, die genutzt werden können Im Thread-Pool werden mehrere Threads für den Benutzer bereit gehalten Aufruf aus dem Thread-Pool ist schneller, als die normale Thread-Erzeugung
Der .NET-Thread-Pool Achtung: Man kann nur eine bestimmte Anzahl von Pool-Threads nutzen Abfrage, wieviele Threads sind maximal möglich: ThreadPool.GetMaxThreads Abfrage, wieviele Threads sind jetzt noch möglich: ThreadPool.GetAvailableThreads Wird versucht, mehr Threads als erlaubt, zu erzeugen, so landen die überzähligen Threads in einer Warteschlange Property „IsThreadPoolThread“
Der .NET-Thread-Pool Systemkonzept: Pro Prozess ein ThreadPool Anforderungen an den Pool werden über eine Warteschlange gestellt Muss eine Anforderung länger als eine halbe Sekunde warten, wird neuer Pool-Thread erzeugt Maximal jedoch 25 Wird Pool-Thread 30 Sekunden nicht benötigt, wird Thread aus dem Pool entfernt
Der .NET-Thread-Pool Die .NET-Runtime benutzt den Thread-Pool ebenfalls: Asynchrone Aufrufe Socket-Verbindungen I/O-Completion Ports Timer Benutzung eines Pool-Threads mit ThreadPool.QueueUserWorkItem Von unmanaged Code: CorQueueUserWorkItem
Der .NET-Thread-Pool Jeder Pool-Thread bekommt einen Standard-Stack, die Standard-Priorität und läuft im MTA Wann sollte man den Thread-Pool nicht benutzen? Wenn der Thread eine bestimmte Priorität haben soll Wenn der Thread lange Zeit laufen soll und evtl. andere Threads blockieren kann Wenn man den Thread immer genau identifizieren will (zur Steuerung,…)
BeginInvoke() Bereitet den Methodenaufruf vor: Signatur: Reicht Werte- und Referenzparameter weiter Setzt übergebenen Callback auf (optional) Reicht Statusobjekt durch Gibt IAsyncResult zurück Signatur: IAsyncResult BeginInvoke( <paramListe der Methode>, AsyncCallback aDelegate, object state)
EndInvoke() Schließt die Operation ab: Liest alle Ausgabe- und Referenzparameter aus Wird auf IAsyncResult aus BeginInvoke() angewendet Liefert den Rückgabewert der aufgerufenen Methode Kann in rufender Methode oder dem Callback aufgerufen werden bool EndInvoke(<paramListe der Methode>, AsyncCallback aDelegate)
Synchron. mit Callback Warten, bis ein asynchroner Aufruf beendet ist Warten auf IAsyncResult.AsyncWaitHandle WaitHandle wird implizit gesetzt, wenn EndInvoke() aufgerufen wird VORSICHT! Wird Callback angegeben und wartet der Hauptthread auf das WaitHandle, dann wird bei Beendigung der asynchronen Methode WaitHandle signalisiert Und Callback aufgerufen Keine Garantie über Reihenfolge!
Weitere Beispiele WinCancel: Durchführung einer zeitaufwändigen Berechnung in WinForms TimerTest: Benutzung von Timern MatrixMT: Matrix-Multiplikation mit MT und ThreadPool SolveGS: Lineares Gleichungssystem mit mehreren Threads lösen MTMDI4: Multithreading mit mehreren Fenstern WinDemo: Spielprogramm mit Threads IO_Calc_MT: IO und Berechnungen in mehreren Threads parallel ausführen
Zusammenfassung Multiple Threading ist mit .NET etwas einfacher geworden Multiple Threading ist nicht mehr abhängig von einer bestimmten Programmiersprache Bevor man loslegt: Analyse, ob MT wirklich benötigt wird Debugging und Testen ist schwieriger Code wird etwas aufwändiger (z.B. durch Synchronisierung) Auch sehr wichtig für „Sanduhr-freie“ Applikationen mit User-Interface
MSDN TechTalk – <<Monat JJJJ>> <<Thema>> 54 Fragen!? MSDN TechTalk – <<Monat JJJJ>> <<Thema>> 54 Uff...