10. Parallele Prozesse
 
zurück
10.6 Das Rendezvous


Ada stellt zur Interaktion von zwei Prozessen u. a. das Rendezvouskonzept zur Verfügung.
Während eines Rendezvous können zwei Prozesse synchron Daten austauschen (Prozeßkommunikation).

Man unterscheidet bei den an einem Rendezvous beteiligten Prozessen den Aufrufer eines Eingangs und den Anbieter eines Eingangs, der einen Eingang vereinbart und Eingangsaufrufe dafür akzeptiert. Beim Rendezvous sind die beiden Beteiligten synchronisiert, d. h. daß ein Eingangsaufrufer blockiert wird, bis sein Eingangsaufruf akzeptiert wird. Merksatz: "Wer zu früh kommt, muß warten".

Ein Rendezvous wird durch den Aufruf eines (Prozeß-) Einganges eingeleitet. Wird der Aufruf akzeptiert, befinden sich beide Prozesse in einem Rendezvous.

Das Rendezvous zwischen den beiden Prozessen kommt zustande, wenn der rufende Prozeß den Eingangsaufruf und der gerufene Prozeß (Anbieter) die Annahmeanweisung erreicht hat.

Der zuerst zum Rendezvous eintreffende Prozeß wartet auf den Partner.


Um ein Rendezvous akzeptieren zu können, muß ein Prozeß einen Eingang besitzen. Ein solcher Prozeßeingang ist identisch zu Unterprogrammen zu parametrieren.


Beispiel:

-- Prozeßdefinition
task type parallel_berechnung is
   entry input( x : in integer );
   entry output( x : out float );
end parallel_berechnung;
-- Deklaration eines Prozessobjektes
prozess : parallel_berechnung;


Hier wird ein Prozeß mit zwei Eingängen definiert. Der erste Eingang nimmt einen Wert entgegen, der zweite gibt einen Wert heraus. Der Prozeß ist als Prozeßtyp ("type") definiert, um mehr als ein Objekt vom gleichen Typ erzeugen zu können. Ohne das Schlüsselwort"type" läge ein einziges, anonymes Prozeßobjekt vor.

Der Eingang eines Prozesses kann in jedem Anweisungsteil aufgerufen werden, in dem das Prozeßobjekt sichtbar ist (Eingangsaufruf).


Beispiel:

Prozess.Input( i ); -- Aufforderung zum Rendezvous durch einen Eingangsaufruf.
Prozess.Output(X => i);

Der Aufruf eines Prozeßeingangs ähnelt also einem vollqualifizierten Prozeduraufruf. Es steht immer zuerst der Name des Prozesses und danach, durch einen Punkt getrennt, der des Eingangs, eventuell gefolgt vom Parameterteil.


Akzeptieren eines Rendezvous

Die Aufforderung (Ruf) zu einem Rendezvous für einen bestimmten Eingang muß von dem gerufenen Prozeß mittels der Annahmeanweisung ("accept") akzeptiert werden. Danach synchronisieren sich beide Prozesse und das Rendezvous beginnt.


Beispiel:

with text_io;
with berechnung;

package body types2 is

   task body parallel_berechnung is
      i : integer := -1;
      f : float := 0.0;
   begin
      accept input( x : in integer ) do
         i := x;
      end input;

      f := berechnung( i );

      accept output( x : out float ) do
         x := f;
      end output;

   end parallel_berechnung;

end types2;

Der gerufene Prozeß kann nur dann einen Ruf annehmen, wenn er sich in der entsprechenden Annahmeanweisung ("accept") befindet. Ein Prozeß, der sich in einer Annahmeanweisung befindet (sich also blockiert), erhält frühestens dann die Prozessorkapazität, wenn ein Ruf zu diesem Eingang an ihn ergeht. Würde ein Prozeß den Eingang"output" aufrufen und erwartete der gerufene Prozeß einen Ruf für den anderen Eingang, so läge eine Verklemmung ("deadlock") vor. Eine Verklemmung liegt immer dann vor, wenn zwei (oder mehr) Prozesse aufeinander warten (z. B. in einer Accept-Anweisung). Auch ein Prozeß, der einen seiner eigenen Eingänge aufruft, verklemmt sich unweigerlich, denn er kann nicht gleichzeitig einen Eingang aufrufen und auch einen Eingangsaufruf für diesen akzeptieren.

