Die Computerseite von Eckart Winkler
Coroutinen in Modula-2

 

Dieser Artikel ist im Franzis-Verlag in der Zeitschrift "mc", Ausgabe 11/1987 erschienen. Außerdem gibt es hier noch einen analogen Artikel Coroutinen in C. Zur Erläuterung des verwendeten Zufallszahlengenerators sei auf den Artikel Zufallszahlengeneratoren verwiesen
 



Aufrufhierarchien

Die Programmiersprache Modula-2 hat ihren Namen erhalten, weil sie die Möglichkeit bietet, modular zu programmieren und Prozeduren zu universell anwendbaren Moduln zusammenzufassen. Darüber hinaus machen einige nicht so bekannte Konzepte diese Sprache beispielsweise für die Systemprogrammierung interessant. Eins ist das der sogenannten Coroutinen.

Um zu verstehen, was damit gemeint ist, müssen wir uns zunächst über die Vorgänge bei der Ausführung eines herkömmlichen Programms, etwa in Pascal, klarwerden. Da gibt es nämlich ein Hauptprogramm. Beim Programmstart wird mit der Ausführung von diesem begonnen. Ein Unterprogrammaufruf bewirkt, daß die Kontrolle an das betreffende Unterprogramm übergeben wird. Erst wenn es abgearbeitet ist, wird mit dem Hauptprogramm weitergemacht.

Das kann natürlich auch über mehrere Verschachtelungsebenen gehen. Das Hauptprogramm ruft z.B. ein Unterprogramm U1 auf, dieses wiederum U2. Nach Beendigung von U2 kehrt die Kontrolle zunächst zu U1 zurück. Erst wenn dieses fertig ist, kommt das Hauptprogramm wieder an die Reihe. Die Kontrollrückgabe geschieht jeweils automatisch, der Programmierer muß sich darum nicht kümmern.

Wir haben es also mit einer strengen Hierarchie zu tun. Jedes Unterprogramm gibt die Kontrolle an denjenigen Programmteil zurück, von dem es sie erhalten hat. Zudem erfolgt die Rückgabe der Kontrolle erst nach der vollständigen Abarbeitung des Unterprogramms.


Coroutinen, das Alternativkonzept

Mittels Coroutinen haben wir die Möglichkeit, diese beiden Regeln außer Kraft zu setzen. Denn jede Coroutine kann die Kontrolle explizit an eine beliebige andere Coroutine oder an das Hauptprogramm weitergeben. Dazu muß die Abarbeitung nicht am Coroutinen-Ende angelangt sein, ganz im Gegenteil, das ist überhaupt nicht gestattet. Erhält sie später die Kontrolle zurück, vielleicht von einer ganz anderen Coroutine, als der sie sie abgegeben hat, wird genau an der Unterbrechungsstelle weitergemacht. Die Werte aller lokaler Variabler bleiben über die Unterbrechung hinaus erhalten.

Da Modula für Einprozessorsysteme gedacht ist, ist zu jeder Zeit natürlich nur eine Coroutine aktiv. Übertragen beispielsweise auf die Simulation eines Gesellschaftsspiels heißt das, zu jeder Zeit ist nur ein Spieler am Zug. Gibt er die Kontrolle ab, kommt der nächste Spieler an die Reihe. Der ihn betreffende Spielstand, das heißt etwa die Stellung der Spielfiguren oder das vorhandene Spielgeld, muß natürlich erhalten bleiben. Das wäre der Zweck der lokalen Variablen.

Zugrunde gelegt wird jeweils eine gewöhnliche Prozedur. Durch die Anweisung NEWPROCESS wird sie zur Coroutine. Die Übergabe der Kontrolle geschieht durch die Anweisung TRANSFER. Dabei wird der augenblickliche Zustand der aufrufenden Coroutine eingefroren, d.h. alle Variablenwerte und alle Registerinhalte werden in einen eigenen Speicherbereich gerettet, damit beim nächsten Aufruf an genau dieser Stelle mit demselben Zustand fortgefahren werden kann.

Bei der aufgerufenen Coroutine läuft alles umgekehrt: Aus dem eigenen Speicherbereich werden alle Variablen und Registerinhalte geholt, und es wird an der Stelle weitergemacht, an der die letzte Unterbrechung stattgefunden hatte.


Implementierung von Coroutinen

So weit die Theorie. Wie sieht nun die Praxis in Modula-2 aus? Wir hatten bereits gesehen, daß jede Coroutine einen Speicherbereich benötigt, in dem ihr Status zum Unterbrechungszeitpunkt festgehalten wird. Zur Bereitstellung dieses Speicherbereichs gibt es zwei Möglichkeiten, die in der Wirkung völlig identisch sind:

1.) Wir verwenden den Speicherplatz einer zuvor deklarierten globalen Variablen. Dazu eignet sich ein Feld vom Typ WORD. Der Prozedur NEWPROCESS müssen wir die Startadresse und die Größe des Speicherbereichs (in Bytes) mitteilen. Diese Informationen erhalten wir durch ADR und SIZE. Der Typ WORD sowie die Funktionen ADR und SIZE müssen (genauso wie NEWPROCESS und TRANSFER) aus dem Pseudo-Modul SYSTEM importiert werden.

