Advanced   Java   Services Monitor.wait() und Monitor.Pulse() Back Next Up Home


Wait() und Pulse() sind ein Team

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.


Wait() und Pulse() verwenden

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);
   // ...
}

Beispiel 1

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

csharp-threads-49.jpg

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.



Mit Monitor.Enter() und Monitor.Exit() einen synchronisierten Bereich betreten und verlassen

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

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.


Consumer-Producer Modell

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.


Beispiel

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


Ablauf für verschiedene, aber feste sleep()-Zeiten

In diesem Fall wird ein Thread in der Sleep-Phase unterbrochen, der andere in der Wait-Phase.

csharp-threads-50.jpg


Ablauf für zufällig gewählte sleep()-Zeiten

Hier kann es vorkommen, daß die Interrupts Consumer und Producer in der sleep-Phase unterbrechen.

csharp-threads-516.jpg

Valid XHTML 1.0 Strict top Back Next Up Home