Ein rufender Prozeß, der mit einem anderen in ein Rendezvous eintreten will, kann die Wartezeit zur Synchronisation begrenzen. Ein gerufener Prozeß kann auf ein oder mehrere alternative Rendezvous warten und diese mit Bedingungen versehen. Welches Rendezvous akzeptiert wird, ist abhängig von den jeweiligen Bedingungen.

Ada definiert Sprachmittel, um nach einer vorgegebenen Wartezeit den Versuch auf Synchronisation, Aufrufen und Akzeptieren abzubrechen.

Abruchbedingungen beim Aufrufen


1. Zeitlich befristeter Eingangsaufruf



Die Auswahlanweisung ("select") erlaubt mittels einer Verzögerungsalternative eine Zeitschranke festzulegen, die angibt, wie lange er mindestens auf die Akzeptierung des Eingangsaufrufes warten will (timed_entry_call <BNF>).


Beispiel:

select
   parallel_berechnung.input( i );
or
   -- Verzögerungsalternative
   delay 2 * 60.0;
   uebergabe_nicht_moeglich;
end select;


Im Beispiel wird mindestens zwei Minuten lang darauf gewartet, daß der Prozeß"parallel_berechnung" den Aufruf des Einganges"input" akzeptiert. Sind zwei Minuten vergangen und der rufende Prozeß wird aktiv, so wird zunächst die Prozedur"uebergabe_nicht_moeglich" und danach die nächste Anweisung nach der Auswahlanweisung ausgeführt.


2. Bedingter Eingangsaufruf


Will der rufende Prozeß nur dann in ein Rendezvous eintreten, wenn der gerufene Prozeß sofort dazu bereit ist, muß der rufende Prozeß die bedingte Auswahlanweisung benutzen.


Beispiel:

select
   parallel_berechnung.input( i );
else
   uebergabe_nicht_moeglich;
end select;


Akzeptiert der Prozeß "parallel_berechnung" nicht sofort die Aufforderung zum Rendezvous, so wird "uebergabe_nicht_moeglich" und danach die nächste Anweisung nach der Auswahlanweisung ausgeführt. Damit der Prozeß die Aufforderung sofort akzeptieren kann, muß er sich schon in einer Annahmeanweisung aufhalten.


3. Asynchrone Steuerflußänderung


Ein weiteres neues Sprachmittel ist die asynchrone Steuerflußänderung.

Durch einen Eingangsaufruf oder durch Ablauf vorgegebener Zeit wird der Abbruch der Ausführung eines Blockes herbeigeführt. Dieser auszuführende Block befindet sich bei dieser Anweisung zwischen "then abort" und"end select". In den folgenden beiden Beispielen ist die abzubrechende Anweisung mit "Ziemlich_lange_Berechnung" bezeichnet.


Beispiel 1:

-- Bei Aufruf des Eingangsaufrufes "Abbruch" wird die Ausführung
-- des Unterprogrammes "Ziemlich_lange_Berechnung" abgebrochen:
select
   accept Abbruch;
   put_line ("Ausführung wurde abgebrochen!");
then abort
   -- die Ausführung der hier enthaltenen Anweisungen wird abgebrochen,
   -- wenn während der Berechnung der Eingangsaufruf "Abbruch" stattfindet.
   Ziemlich_lange_Berechnung;
end select;




Beispiel 2:

-- nach Ablauf von 5.0 Sekunden wird die Ausführung
-- des Unterprogrammes "Ziemlich_lange_Berechnung" abgebrochen:
select
   delay 5.0;
   put_line ("Ausführung wurde abgebrochen!");
then abort
   -- die Ausführung der hier enthaltenen Anweisungen wird abgebrochen,
   -- wenn während der Berechnung die vorgegebene Zeit von 5.0
   -- Sekunden überschritten wird:
   Ziemlich_lange_Berechnung;
end select;

Abruchbedingungen beim Akzeptieren

Anders als beim Aufrufen eines Eingangs, bei dem genau ein Eingang angesprochen werden muß, kann ein Prozeß mehrere alternative Eingänge überwachen und diese mit Bedingungen versehen. Welcher Eingang akzeptiert wird, ist abhängig von den jeweiligen Bedingungen.

Es gibt drei Arten, ein Rendezvous zu akzeptieren: Siehe Befristetes Warten, Bedingtes Warten und Warten mit Prozeßbeendigung. Jede dieser Möglichkeiten kann mit einer booleschen Bedingung versehen werden, dem sogenannten Wächter.

Eine Alternative zur Akzeptierung eines Rendezvous heißt offen, falls sie keinen Wächter besitzt oder die Auswertung dieses Wächters "wahr" (True) ergibt, andernfalls heißt sie geschlossen.