2.) Wir beschaffen uns den Speicherplatz mittels ALLOCATE (aus dem Bibliotheksmodul Storage). Hierzu müssen wir eine Variable va vom Typ ADDRESS (aus SYSTEM) deklarieren. Der Aufruf ALLOCATE(va,n) reserviert n Bytes Speicherplatz. Über die Variable va wird die Anfangsadresse ausgegeben, die vom System bestimmt wird.

Wie macht man aus einer Prozedur eine Coroutine? Nun, zunächst muß die Prozedur einigen Anforderungen genügen. So muß es eine parameterlose Prozedur sein. Die Klammern mit der Parameterliste hinter dem Prozedurnamen entfallen also. Außerdem muß, wie bereits erwähnt, dafür gesorgt sein, daß niemals das Ende der Prozedur erreicht werden kann. Denn ansonsten käme es zu einem Laufzeitfehler. Am einfachsten geht das mit Hilfe der Endlosschleife LOOP. Um der Coroutine einen Sinn zu geben, sollte auch mindestens eine TRANSFER-Anweisung vorhanden sein.

Zur Deklaration als Coroutine wird nun ein Name benötigt, der vom Prozedurnamen verschieden sein muß. Dieser wird als Typ PROCESS vereinbart, PROCESS stammt wieder aus SYSTEM. Jetzt fehlt lediglich noch die Anweisung NEWPROCESS. Diese ist in der folgenden Form anzugeben: NEWPROCESS(pn,st,si,cn). Hierbei ist pn der Prozedurname und cn der Coroutinenname. Unter diesem wird später auf die Coroutine Bezug genommen. st ist die Startadresse des Speicherbereichs, si dessen Größe.

Die Größe, die hier nötig ist, hängt von dem verwendeten Rechner ab. Selbst wenn keine Variablen verwendet werden, kann schon ein recht großer Bereich erforderlich werden, da die Registerinhalte mit abgespeichert werden. Das Hauptprogramm ist automatisch Coroutine. Hier ist ein NEWPROCESS nicht erforderlich.


Ein Beispiel

Wie bereits angesprochen, finden Coroutinen oft Anwendung im Bereich der Simulationen. Das folgende Beispielprogramm ist eine Spielsimulation. Simuliert wird ein Würfelspiel ähnlich "Mensch ärgere dich nicht" zwischen zwei Spielern. Gewürfelt wird abwechselnd, die Würfe eines jeden werden addiert. Wer als erster die Gewinnsumme erreicht, hier 11, hat gewonnen. Diese Summe muß exakt erreicht werden, sonst kommt der andere an die Reihe.

MODULE WuerfelSpiel;

FROM SYSTEM IMPORT WORD,ADR,SIZE,PROCESS,NEWPROCESS,TRANSFER;
FROM InOut IMPORT Write,WriteCard,WriteString,WriteLn,ReadCard;

CONST Gewinn=11;

VAR Spieler1,Spieler2,Haupt : PROCESS;
    Bereich1,Bereich2 : ARRAY[1..200] OF WORD;
    Start : [1..250];

PROCEDURE Sp1;
  VAR Summe,Wurf : CARDINAL;
  BEGIN
    Summe:=0;
    LOOP
      Wurf:=Wuerfel();
      WriteString('Spieler 1 //');WriteCard(Summe,3);WriteLn;
      WriteString('  gewuerfelt :');WriteCard(Wurf,2);WriteLn;
      IF Summe+Wurf>Gewinn THEN
        WriteString('  Ich kann nicht...');WriteLn;
        TRANSFER(Spieler1,Spieler2)
      ELSIF Summe+Wurf=Gewinn THEN
        WriteString('  Ich habe gewonnen!');WriteLn;
        TRANSFER(Spieler1,Haupt)
      ELSE
        Summe:=Summe+Wurf;
        WriteString('  Ich habe jetzt');WriteCard(Summe,3);WriteLn;
        TRANSFER(Spieler1,Spieler2)
      END (* IF *)
    END (* LOOP *)
  END Sp1;

PROCEDURE Sp2;
  VAR Summe,Wurf : CARDINAL;
  BEGIN
    Summe:=0;
    LOOP
      Wurf:=Wuerfel();
      WriteString('Spieler 2 //');WriteCard(Summe,3);WriteLn;
      WriteString('  gewuerfelt :');WriteCard(Wurf,2);WriteLn;
      IF Summe+Wurf>Gewinn THEN
        WriteString('  Ich kann nicht...');WriteLn;
        TRANSFER(Spieler2,Spieler1)
      ELSIF Summe+Wurf=Gewinn THEN
        WriteString('  Ich habe gewonnen!');WriteLn;
        TRANSFER(Spieler2,Haupt)
      ELSE
        Summe:=Summe+Wurf;
        WriteString('  Ich habe jetzt');WriteCard(Summe,3);WriteLn;
        TRANSFER(Spieler2,Spieler1)
      END (* IF *)
    END (* LOOP *)
  END Sp2;

