Inhaltsverzeichnis         

7 .NET Multithreading

7.1 Einführung

In einem Prozess können mehrere Befehlsdatenströme parallel verarbeitet werden. Jeder Befehlsdatenstrom wird als Thread bezeichnet.




Die Befehlsdatenströme werden auf der CPU von einem Kern (Core) verarbeitet. Moderne CPU's besitzen mehrere Kerne (Duo = 2, Quad = 4), und können somit Threads tatsächlich parallel ausführen. Klassisch ist die quasi- parallele Verarbeitung von Threads mit einem einzigen Kern. Dabei wird nur ein Teil des Threads innerhalb eines kurzen Zeitintervalls (=Zeitscheibe) vom Kern verarbeitet. Der Zustand des Kerns am Ende der Zeitscheibe wird in einem Datensatz zum Thread gespeichert und dann an das Ende der Warteschlange der "Running Threads" gestellt. Aus der Warteschlange der "Running Threads" wird dann der nächste ausführungsbereite Thread in den Kern geladen und ausgeführt.

Hat eine CPU mehrere Kerne, dann wird jeder Kern im Zeitscheiben- Betrieb gefahren.

Befinden sich in einem Befehlsdatenstrom z.B. IO- Befehle für die Festplatte, deren Ausführungszeit extrem lang sein kann, dann kann der Thread blockieren. Dabei wird der Thread in den Pool der blockierten Threads gestellt (Waiting Threads). Anschließend wird der nächste ausführungsbereite Thread aus der Warteschlange in den Kern geladen. Aus dem Pool der Waiting Threads wird der Thread erst wieder zurück in die Warteschlange der Ausführungsbereiten gestellt, wenn der blockierende IO- Befehl beendet wurde.

Threads können auch dauerhaft blockiert werden. Sie befinden sich dann im Pool der suspendierten Threads, aus dem sie erst mit einem Resume – Befehl "befreit" werden können.








7.1.1 Wettstreit um die Ressourcen: Race Conditions

Durch die Verarbeitung von Befehlen werden die vom Anwender gewünschten Berechnungen durchgeführt. Dabei werden Werte aus den Registern, Arbeitsspeicher und Peripherieports gelesen und geschrieben. Register, Arbeitspeicher und Peheripherie werden als Ressourcen bezeichnet. Wenn mehrere Befehlsdatenströme gleichzeitig im System aktiv sind, kann es passieren, das ein und dieselbe Ressource von diesen gleichzeitig bearbeite wird. Diese wird als Race Condition bezeichnet.

Formalisierung:

Ta

Thread a

Ta.Op(t)

Operation (Befehl), der zum Zeitpunkt t im Thread ausgeführt wird

Ta.Op(t).IO

Ressource, die von einem Befehl im Thread zum Zeitpunkt t gelesen/geschrieben wird

RaceCondition

a != b && Ta.Op(t).IO != null && Tb.Op(t).IO != null && Ta.Op(t).IO == Tb.Op(t).IO



7.2 Erzeugen und Verwalten eines Threads




Jeder Befehlsdatenstrom hat einen Anfang. Dieser Einssprungpunkt muss eine Signatur besitzten, wie sie der vordefinierte Delegate ThreadStart definiert:

public delegate void ThreadStart();

Threads werden in .NET als Instanzen der Klasse Thread verwaltet. Der Einsprungpunkt in den Befehlsdatenstrom kann verpackt in einem ThreadStart- Objekt und dem Konstruktor der thread- Klasse übergeben werden.

Thread myFirstThread = new Thread(new ThreadStart(MyProc));

In die Warteschlange der ausführungsbereiten Threads wird ein Thread mit der Methode start() der Thread- Instanz gestellt:

myFirstThread.start();

In einem Befehlsdatenstrom kann jederzeit auf die ihn verwaltende Thread- Instanz zugegriffen werden mittels:

public static Thread CurrentThread {get;}

7.2.1 Thread im Kurzschlaf

Für eine frei definierbare Zeitspanne kann ein Thread blockiert werden mittels:

class Thread {
  ...
  public static void Sleep(Int32);
  public static void Sleep(TimeSpan);
  …
}



  1. Bei Übergabe einer 0 an Sleep wird die restliche Zeitscheibe abgegeben.

  2. Durch Timeout.Infinite blockiert der Thread bis zum Programmende oder bis zum Ausführung der Methode <ThreadInstanz>.Interrupt()

7.2.2 Threads im "Dornröschen" Schlaf

Durch die Methode <ThreadInstanz>.Suspend() wird ein Thread auf unbestimmte Zeit blockiert. Mittels <ThreadInstanz>.Resume() kann die Blockade wieder aufgehoben werden.

7.2.3 Thread beenden

Threads können vorzeitig mit der Methode Abort() beendet werden. Wurde für ein Threadinstanz Abort aufgerufen, dann wird seinem Befehlsdatenstrom die Ausnahme ThreadAbortException geworfen. Mittels eines try...catch- Blockes muß diese dann abgefangen werden.