Überwachen mehrerer Eingänge

with Text_IO;
use Text_IO;
procedure MutEx1 is
   -- Monitortask zum Kapseln einer Variable mit Gewährleistung des
   -- wechselseitigen Ausschlusses (mutual exclusion):
   task Monitor is
      entry Schreiben (Wert : in Integer);
      entry Lesen (Wert : out Integer);
      entry Ende;
   end Monitor;
   task body Monitor is
      Lokal : Integer := -1; -- gekapselte Variable
   begin
      loop
         select
            accept Schreiben (Wert : in Integer) do
               Lokal := Wert;
            end Schreiben;
         or
            accept Lesen (Wert : out Integer) do
               Wert := Lokal;
            end Lesen;
         or
            accept Ende;
            exit;
         end select;
      end loop;
   end Monitor;
begin -- MutEx1
   Monitor.Schreiben (Wert => 4711);
end MutEx1;

1. Akzeptieren mit Wächter

Jeder Wächter wird beim Eintritt in die Auswahlanweisung genau einmal ausgewertet. Liefert sie den Wert "wahr" (True), so gilt die Alternative als offen und wird während der gesamten Verweildauer in der Auswahlanweisung berücksichtigt. Liefert der Wächter "falsch" (False), so gilt die Alternative als geschlossen und wird während der gesamten Verweildauer in der Auswahlanweisung nicht mehr berücksichtigt. Da ein Wächter nur einmal beim Eintritt in die Auswahlanweisung ausgewertet wird, ist es unerheblich, ob er während der Verweildauer seinen Wert ändert.


Beispiel:

with text_io;
with berechnung;
package body types3 is
   task body parallel_berechnung is
      i : integer := -1;
      f : float := 0.0;
      bedingung : boolean := true; -- Dies ist der Waechter.
   begin
      endlos_schleife:
      loop
         select
            when bedingung =>
               accept input( x : in integer ) do
                  i := x;
               end input;
               f := berechnung( i );
               bedingung := false;
         or
            when not bedingung =>
               accept output( x : out float ) do
                  x := f;
               end output;
               exit endlos_schleife; -- Abbruch
         end select;
         -- weitere Anweisungen.
      end loop endlos_schleife;
   end parallel_berechnung;
end types3;

Der Wächter"bedingung" hält eine der beiden Alternativen offen. Die Auswahlanweisung hat keine Wahl, ein Rendezvous für "input" oder "output" zu akzeptieren. Der Wächter verwehrt ihr eine Alternative. Ohne Wächter wartet der Prozeß gleichzeitig auf ein Rendezvous für "input" und"output". Die erste Aufforderung, gleichgültig welche, wird akzeptiert.

Der Wächter darf eine beliebige boolesche Bedingung sein, also z. B. auch eine Funktion mit Ergebnistyp Boolean. Es müssen nicht alle Alternativen mit einem Wächter geschützt werden.


2. Befristetes Warten


Die Akzeptierung eines Rendezvous kann durch eine Zeitschranke begrenzt werden. Diese Zeitschranke gibt die Mindestzeitdauer an, mit der auf einen Ruf gewartet wird. Diese Mindest-zeitdauer wird in der Verzögerungsalternative definiert.


Beispiel 1:

task Befrister_Server is
   entry Eingang;
end Befrister_Server;
task body Befrister_Server is
begin
   loop
      select
         accept Eingang;
         Put_Line ("Eingangsaufruf wurde bedient!");
      or
         delay 60.0;
         Put_Line ("Timeout nach einer Minute!");
      end select;
   end loop;
end Befrister_Server;



Beispiel 2:

with text_io;
with berechnung;
package body types4 is
   task body parallel_berechnung is
      i : integer := -1;
      f : float := 0.0;
          bedingung : boolean := true;
   begin
      endlos_schleife:
      loop
         select
            when bedingung =>
                accept input( x : in integer ) do
                   i := x;
                end input;
                f := berechnung( i );
                bedingung := false;
          or
             when not bedingung =>
                accept output( x : out float ) do
                   x := f;
                end output;
                exit endlos_schleife; -- Abbruch
          or
             -- Verzögerungsalternative
             delay 0.001;
          end select;

         -- Folgende Anweisung ist eventuell notwendig !
         -- Das Zuteilungsverfahren teilt nicht unbedingt allen Prozessen
         -- nacheinander die Prozessorkapazitaet zu. Falls sich immer nur
         -- zwei Prozesse abwechseln, deren 'bedingung' falsch (false) ist,
         -- bricht das Programm nicht ab.
         if not bedingung then
            delay 1.0;
         end if;
         -- weitere Anweisungen.
      end loop endlos_schleife;
   end parallel_berechnung;
