Martin Korneffel: IT Beratung, Softwareentwicklung Hans-Kächele-Str. 11, 70599 Stuttgart -- Tel: 0711/5283392 -- Email: Martin.Korneffel@t-online.de -- Web: www.mkoit.de |
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.
Jeder Befehlsdatenstrom hat einen Anfang. Dieser Einssprungpunkt muß 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 Einspungpunkt in den Befehlsdatenstrom kann verpackt in einem ThreadStart- Objekt 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:
myFirstThread.start();
In einem Befehlsdatenstrom kann jederzeit auf die ihn verwaltende Thread Instanz zugegriffen werden mittels:
public static Thread CurrentThread {get;}
Für eine frei definierbare Zeitspanne kann ein Thread blockiert werden mittels:
class Thread { ... public static void Sleep(Int32); public static void Sleep(TimeSpan); … }
Bei Übergabe einer 0 an Sleep wird die restliche Zeitscheibe abgegeben.
Durch Timeout.Infinite blockiert der Thread bis zum Programmende oder bis zum Ausführung der Methode <ThreadInstanz>.Interrupt()
Durch die Methode <ThreadInstanz>.Suspend() wird ein Thread auf unbestimmte Zeit blockiert. Mittels <ThreadInstanz>.Resume() kann die Blockade wieder aufgehoben werden.
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 !
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:
Highest
AboveNormal
BelowNormal
Lowest
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.
Überschneiden sich Befehlsdatenströme mehrerer verschiedener Threads, 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.
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(); } } }
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
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.
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(); }