Soll der vorzeitige Abbruch Abort() verhindert werden, dann muß im catch- Block ResetAbort() aufgerufen werden.

Achtung: Einem ResetAbort() in einem catch- Block darf kein goto- Befehl folgen- das ist verboten und führt zum Auslösen weiterer Ausnahmen !

7.2.4 Prioritäten

Wie wichtig die schnelle Ausführung des Befehlsdatenstroms hinter einem Thread ist, wird durch eine Priorität ausgedrückt. Die möglichen Werte werden durch den Enum ThreadPriority definiert:

Bezüglich der Prioritäten wird die Warteschlange der ausführungsbereiten Threads regelmäßig sortiert. Um auch Threads mit geringer Priorität eine Chance zur Ausführung zu ermöglichen, wird gelegentlich auch nach Absteigender Reihenfolge bezüglich der Prioritäten sortiert.

7.2.5 Threadpool

Q: Details zum CLR- Threadpool

Häufig können Anwendungen die Ressourcen besser nutzen, wenn wiederkehrende Teilaufgaben, die rechenintensiv sind oder langsame Peripherie ansteuern, asynchron ausgeführt werden. Um den Aufwand für das Erzeugen neuer, und löschen nicht mehr benötigter Threads zu minimieren, wurde der Threadpool erfunden.

An die Methode ThreadPool.QueueUserWorkItem(...) kann in einem WaitCallback - Delegate die Einsprungadresse des asynchron auszuführenden Unterprogramms übergeben werden. Beim ersten Aufruf wird dabei vom Threadpool ein Thread erzeugt und das Unterprogramm in diesem gestartet.




Wenn das Unterprogramm endet, dann wird der Thread mittels Suspend in den Dornröschen- Schlaf versetzt. Wird in den nächsten 40s an Threadpool erneut ein asynchron auszuführendes Unterprogramm übergeben, dann weckt dieser den Thread wieder auf und startet die Ausführung des neuen Unterprogramms in diesem.

Diese Vorgehensweise spart die Zeit für das Erstellen und Löschen eines Threads ein. Insbesondere bei häufigem Start asynchroner Prozeduren wird die zur Verfügung stehende Rechenleistung effizienter genutzt.

7.2.5.1 Asynchroner Methodenstart mittels Delegates

7.3 Synchronisierung

7.3.1 Kritische Abschnitte

Wenn eine Race Condition auftritt, kann es zu fehlerhaften Berechnungen kommen. Beispiel:

100: wert ++;
101: Console.WriteLine(wert);

Wird ein Thread nach Zeile 100 unterbrochen, dann können andere Threads die Variable wert erhöhen, ohne das der unterbrochene Thread etwas davon merkt. Nach beendeter Unterbrechung werden die Registerstände zurückgeladen, und der Thread gibt fälschlicherweise den alten Wert für wert aus, obwohl aktuell schon ein viel höherer Stand erreicht wurde.

Mittels der Klasse Monitor können kritische Abschnitte gesichert werden. Wird ein so gesicherter kritischer Abschnitt von einem Thread durchlaufen, blokieren alle anderen, wenn sie versuchen, diesen ebenfalls zu betreten.

 99: Monitor.Enter(this);
100:   wert ++;
101:   Console.WriteLine(wert);
102: Monitor.Exit(this);

C# als auch VB.NET bieten vereinfachte Konstruktionen an, die lock – Blöcke:

// C#
 99: lock(this) {
100:   wert ++;
101:   Console.WriteLine(wert);
102: }
' vb.net
 99: SyncLock Me 
100:   wert += 1
101:   Console.WriteLine(wert)
102: End SyncLock

Achtung: Kritische Abschnitte können mittels Monitor nur innerhalb eines Prozesses gesichert werden. Der konkurierende Zugriff auf Resourcen unabhängig aus verschiedenen Prozessen kann nur mittels benannter WaitHandle gesteuert werden.

7.3.1.1 Beispiel für Kritische Abschnitte

using System;
using System.Collections.Generic;
using System.Text;

namespace PCC
{
    public class CCritical
    {
        int eingang = 100000; // Hallo
        int ausgang = 0;

        public int transaktion()
        {
            int bilanzsumme = 0;
            try
            {
                //System.Threading.Monitor.Enter(this);

                // lock ruft auch Monitor Enter und Exit auf, stellt
                // aber sicher, daß in jedem Fall beim verlassen des 
                // Blockes Monitor.Exit aufgerufen wird
                lock (this)
                {
                    eingang--;

                    System.Threading.Thread.Sleep(100);

                    ausgang++;

                    bilanzsumme = eingang + ausgang;
                }

                //System.Threading.Monitor.Exit(this);
            }
            catch (Exception)
            {
            }

            return bilanzsumme; 
        }

        static CCritical critical = new CCritical();

