Ein Einsatzgebiet parallel laufender Threads besteht in der gemeinsamen Abarbeitung einer Aufgabe. Durch die parallelen Threads können Teilbereiche der Aufgabe gleichzeitig erledigt werden. In solchen, aber auch in vielen andern Szenarien, ist eine Synchronisation der Threads nötig. Diese Synchronisation kann relativ einfach mittels Signalisierung oder Benachrichtigung realisiert werden. In C# stehen dafür die sogenannten Event Wait Handles zur Verfügung. In diesem Artikel möchte ich Ihnen diese Wait Handles vorstellen.
AutoResetEvent, ManualResetEvent
AutoResetEvent und ManualResetEvent stammen beide von der Klasse EventWaitHandle ab. Ein Wait Handle wird dazu genutzt einem Thread ein Signal zu senden. Ein EventWaitHandle kann mittels der Funktion Set gesetzt und mittels der Funktion Reset wieder aufgehoben werden. Dabei spielt es keine Rolle ob Funktionen mehrfach aufgerufen werden. Mehrfache Aufrufe haben die gleiche Wirkung wie der einfache Aufruf. Der wartende Thread kann mittels der Funktion WaitOne so lange pausieren bis das Wait Handle gesetzt wurde.
Der Unterschied zwischen AutoResetEvent und ManualResetEvent besteht im Verhalten nach dem Aufruf der WaitOne Funktion. Bei einem AutoResetEvent erfolgt ein automatisches Reset des Wait Handles. Beim ManualResetEvent bleibt das Wait Handle hingegen gesetzt.
Gibt es mehrere wartende Threads, dann wird beim AutoResetEvent nur ein Thread fortgesetzt und die anderen Threads warten bis zum jeweils nächsten Setzen des Wait Handles. Beim ManualResetEvent hingegen können gleich alle wartende Threads fortgesetzt werden.
Aus diesen beiden unterschiedlichen Verhaltensweisen der Wait Events ergeben sich deren typische Einsatzgebiete. AutoResetEvent wird hauptsächlich angewendet, wenn ein exklusiver Zugriff auf eine Ressource sichergestellt werden muss. ManualResetEvent kommt meist zum Einsatz wenn Threads auf das Beenden eines andern Threads warten müssen.
Das nachfolgende Beispiel zeigt die Verwendung von AutoResetEvent.
private static AutoResetEvent _waitHandle; static void Main(string[] args) { _waitHandle = new AutoResetEvent(false); new Thread(DoSomething).Start("Thread A"); new Thread(DoSomething).Start("Thread B"); new Thread(DoSomething).Start("Thread C"); Console.WriteLine("Press any key to continue"); Console.ReadKey(); _waitHandle.Set(); Thread.Sleep(100); Console.WriteLine("Press any key to continue"); Console.ReadKey(); _waitHandle.Set(); Thread.Sleep(100); Console.WriteLine("Press any key to continue"); Console.ReadKey(); _waitHandle.Set(); Thread.Sleep(100); } private static void DoSomething(object value) { _waitHandle.WaitOne(); Console.WriteLine(value); }
Die Anwendung erzeugt die nachfolgende Ausgabe. Wie Sie sehen können wird bei jedem Aufruf von Set nur einer der Threads fortgesetzt.
Press any key to continue
Thread B
Press any key to continue
Thread A
Press any key to continue
Thread C
Verwendet man hingegen ein ManualResetEvent, dann werden beim Aufruf von Set alle Threads fortgesetzt. Der nachfolgende Quellcode zeigt das entsprechend angepasste Beispiel.
private static ManualResetEvent _waitHandle; static void Main(string[] args) { _waitHandle = new ManualResetEvent(false); new Thread(DoSomething).Start("Thread A"); new Thread(DoSomething).Start("Thread B"); new Thread(DoSomething).Start("Thread C"); Console.WriteLine("Press any key to continue"); Console.ReadKey(); _waitHandle.Set(); Thread.Sleep(100); Console.WriteLine("Press any key to continue"); Console.ReadKey(); _waitHandle.Set(); Thread.Sleep(100); Console.WriteLine("Press any key to continue"); Console.ReadKey(); _waitHandle.Set(); Thread.Sleep(100); } private static void DoSomething(object value) { _waitHandle.WaitOne(); Console.WriteLine(value); }
Die Anwendung erzeugt nun die folgende Ausgabe:
Press any key to continue
Thread B
Thread A
Thread C
Press any key to continue
Press any key to continue
ManualResetEventSlim
Mit .Net 4.0 wurde eine neue Version des ManualResetEvent eingeführt, das ManualResetEventSlim. Dieses neue Event Wait Handle ist für kurze Wartezeiten optimiert. Das heisst, in Szenarien in welchen die Threads nur sehr kurz auf das Wait Event warten müssen, bietet sich das neue optimierte ManualResetEventSlim an. Sind die Wartezeiten hingegen nicht kurz, dann sollte das normale ManualResetEvent verwendet werden.
Der Zweck dieses neuen Wait Handles, also die Optimierung hinsichtlich Szenarien mit kurzen Wartezeiten, erklärt auch warum es kein AutoResetEventSlim gibt. Ein AutoResetEvent dient wie beschrieben hauptsächlich dem sicheren Zugriff auf eine gemeinsame Ressource. In solchen Fällen hat man typischerweise keine kurzen Wartezeiten. Daher wird auch kein AutoResetEventSlim angeboten.
CountdownEvent
Mittels des CountdownEvent kann auf mehr als einen Thread gewartet werden. Beim Erstellen des Wait Handles wird die Anzahl an Threads bzw. Wartesignalen angegeben. Die Wait Funktion wartet dann so lange bis diese Anzahl an Wartesignalen erreicht wurde. Das nachfolgende Beispiel zeigt eine entsprechende Anwendung.
private static CountdownEvent _waitHandle; static void Main(string[] args) { _waitHandle = new CountdownEvent(3); new Thread(DoSomething).Start("Thread A"); new Thread(DoSomething).Start("Thread B"); new Thread(DoSomething).Start("Thread C"); Console.WriteLine("Press any key to continue"); Console.ReadKey(); _waitHandle.Signal(); Thread.Sleep(100); Console.WriteLine("Press any key to continue"); Console.ReadKey(); _waitHandle.Signal(); Thread.Sleep(100); Console.WriteLine("Press any key to continue"); Console.ReadKey(); _waitHandle.Signal(); Thread.Sleep(100); } private static void DoSomething(object value) { _waitHandle.Wait(); Console.WriteLine(value); }
Alle drei Threads warten so lange bis die Anzahl an Signalen, in diesem Fall drei Signale, empfangen wurden. Die Anwendung erzeugt daher folgende Ausgabe.
Press any key to continue
Press any key to continue
Press any key to continue
Thread B
Thread A
Thread C
Fazit
Mittels der verschiedenen Event Wait Handles kann eine Signalisierung zwischen unterschiedlichen Threads implementiert werden. Diese Signalisierung dient der Synchronisierung der Threads. Es stehen unterschiedliche Wait Handles zur Verfügung, welche je nach Anwendungsszenario gewählt werden sollten.