end types4;
Je nach Wert von "bedingung" wird mindestens eine Millisekunde auf ein Rendezvous für den Eingang "input" oder "output" gewartet. Da die Auswahlanweisung nach der vorgebenen Zeit verlassen wird, auch wenn kein Rendezvous eingetreten ist, darf die Funktion"berechnung" nicht mehr außerhalb der Alternative stehen, denn bei einem Rendezvous für "output" wird kein Initialwert übergeben. In wieweit es sinnvoll ist, ein Ergebnis zu verlangen, wenn vorher kein Initialwert übergeben wurde, bleibe an dieser Stelle unerörtert.

Ist mehr als ein alternativer Zweig mit einer Zeitschranke in der Auswahlanweisung vorhanden, so wird derjenige Zweig ausgewählt, dessen Zeitschranke als erste überschritten ist (frühestes Verfallsdatum). Sind mehrere identische Zeitschranken angegeben, so wird eine zufällig ausgewählt.

Ein für die zeitliche Akzeptierung oft angeführtes Beispiel ist die "Toter-Mann"-Schaltung, die in Lokomotiven eingesetzt wird:

loop
   select
      accept tot_mann_taste;
   or
      delay 30.0;
      notbremsung;
   end select;
end loop;


Wird eine bestimmte Taste nicht innerhalb von 30 Sekunden nach der letzten Betätigung betätigt, so wird (z. B. der Zug) sofort gebremst.


3. Bedingtes Warten

Will ein Prozeß nur dann ein Rendezvous akzeptieren, wenn es sofort möglich ist und sonst nicht, so ist dies mit einem Else-Zweig zu erreichen.


Beispiel 1:

task Bedingter_Server is
   entry Eingang;
end Bedingter_Server;

task body Bedingter_Server is
begin
   loop
      select
         accept Eingang;
         Put_Line ("Eingangsaufruf wurde bedient!");
      else
         Put_Line ("Zum aktuellen Zeitpunkt kein Eingangsaufruf vorhanden!");
      end select;
   end loop;
end Bedingter_Server;


Beispiel 2:

with text_io;
with berechnung;
package body types5 is
   task body parallel_berechnung is
      i : integer := -1;
      f : float := 0.0;
      bedingung : boolean := true;
      procedure kein_rendezvous_erhalten is
      begin
         text_io.put_line( "kein Rendezvous erhalten" );
      end kein_rendezvous_erhalten;
   begin
      endlos_schleife:
      loop
         select
            when bedingung =>
               accept input( x : in integer ) do
                  i := x;
               end input;
               f := berechnung( i );
               bedingung := false;
         or
            when not bedingung =>
               accept output( x : out float ) do
                  x := f;
               end output;
               exit endlos_schleife; -- Abbruch
         else -- Else-Zweig
              -- Kein Prozeß hat auf ein kein_rendezvous_erhalten;
              -- Rendezvous gewartet.
         end select;

         -- Manche Zuteilungsvefahren erfordern fuer den gewuenschten Ablauf
         -- folgende Anweisung.
         -- delay 0.1;
         -- weitere Anweisungen.
      end loop endlos_schleife;
   end parallel_berechnung;
end types5;

Immer dann, wenn kein anderer Prozeß bereit ist, sofort mit der gerade offenen Alternative (Eingang) in ein Rendezvous einzutreten, wird die (lokale) Prozedur "kein_rendezvous_erhalten" aufgerufen. Da die Auswahlanweisung sofort verlassen wird, wenn kein Rendezvous eingetreten ist, darf die Funktion"berechnung" nicht mehr außerhalb der Alternative stehen, denn es wird nicht immer ein Initalwert übergeben. Ein Rendezvous kommt genau dann zustande, wenn ein Prozeß einen Eingangsaufruf getätigt hat und wartet.

In diesem Beispiel läuft der Prozeß ständig ohne Pause in einer Endlosschleife. Ein solches Verhalten wird als aktives Warten ("busy waiting") und in diesem speziellen Fall auch als "polling" bezeichnet. "Polling" steht allgemein für den Vorgang, daß eine Schnittstelle, hier ein Eingang, abgefragt wird, ob irgendetwas abzuholen ist und wenn nicht, die Abfrage abgebrochen und umgehend von neuem gestartet wird. "Polling" wie auch "busy waiting" beansprucht (viel) Prozessorzeit, meist ohne Ergebnis. Diese Zeit könnte anderen Prozessen zur Verfügung stehen, wenn dieser Prozeß nur ab und zu die Schnittstelle abfragte.


