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)