Advanced Java Services | Monitor.wait() und Monitor.Pulse() |
Ist ein Thread im Zustand Sleep so schläft der Thread solange bis das Zeitintervall abgelaufen ist oder auf eine Interruptaufforderung mit einem Abbruch des Threads reagiert wird. Mit der Methode Wait() gibt es einen neuen Pausenzustand. Durch einen Aufruf vont Wait() geht ein Thread in einen Wartezustand, der im Normalfall durch einen Aufruf von Pulse() oder PulseAll() beendet wird, der von einem anderen Thread ausgelöst wird. Dazu ist es notwendig, daß beide Aufrufe synchronisiert zum gleichen Objekt stattfinden. Ist das nicht der Fall, so wird eine System.Threading.SynchronizationLockException ausgelöst. In Ausnahmefällen kann ein Wait() auch durch eine Interruptaufforderung beendet werden. Durch die statische Hilfsklasse System.Threading.Monitor stehen Wait(), Pulse() und PulseAll() für jedes Objekt zur Verfügung.
Es gibt nur zwei Möglichkeiten, Wait() und Pulse() zu verwenden, da sie in gelockten Bereichen stehen müssen. Falls die ganzen Methoden gelockt sind, schreibt man
[MethodImplAttribute(MethodImplOptions.Synchronized)] public void MyMethod() { // ... Monitor.Wait(x); // ... }
bzw.
[MethodImplAttribute(MethodImplOptions.Synchronized)] public void MyMethod() { // ... Monitor.Pulse(x); // ... }
Ist dies nicht der Fall, so muß man die Abschnitte locken.
lock(x) { // ... Monitor.Pulse(x); // PulseAll(x); // ... }
und
lock(x) { // ... Monitor.Wait(x); // ... }
Das Beispiel zeigt einen Thread Waiter, der dreimal im Sekundentakt die Uhrzeit ausgibt und sich dann selbst in einen wait-Zustand versetzt. Main wartet mit sleep() fünf Sekunden und benachrichtigt dann den Thread mit Pulse().
Die Threadklasse
class Waiter
{
public Waiter()
{}
public void Task()
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine("waiter " + DateTime.Now.ToLongTimeString());
Thread.Sleep(1000);
}
lock(this)
{
Console.WriteLine("waiter waits now");
Monitor.Wait(this);
}
Console.WriteLine("waiter received notification");
Console.WriteLine("waiter " + DateTime.Now.ToLongTimeString());
}
} // end class Waiter
Die Mainklasse
public static void Main(String[] args)
{
Waiter waiter = new Waiter();
Thread th = new Thread(waiter.Task);
th.Start();
Console.WriteLine("main is going to sleep 5 seconds");
Thread.Sleep(5000);
Console.WriteLine("main ends sleeping, notifies waiter");
lock(waiter)
{
Monitor.Pulse(waiter);
}
}
Die Ausgabe
Man beachte, daß Wait() und Pulse() zum gleichen Objekt synchronisiert sind. Das ist notwendig. Eine Synchronistion zu verschiedenen Objekten führt ebenfalls zu einer SynchronizationLockException.
Die Klasse Monitor ermöglicht es, auf eine weitere Art einen synchronisierten Block einzurichten. Das Konstrukt
lock (x) { //.... }
ist nach msdn.microsoft.com/en-us/library/ms173179.aspx?cs-save-lang=1&cs-lang=csharp#code-snippet-2 äquivalent zu
Monitor.Enter(x); try { //... } finally { Monitor.Exit(x); }
Beispiel 2 entspricht dem Beispiel 1, jedoch wird statt lock() nun Monitor.Enter() und Monitor.Exit() verwendet.
Die Threadklasse
public class Waiter2
{
public Waiter2()
{}
public void Task()
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine("waiter " + DateTime.Now.ToLongTimeString());
Thread.Sleep(1000);
}
Monitor.Enter(this);
try
{
Console.WriteLine("waiter waits now");
Monitor.Wait(this);
}
finally
{
Monitor.Exit(this);
}
Console.WriteLine("waiter received notification");
Console.WriteLine("waiter " + DateTime.Now.ToLongTimeString());
}
}
Die Mainklasse
public static void Beispiel2()
{
Waiter2 waiter = new Waiter2();
Thread th = new Thread(waiter.Task);
th.Start();
Console.WriteLine("main goes to sleep");
Thread.Sleep(5000);
Console.WriteLine("main ends sleeping, notifies waiter");
Monitor.Enter(waiter);
try
{
Monitor.Pulse(waiter);
}
finally
{
Monitor.Exit(waiter);
}
}
Die Ausgabe ist natürlich analog zum ersten Beispiel.
Da Wait() den Lock zurückgibt eignet es sich gut für sogenante Consumer-Producer Modelle. Consumer und Producer sind Threads die ein Lager leeren oder füllen, d.h. sie greifen auf ein gemeinsames Lager zu. Das Lager bietet Put() und Get() Methoden an mit denen man Waren im Lager deponieren oder entnehmen kann. Put() und Get() können mit Wait() in einen Wartezustand gehen je nachdem ob das Lager voll oder leer ist. Daher sind Put() und Get() synchronisiert.
Die klassische Lösung des Consumer-Producer-Modells ist zugleich simpel und raffiniert. Diese Lösung arbeitet mit einer einzigen booleschen Variablen, die meist available heißt. Die Put()-Methode "denkt" sich, "solange was da ist, braucht man nichts Auffüllen, also warten", und die Get()-Methode denkt sich "solange nichts da ist, kann nichts abgeholt werden, also warten". In den Put()- und Get()-Methoden wechseln sich also wait() und notify() ab.
In diesem Beispiel leeren (Consumer) oder füllen (Producer) das Lager in unregelmäßigen Abständen quasi endlos. Dies wird durch unterschiedliche Sleep()-Zeiten und eine Endlosschleife realisiert.
In Main() soll das Programm durch Interrupt-Aufrufe für Consumer und Producer beendet werden. Die Interrupts können die Threads in zwei verschiedenen Zuständen erreichen, in der Sleep()-Phase oder in der Wait()-Phase. In beiden Fällen soll die Anwendung beendet werden, hierbei fordert die Beendigung durch Unterbrechung der Wait()-Phase den größeren Aufwand. Das Beenden während der Wait()-Phase wird durch geeignete Returnwerte der Get()- und Put()- Methode erreicht.
Das Lager sieht daher folgendermaßen aus.
using System; using System.Threading; using System.Runtime.CompilerServices; // [MethodImplAttribute(MethodImplOptions.Synchronized)] public class Stock { private int MAX; // maximaler Bestand private int content; // aktueller Lagerbestand private bool available = false; public Stock(int MAX) { this.MAX = MAX; this.content = MAX - 1; } /* Put */ [MethodImplAttribute(MethodImplOptions.Synchronized)] public int Put() { // === solange was da, braucht nicht aufgefüllt werden, also warten === while (available == true) { Console.WriteLine(Thread.CurrentThread.Name + " must wait"); try { Monitor.Wait(this); } catch (ThreadInterruptedException ex) { return -1; } } //hier ist available = false content++; // nach Auffüllen kann available true gesetzt werden. Console.WriteLine(Thread.CurrentThread.Name + " puts 1, content = " + content); available = true; Monitor.PulseAll(this); return 0; } /* Get */ [MethodImplAttribute(MethodImplOptions.Synchronized)] public int Get() { Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US"); // funzt // === solange nichts da ist, kann nichts abgeholt werden, also warten === while (available == false) { Console.WriteLine("Stock: Get: " + Thread.CurrentThread.Name + " must wait"); try { Monitor.Wait(this); } catch (ThreadInterruptedException ex) { return -1; } } // available = true content--; Console.WriteLine(Thread.CurrentThread.Name + " gets 1, content = " + content); // da der genaue Stand nicht geprüft wird, setzt man hier sofort auf false available = false; Monitor.PulseAll(this); return 1; } } // end Stock
Der Consumer Falls der Interrupt während der Wait-Phase ankommt, liefert get -1 zurück.
public class Consumer { private String name; private Stock lager; private int sleepTime; private Random rd = new Random(); public Consumer(Stock c, String name, int sleepTime) { this.lager = c; this.name = name; this.sleepTime = sleepTime; } public void Task() { Thread.CurrentThread.Name = name; int ret = 0; for (; ; ) { ret = lager.Get();// 1 abholen if (ret == -1) // Get hat einen Interrupt erhalten (A) { Console.WriteLine("consumer wait interrupted, end consumer"); return; } try { //Thread.Sleep(rd.Next(1, sleepTime)); Thread.Sleep(sleepTime); } catch (ThreadInterruptedException ex) { Console.WriteLine("consumer sleep interrupted, end consumer"); break; // 1 } } } } // end consumer
Der Producer Falls der Interrupt während der wait-Phase ankommt, liefert get -1 zurück.
public class Producer { private String name; private Stock lager; private int sleepTime; private Random rd = new Random(); public Producer(Stock lager, String name, int sleepTime) { this.lager = lager; this.name = name; this.sleepTime = sleepTime; } public void Task() { Thread.CurrentThread.Name = name; int ret = 0; for (; ; ) { ret = lager.Put(); // auffüllen um 1 if (ret == -1) // Put hat einen Interrupt erhalten (B) { Console.WriteLine("producer wait interrupted, end producer"); return; } try { //Thread.Sleep(rd.Next(1, sleepTime)); Thread.Sleep(sleepTime); } catch (ThreadInterruptedException ex) { Console.WriteLine("producer sleep interrupted, end producer"); break; // 2 } } } } // end Producer
Main
public static void Main(String[] args) { Stock lager = new Stock(5); Consumer c1 = new Consumer(lager, "consumer", 20); Producer p1 = new Producer(lager, "producer", 100); Thread consumerThread = new Thread(c1.Run); Thread producerThread = new Thread(p1.Run); consumerThread.Start(); producerThread.Start(); try { Thread.Sleep(1000); consumerThread.Interrupt(); producerThread.Interrupt(); } catch (ThreadInterruptedException ex) { Console.WriteLine(ex); } } // end Main
In diesem Fall wird ein Thread in der Sleep-Phase unterbrochen, der andere in der Wait-Phase.
Hier kann es vorkommen, daß die Interrupts Consumer und Producer in der sleep-Phase unterbrechen.