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).
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:
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/
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).
Foreground-Threads verhindern das herunterfahren vom Programm bis sie beendet wurden. Threadpool Threads sind immer background-Threads und das kann man nicht ändern.
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.
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.
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.
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 } }
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:
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.
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.
[…] ← Vorige Post Nächster Post → […]
[…] letzten Blog haben wir gesehen, dass die bewährte Thread Klasse in gewisse Fälle immer noch die beste Lösung ist. Für kurze Aufgaben wird aber der Threadpool […]
[…] Task.Factory.StartNew() wird häufig mit der Option TaskCreationOptions.LongRunning verwendet. Damit gibt man dem TaskScheduler den Hinweis, dass der Task länger dauert. Der TaskScheduler wird daraufhin anstatt eines ThreadPool-Threads einen eigenen Thread für die Aufgabe verwenden (bei der jetzigen Implementation vom .NET Framework, siehe Abschnitt „Langläufige Aufgaben“ C# Concurrency Teil 3: Die bewährte Thread Klasse). […]