4. Warten mit Prozessbeendigung



Ein Prozeß, der einen Eingangsaufruf akzeptiert, wartet mit Sicherheit dann vergeblich, wenn alle anderen Prozesse, die ihn für ein Rendezvous aufrufen könnten, den Status beendet haben. Durch das Akzeptieren mit einer Terminate-Alternative wird in einem solchen Fall auch der wartende Prozeß kooperativ beendet.


Beispiel 1:

task Beendender_Server is
   entry Eingang;
end Beendender_Server;

task body Beendender_Server is
begin
   loop
      select
         accept Eingang;
         Put_Line ("Eingangsaufruf wurde bedient!");

      or
         terminate; -- Beendigung, wenn alle abhängigen Prozesse und
                    -- der Meister fertig ausgeführt sind oder in einer
                    -- Terminate-Alternative stehen.
      end select;
   end loop;
end Beendender_Server;



Beispiel 2:

with text_io;
with berechnung;

package body types6 is

   task body parallel_berechnung is
      i : integer := -1;
      f : float := 0.0;
      bedingung : boolean := true;
      procedure kein_rendezvous_erhalten is
      begin
         text_io.put_line( "kein Rendezvous erhalten" );
      end kein_rendezvous_erhalten;
   begin
      endlos_schleife:
      loop
         select
            when bedingung =>
               accept input( x : in integer ) do
                  i := x;
               end input;
               f := berechnung( i );
               bedingung := false;
         or
            when not bedingung =>
               accept output( x : out float ) do
                  x := f;
               end output;
         or -- Kein Prozess kann noch ein
            terminate; -- Rendezvous eingehen.
         end select;
         delay 0.1; -- Fuer bestimmte Zuteilungsverfahren notwendig.
                    -- weitere Anweisungen.
      end loop endlos_schleife;
   end parallel_berechnung;
end types6;

Es wäre möglich, die alternative Auswahl "terminate" mit einem Wächter zu versehen. Eine"terminate"- Alternative kann keine weiteren Anweisungen enthalten. Die Reihenfolge ist unerheblich, die Terminate-Alternative kann auch am Anfang oder mitten unter anderen Alternativen stehen. Als letzte Alternative ist sie allerdings am auffälligsten und daher zu bevorzugen. Sie darf nicht mit einer Verzögerungsalternative ("delay") oder in Verbindung mit dem bedingten Warten verwendet werden.

Beachte:

Für den Anbieter eines Rendezvous gibt es eine Vielzahl an Möglichkeiten, den Eingangsaufruf zu akzeptieren. Neben der einfachen Accept-Anweisung gibt es das Selektive Akzeptieren, das vom Anbieter verwendet wird, um:
  1. mehr als einen Eingang gleichzeitig auf Eingangsaufrufe zu überwachen.
  2. einen oder mehrere Eingänge nur zum aktuellen Zeitpunkt zu überprüfen und bei Fehlen von Eingangsaufrufen sofort das Rendezvousangebot wieder abzubauen.
  3. einen oder mehrere Eingänge nur für eine bestimmte Zeitdauer zu überwachen
  4. einen oder mehrere Eingänge zu überwachen und bei bestimmten Bedingungen sich kooperativ selbst zu beendigen.
Zusätzlich kann man jeden Eingang mit einem booleschen Ausdruck versehen, um Eingänge zu öffnen oder zu schließen (Wächter, guards)

Für den Aufrufer eines Eingangs existieren ebenfalls Möglichkeiten, Einfluß auf das Rendezvous zu nehmen, um
  1. einen Eingangsaufruf nur für eine bestimmte Zeitdauer zu tätigen (timed_entry_call).
  2. einen Eingangsaufruf nur für den aktuellen Zeitpunkt gelten zu lassen. Wird er nicht sofort akzeptiert, wird der Aufruf storniert (conditional_entry_call)
Zusätzlich können mittels der asynchronen Selekt-Anweisung Anweisungsfolgen durch akzeptierte Eingangsaufrufe oder abgelaufene Zeitdauern unterbrochen werden.


 
zurück
 Index   Ada Tour - Dokumentation  
© 2003 Förderverein Ada Deutschland e.V.