Advanced Java Services | Synchronisieren von Methoden und Blöcken |
Es gibt bekanntlich Resourcen, die exklusiv vergeben werden müssen, hierzu gehören u.a. Ausgaben auf den Drucker und Schreibzugriffe auf Dateien. Damit ein Thread eine Aufgabe exklusiv abarbeiten kann muß der zugehörige Code, eine ganze Methode oder ein Block besonders gekennzeichnet werden. Betritt ein Thread einen derartigen Codebereich wird dieser exklusiv abgearbeitet. Kein anderer Thread kann dann diesen Bereich betreten. Dazu hat jedes Objekt einen Monitor. Diese Überwachungsinstanz enthält pro Objekt genau einen Lock. Will ein Thread einen geschützten Bereich betreten, so muß er vom zugehörigen Objekt einen Lock anfordern. Ist dieser bereits vergeben, so muß er warten. Da es nur einen Lock gibt sperrt ein Thread, der diesen Lock besitzt zwangsläufig alle anderen auf diese Art geschützten Bereiche dieses Objekts. Es werden damit evtl. auch Bereiche gesperrt, die mit dem von diesem Thread gerade abgearbeiteten Code garnichts zu tun haben. Performanceverluste sind die logische Folge dieses sozusagen rabiaten Verhaltens.
Zum Synchronisieren von ganzen Methoden stellt C# in System.Runtime.CompilerServices die Klasse MethodImplAttribute bereit. Dem Konstruktor dieser Klasse können verschiedene Optionen übergeben werden, die die Enumeration MethodImplOptions zur Verfügung stellt (siehe msdn unter system.runtime.compilerservices.methodimplattribute und system.runtime.compilerservices.methodimploptions). Hier interessiert nur die Option MethodImplOptions.Synchronized.
Eine Methode, die exklusiv ablaufen soll, egal ob statisch oder nichtstatisch wird folgendermaßen gekennzeichnet.
[MethodImplAttribute(MethodImplOptions.Synchronized)] public void MyMethod() { // Code }
Um nicht nur den exklusiven Zugriff zu demonstrieren sondern auch das Blockieren der anderen Methoden werden in der folgenden Klasse drei synchronisierte Methoden implementiert, die jeweils von drei nichtsynchronisierten Methoden gerufen werden. Dadurch kann es vorkommen, daß versuchte Methodenaufruf zunächst mißlingt, weil ein anderer Thread in der Zwischewnzeit zum Zug kommt. Hat aber ein Thread eine Methode, so können die anderen Methoden nicht ablaufen.
class Test { public void Calling1() { Console.WriteLine("Try to Call Method1"); Method1(); } public void Calling2() { Console.WriteLine("Try to Call Method2"); Method2(); } public void Calling3() { Console.WriteLine("Try to Call Method3"); Method3(); } // Das folgende Attribut synchronisiert die ganze Methode [MethodImplAttribute(MethodImplOptions.Synchronized)] public void Method1() { Console.Write("Method1 "); for (int i = 0; i < 10; i++) { Console.Write(". "); Thread.Sleep(200); } Console.WriteLine(); } [MethodImplAttribute(MethodImplOptions.Synchronized)] public void Method2() { Console.Write("Method2 "); for (int i = 0; i < 10; i++) { Console.Write("- "); Thread.Sleep(200); } Console.WriteLine(); } [MethodImplAttribute(MethodImplOptions.Synchronized)] public void Method3() { Console.Write("Method3 "); for (int i = 0; i < 10; i++) { Console.Write("* "); Thread.Sleep(200); } Console.WriteLine(); } } // end class
public static void Main(string[] args) { Test test = new Test(); Thread th1 = new Thread(test.Calling1); Thread th2 = new Thread(test.Calling2); Thread th3 = new Thread(test.Calling3); th1.Start(); th2.Start(); th3.Start(); }
Drei Abläufe
Es gibt in C# verschiedene Möglichkeiten Codeblöcke exklusiv ablaufen zu lassen, die einfachste arbeitet mit dem Schlüsselwort lock.
lock(this) { // statements }
Das lock-Konzept ist nur eine Variante des Synchronisierens von ganzen Methoden, so kann man etwa statt
[MethodImplAttribute(MethodImplOptions.Synchronized)] public void MyMethod() { // Code }
auch schreiben
public void MyMethod() { lock(this) { // statements } }
Die folgende Methode enthält einen synchronisierten und einen nichtsynchronisierten Teil. Zwei Threads greifen auf die Methode zu und geben zwei Strings zehnmal aus, einmal ungeschützt und einmal geschützt.
class SynchronizedDemo2 { public void Print(String s1, String s2) { // nicht gelockt for (int i = 0; i < 10; i++) { Console.Write(s1); Thread.Sleep(200); } lock(this) { for (int i = 0; i < 10; i++) { Console.Write(s2); Thread.Sleep(200); } } } }
Der erste Thread gibt 1 (ungeschützt) und A (geschützt) aus. Der zweite Thread gibt 2 (ungeschützt) und B (geschützt) aus.
public static void Main(string[] args) { SynchronizedDemo2 sd = new SynchronizedDemo2(); Thread th1 = new Thread(() => sd.Print("1", "A")); Thread th2 = new Thread(() => sd.Print("2", "B")); th1.Start(); th2.Start(); th1.Join(); th2.Join(); // Nur damit, ein Zeilenvorschub nach Beendigung der Threads gemacht wird. Console.WriteLine(); }
Drei Abläufe
Mit lock kann man auch die ganze Methode synchroniseren indem man statt
[MethodImplAttribute(MethodImplOptions.Synchronized)] public void Method() { // statements }
schreibt
public static void Method() { lock(this) { // statements } }
In einer statischen Methode hat man keinen Objektbezug. In diesem Fall hilft man sich mit einem statisch angelegten Objekt im privaten Datenteil und kann dann zu diesem Synchronisieren.
class SynchronizedDemo3 { private static Object lockObject = new Object(); public static void Print(String s1, String s2) { // nicht gelockt for (int i = 0; i < 10; i++) { Console.Write(s1); Thread.Sleep(200); } lock (lockObject) { for (int i = 0; i < 10; i++) { Console.Write(s2); Thread.Sleep(200); } } } }
Hier erhebt sich natürlich die Frage: Was passiert mit dem Lock während der Thread schläft?. Da es die sleep()-Phasen auch im synchronisierten Bereich gibt wird der Lock nicht zurückgegeben. Sonst würde ja der zweite Thread darauf zugreifen und die Ausgaben würden sich mischen. Die folgenden Tabelle klärt das für alle threadrelevanten Methoden.
Es ist wichtig zu wissen, welche Methoden beim Aufruf einen Lock zurückgeben oder nicht.
Lockverhalten der threadrelevanten Methoden | ||
Methode | Klasse | Lockverhalten |
Join() | Thread | Behält den Lock |
Sleep() | Thread | Behält den Lock |
Yield() | Thread | Behält den Lock |
Was passiert, wenn ein Thread im Besitz eines Locks ist und einen weiteren Bereich betreten will, der mit demselben Lock geschützt ist. Er wird einen Lock anfordern, aber er kann ja keinen mehr bekommen. Kann sich der Monitor merken, an wen er den Lock gegeben hat. Kann er sagen, du hast ja den Lock schon, verwende ihn. Wir betrachten die folgende Situation.
public class SynchronizeIsReentrant { [MethodImplAttribute(MethodImplOptions.Synchronized)] public void First() { Console.WriteLine("First"); Second(); Console.WriteLine("First"); Second(); } [MethodImplAttribute(MethodImplOptions.Synchronized)] public void Second() { Console.WriteLine("Second"); } }
Beim Eintritt in die Methode Second() fordert der ausführende Thread erneut einen Lock vom Monitor. Nun gibt es zwei Möglichkeiten wie der Monitor reagieren kann. Dazu stellen wir uns den Monitor mal als sprechendes Wesen vor.
Erste Möglichkeit:
Monitor sagt: Ich habe keinen Lock mehr (basta).
Zweite Möglichkeit:
Monitor sagt: Du hast doch schon den Lock, er gilt auch für die neue Situation.
In der ersten Situation spricht man von einem nichtreentranten Lock, die zweite Situation nennt man wie zu erwarten reentrant. Ein reentranter Lock ist so etwas wie ein Generalschlüssel, er sperrt überall. Wäre die Synchronisation nicht reentrant implementiert, so käme es hier zu einem Deadlock da ja der Monitor nur einen Lock hat.
Wir brauchen noch ein Main
private static void Main(string[] args) { SynchronizeIsReentrant sir = new SynchronizeIsReentrant(); Thread th1 = new Thread(sir.First); th1.Start(); }
Screenshot