Letztes Mal haben wir die Gefahren vom Multithreading angeschaut und zum Schluss gekommen, dass Multithreading die Komplexität der Software erhöht und nur überlegt und gezielt eingesetzt werden soll. Dieser Teil beleuchtet die bewährte Thread Klasse und erklärt, in welchen Fällen es legitim ist, das älteste Multithreading Mittel von .NET Framework einzusetzen.
Wie im ersten Teil erklärt, verwendet man Thread Technologien zur Lösung von CPU-bound Aufgaben, also Aufgaben die Prozessor-Resourcen beanspruchen und keine asynchronen Arbeiten erledigen (für asynchrone Arbeiten wird async\await eingesetzt).
Gründe für die Thread-Klasse
Die Thread Klasse ist der direktester Weg um einen Thread zu erstellen. Weil der Thread nicht aus dem Threadpool kommt, sondern direkt vom System, ist man frei den Thread zu manipulieren.
Die bewährte Thread Klasse wird verwendet, wenn:
- Die Aufgabe langläufig ist.
- Der Thread STA (Single Threaded Apartment) sein muss: für COM-Objekte und UI Steuerelemente.
- Der Thread ein foreground Thread sein soll.
- Der Thread forciert abgebrochen werden soll.
- Der Thread einen Namen haben muss (z.B. zum Debuggen).
Langläufige Aufgaben
Oft muss man während des ganzen Ablaufs des Programms zyklisches Arbeiten ausführen. Ein Threadpool Thread ist gedacht für kurze einmalige Aufgaben und ist deshalb nicht geeignet.
Es wird aber davon abgeraten sich auf diese Implementierung zu verlassen, weil das möglicherweise in die Zukunft ändern kann.
Hier ein Screenshot von https://coderkarl.wordpress.com/2012/12/13/long-running-tasks-and-threads/
Der Thread soll STA (Single Threaded Apartment) sein
COM- und UI-Objekte sind single Threaded und verlangen das STA-Threading Model. Wenn man z.B. im Voraus die DLLs einer grossen Bibliothek (z.B. DevExpress) ‚vor‘-laden möchte, dann kann man ein dummy Steuerelement aus der Bibliothek auf einem temporären Thread kreieren. Wird das Steuerelement später auf dem UI-Thread instanziiert, dann ist die Ladezeit kürzer weil die DLLs schon im Memory sind. Dieses ‚vor‘-laden geht nur auf einem STA-Thread. Threads aus dem Threadpool sind immer MTA (Multi Threaded Apartment).
Der Thread soll ein foreground Thread sein
Foreground-Threads verhindern das herunterfahren vom Programm bis sie beendet wurden. Threadpool Threads sind immer background-Threads und das kann man nicht ändern.
Thread forciert abbrechen
Wenn eine Aufgabe unzuverlässig ist (z.B. ruft Code aus Fremdbibliotheken auf) und das Potential hat um ewig ‚hängen‘ zu bleiben, dann kann man mit der Thread-Klasse den Thread knallhart abbrechen (mit Thread.Abort). Bei Threadpool-Threads ist das „Bad Practice“ weil man so dem Thread Scheduler einen Thread wegnimmt.
Thread Name
Zur Identifikation (z.B. im Debugger) kann man den Thread der Thread-Klasse einen Namen geben. Bei einem Threadpool-Thread ist dies „Bad Practice“ weil der Thread möglicherweise wiederverwendet wird für andere Aufgaben.
Und Thread Priorität?
Man kann einem Thread höhere Ausführ-Priorität geben. Beim Threadpool darf man das ebenfalls machen weil der Threadpool Manager diese Priorität zurück auf Normal setzt sobald der Thread zurück landet im Threadpool.
Die Thread-Klasse
Im unterstehenden Beispiel wird demonstriert, wie einen Thread mit Parameter kreiert und gestartet wird.
public class CyclicWorker { private readonly object _cancelLock = new object(); private bool _cancel; private Thread _cyclickWorkerThread; public void Start(int invervalTimeMs) { if (_cyclickWorkerThread != null) { return; } _cyclickWorkerThread = new Thread(DoWork); _cyclickWorkerThread.Name = "CyclickWorkerThread"; _cyclickWorkerThread.Start(invervalTimeMs); } public void Stop() { if (_cyclickWorkerThread == null) { return; } Debug.WriteLine("Stop(): setting cancel flag..."); lock (_cancelLock) { _cancel = true; } _cyclickWorkerThread.Interrupt(); _cyclickWorkerThread.Join(1000); if (_cyclickWorkerThread.IsAlive) { Debug.WriteLine("Stop(): thread still alive, aborting it..."); _cyclickWorkerThread.Abort(); _cyclickWorkerThread.Join(1000); } else { Debug.WriteLine("Stop(): thread was cancelled..."); } _cyclickWorkerThread = null; } private void CyclicWork() { // Do the cyclic work here... Debug.WriteLine(DateTime.Now); } private void DoWork(object invervalTimeMs) { int interval = (int) invervalTimeMs; while (true) { try { lock (_cancelLock) { if (_cancel) { break; } } CyclicWork(); Thread.Sleep(interval); //throw new Exception(); } catch (ThreadInterruptedException) { Debug.WriteLine("DoWork(): ThreadInterruptedException"); break; } catch (Exception ex) { Debug.WriteLine("DoWork(): Exception occurred. Details: " + ex); // Try to recover or let thread end by breaking out of the endless loop break; } } // Cleanup resources here } }
Thread stoppen
Beim Stoppen wird erst mit einem geteilten Semaphore (gelockter Boolean) den Thread mitgeteilt, dass er sich beenden soll. Für diese Aufgabe kann auch ManuelResetEvent oder CancellationToken verwendet werden. Dieser Semaphor wird im Thread an gezielten Orten abgefragt. So hat man die volle Kontrolle wo der Thread abgebrochen wird.
Der Thread kann aber blockiert sein und ist dann nicht in der Lage das Cancel-Flag zu verarbeiten. Je nach Schweregrad gibt es folgende Blockierungen:
- Der Thread ist am warten in einem BCL (Base Class Library) blockierenden Aufruf (z.B. Thread.Sleep, WaitHandle.WaitOne).
- Der Thread wartet auf eine Antwort eines asynchronen Aufrufes (z.B. Datenbank oder Netzwerk Abfrage). Das sollte heute übrigens mit async\await gelöst werden wodurch kein Thread während des Wartens verschwendet wird.
- Der Thread hängt ungewollt z.B. in einem Aufruf einer Fremdbibliothek.
Mit Thread.Interrupt wird eine ThreadInterruptedException in den BCL blockierenden Aufrufe (z.B. Thread.Sleep) injiziert. Die Exception muss man im Thread abfangen und verschweigen. Mit Thread.Interrupt wird der Thread also abgebrochen an ungefährliche Stellen.
Wenn der Thread immer noch nicht beendet wurde, dann wird Thread.Abort aufgerufen. Thread.abort ist ein Pferdemittel das nur zur Not angewendet werden darf. Der Thread wird unkontrolliert abgebrochen und hat keine Möglichkeit eventuelle Ressourcen sauber abzuschliessen.
Nach Thread.Interrupt und Thread.Abort wird mit Thread.Join gewartet bis der Thread beendet wird. Es wird die Überladung verwendet mit Timeout um das Warten zeitlich zu limitieren falls das Abbrechen nicht geklappt hat.
Follow up
Im nächsten Teil schauen wir den Threadpool an. Es wird gezeigt wie man selber eine Aufgabe ausführen kann auf einem Thread aus dem Threadpool. Ausserdem wird demonstriert, dass es in bestimmten Fällen bis zu einer halben Sekunde Zeitverlust auftreten kann wenn der Threadpool keine Threads mehr frei hat.
Pingback: Noser Blog C# Concurrency Teil 2: Die Gefahren von Multithreading - Noser Blog
Pingback: Noser Blog C# Concurrency Teil 4: Der Threadpool - Noser Blog
Pingback: Noser Blog C# Concurrency Teil 8: Delegate Tasks - Noser Blog