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);
…
}
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()
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:
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.
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();
}