PROCEDURE Wuerfel():CARDINAL;
  BEGIN
    Start:=(Start*Start) MOD 251;
    RETURN (Start MOD 6)+1
  END Wuerfel;

BEGIN (* Hauptprogramm *)
  WriteString('Startwert :');WriteLn;
  ReadCard(Start);
  WriteString('*** WUERFELSPIEL ***');WriteLn;WriteLn;
  WriteString('Gewinnsumme :');WriteCard(Gewinn,3);WriteLn;WriteLn;
  NEWPROCESS(Sp1,ADR(Bereich1),SIZE(Bereich1),Spieler1);
  NEWPROCESS(Sp2,ADR(Bereich2),SIZE(Bereich2),Spieler2);
  TRANSFER(Haupt,Spieler1);
  WriteString('*** ENDE DES SPIELS ***');WriteLn;
  ReadCard(Start)
END WuerfelSpiel.

Bei unserem Spiel sind also zwei Spieler beteiligt, jeder wird durch eine Coroutine repräsentiert. Die grundlegenden Prozeduren heißen Sp1 und Sp2. Sie sind natürlich weitgehend identisch.


Wie funktioniert es?

Betrachten wir also Sp1. Zur Initialisierung wird die Summe auf 0 gesetzt. Dann beginnt bereits die Endlosschleife. Zu Beginn wird eine Meldung ausgegeben, daß Spieler 1 an der Reihe ist, weiterhin wird die bisherige Summe bekanntgegeben. Dann wird gewürfelt, das Ergebnis ausgedruckt. Es können drei Fälle eintreten. Zum einen kann mit diesem Wurf die Gewinnsumme überschritten worden sein. Dann muß man die Kontrolle dem Gegenspieler überlassen. Oder die Gewinnsumme ist genau erreicht. Dann hat der Spieler gewonnen, die Kontrolle geht zurück an das Hauptprogramm. Anderenfalls wird der Wurf zur Summe addiert und eine Meldung über die neue Summe ausgegeben. Hiernach kommt wiederum der Gegenspieler zum Zug. Der verhält sich natürlich genauso. Bekommt die Coroutine die Kontrolle zurück, wird hinter der TRANSFER-Anweisung weitergemacht. Nun, dort ist die IF-Anweisung zu Ende, und durch die LOOP-Konstruktion gelangt die Ausführung wieder an den Anfang und zum Würfeln.

Die Umwandlung der Prozeduren in Coroutinen erfolgt wie besprochen. Zur Bereitstellung des Speicherbereichs benutzen wir Methode 1. Wir deklarieren Bereich1 und Bereich2 als ARRAY[1..200] OF WORD. Die Coroutinen-Namen sollen Spieler1 und Spieler2 lauten. Diese werden als Typ PROCESS deklariert (,genauso wie Haupt für das Hauptprogramm). Beim Aufruf von NEWPROCESS werden mittels ADR und SIZE die Startadresse und Größe der Speicherbereiche angegeben. Sodann übergibt das Hauptprogramm die Kontrolle an Spieler1. Es erhält sie später vom Gewinner zurück.

Bleibt lediglich zur Simulation des Würfels etwas zu sagen. Wir verwenden hier einen recht einfachen Zufallszahlengenerator. Bei jedem Aufruf von Wuerfel() wird die globale Variable Start gemäß der angegebenen Formel verändert und daraus die Augenzahl berechnet. Dazu ist es natürlich erforderlich, daß bereits ein Anfangswert für Start vorhanden ist. Dieser wird bei Beginn des Hauptprogramms abgefragt.

Abschließend kann man sagen, daß für derartige Simulationen das Coroutinen-Konzept sehr vorteilhaft ist. Denn jeder Spieler behält die relevanten Daten für sich. Die Übergabe der Kontrolle (sprich die Übergabe des Würfels) geht ohne Umweg über das Hauptprogramm (sprich einen Spielleiter) vonstatten. Zusätzlicher Verwaltungsaufwand mit globalen Variablen entfällt hier. Die Simulation ist somit realitätsnäher formuliert und daher auch leichter verständlich.


Literatur

  • Blaschek/Pomberger : Einführung in die Programmierung mit Modula-2 (Springer)
  • Bürgel : Modula-2 für PCs, Band 1 (IWT)
  • Dal Cin/Lutz/Risse : Programmierung in Modula-2 (Teubner)
  • Gleaves : Modula-2 für Pascal-Programmierer (Springer)
  • Richner : Modula-2 für PCs, Band 2 (IWT)
  • Rohlfing-Brosell : Modula-2 (Springer)
  • Sale : Modula-2 (Addison-Wesley)
  • Wirth : Programmieren in Modula-2 (Springer)

 

Übersicht Modula-2 Index