        static void worker()
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("TId {0:d} Bilanz= {1:d}",
                    System.Threading.Thread.CurrentThread.ManagedThreadId,
                    critical.transaktion());
            }
                

        }

        public static void testeKritischenAbschnitt()
        {
            System.Threading.Thread t1 = new System.Threading.Thread(new System.Threading.ThreadStart(worker));
            t1.Priority = System.Threading.ThreadPriority.Lowest;
            t1.Start();

            System.Threading.Thread t2 = new System.Threading.Thread(new System.Threading.ThreadStart(worker));
            t2.Priority = System.Threading.ThreadPriority.Highest;
            t2.Start();

            t1.Join();
            t2.Join();
        }

    }
}

7.3.1.2 in VB

Imports System.Threading
Module Module1

    Dim log As New mko.CLogVb()
    Dim scanner As New MyDirTree(log)

    Sub worker()
        Dim path As String = "C:\Dokumente und Einstellungen\kurs1\Eigene Dateien"
        scanner.scanDir(path)
    End Sub

    Dim vorrat As Integer = 10000000
    Sub worker2()
        Static meinVorrat As Integer = 0
        While vorrat > 0
            'vorrat -= 1
            Interlocked.Decrement(vorrat)
            meinVorrat += 1
        End While
        Console.WriteLine("Vorrat von 2: {0:D}, Priorität: {1:G}", meinVorrat, Thread.CurrentThread.Priority)

    End Sub

    Sub worker3()
        Static meinVorrat As Integer = 0
        While vorrat > 0
            'vorrat -= 1
            Interlocked.Decrement(vorrat)
            meinVorrat += 1
        End While

        Console.WriteLine("Vorrat von 3: {0:D}, Priorität: {1:G}", meinVorrat, Thread.CurrentThread.Priority)

    End Sub

    Dim sparkonto As Integer = 1000
    Dim girokonto As Integer = 0

    Sub buchung()

        While sparkonto > 0
            ' Kritischer Abschnitt
            'SyncLock log
            Monitor.Enter(log)
            Try
                sparkonto -= 1
                Thread.Sleep(100)
                girokonto += 1

                Console.WriteLine("Bilanz: {0:D}", sparkonto + girokonto)
            Finally
                Monitor.Exit(log)
            End Try
            'End SyncLock
        End While

    End Sub




    Sub Main()
        Dim myThread As New Thread(AddressOf worker)

        myThread.Start()

        Console.WriteLine("Ich habe die ThreadId: {0:D}", Thread.CurrentThread.ManagedThreadId)
        Console.WriteLine("Warte auf Thread mit der Id: {0:D}", myThread.ManagedThreadId)

        ' Warten auf Ende bzw. zusammenführen der Threads
        myThread.Join()

        Console.WriteLine("Alle Threads beendet")

        '--------------------------------------------------------------------------
        ' Demo von Prioritäten
        Dim worker2Thread As New Thread(AddressOf worker2)
        Dim worker3Thread As New Thread(AddressOf worker3)

        worker2Thread.Priority = ThreadPriority.Highest
        worker3Thread.Priority = ThreadPriority.Lowest

        worker2Thread.Start()
        worker3Thread.Start()

        worker2Thread.Join()
        worker3Thread.Join()

        Dim bThread1 As New Thread(AddressOf buchung)
        Dim bThread2 As New Thread(AddressOf buchung)

        bThread1.Priority = ThreadPriority.Highest
        bThread2.Priority = ThreadPriority.Lowest


        bThread1.Start()
        bThread2.Start()

        bThread1.Join()
        bThread2.Join()

    End Sub

End Module

7.3.2 WaitHandle

Eine WaitHandle ist eine Betriebssystemresource, auf die ein Thread zugreifen kann und dabei blockiert, wenn sich diese in dem besonderen Zustand "nicht gesetzt" befindet. Mittels der Methode Set kann der Zustand von "nicht gesetzt" auf "gesetzt" geändert werden, wodurch die vorher blockierten Threads wieder in die Warteschlange der Ausführungsbereiten übertragen werden.

WaitHandles können über Prozessgrenzen hinweg eingesetzt werden.

7.3.2.1 Signalisierung mit AutoResetEvent

Manchmal ist es notwendig, das ein Thread seine Arbeit erst dann fortsertzt, bis bestimmte Ereignisse im System eingetreten sind. Dies kann durch Signalisierung über ein AutoResetEvent erreicht werden:

AutoResetEvent fertig = new AutoResetEvent(false);

string ipath;


public void traverse(string path)
{
  ipath = path;

  fertig.Reset();

  Thread thread_traverse = new Thread(new ThreadStart(worker));
  thread_traverse.Start();

  // Warten, bis worker das Ende der Arbeit signalisiert                
  fertig.WaitOne();
                        
                
}

void worker() 
{
  traverse_exe(ipath);
  // Signalisieren, das Arbeit beendet wurde
  fertig.Set();
}