en / de
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

C# Concurrency Teil 3: Die bewährte Thread Klasse

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:

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/
C# Concurrency - die bewährte Thread Klasse

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:

  1. Der Thread ist am warten in einem BCL (Base Class Library) blockierenden Aufruf (z.B. Thread.Sleep, WaitHandle.WaitOne).
  2. 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.
  3. 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.

← Vorige Post
Nächster Post →
Kommentare

3 Antworten zu “C# Concurrency Teil 3: Die bewährte Thread Klasse”

  1. […] 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 […]

  2. […] 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). […]

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden
NACH OBEN
Zur Webcast Übersicht