Wer schon einmal in Modula-2 programmiert hat, hat vielleicht auch mit dem dort realisierten Konzept der Coroutinen Bekanntschaft gemacht. Es ermöglicht auf recht einfache Weise die übersichtliche Darstellung von (quasi-) parallelen Abläufen innerhalb eines Programmes.
Ein einfaches Beispiel für solche nebenläufigen Aktivitäten ist ein Drucker-Spooler. Weitergehende Möglichkeiten demonstrieren Umsetzungen von Simulationsmodellen mit Hilfe solcher Konkurrenten Programme.
Natürlich wird auch hier zu einem bestimmten Zeitpunkt immer nur eine einzige Anweisung einer Coroutine abgearbeitet. Der normale Programmablauf kann jedoch unterbrochen werden, und eine andere Coroutine erhält die Kontrolle. Diese Aktivierung geschieht durch den Aufruf der Prozedur Transfer. Das Verlassen einer Coroutine darf ebenfalls nur über Transfer erfolgen. Beim Erreichen des regulären Prozedurendes würde ein Fehler auftreten.
Doch wie erzeugt man eine Coroutine? Es handelt sich dabei in unserem konkreten Fall um eine ganz normale parameterlose C-Funktionen, die durch den Aufruf von NEWPROCESS (void (*func)(), int *local_stack, int stacksize, long *coroutinen_var) zur Coroutine erklärt wird, func ist dabei ein Zeiger auf die Funktion, die umgewandelt werden soll. Da nirgends im Programmtext explizit ein Funktionsaufruf er folgt, ergäbe es keinen Sinn, wenn die Funktion einen Wert liefern würde. Die beiden nächsten Parameter geben die Adresse und Größe eines Speicherbereichs an, der die lokalen Variablen und den Coroutinen-Beschreibungsblock aufnimmt. Als letztes wird ein Zeiger auf die Coroutinen-Variable übergeben.
Über eben diese Variable kann eine Coroutine identifiziert werden. Beim Kontext Wechsel mittels Transfer (long *from, long *to) geben die beiden Parameter zweckmäßigerweise die Adressen der entsprechenden Coroutinen Variablen an. Unter from wird der Zustand des gerade aktiven Prozesses gewissermaßen 'eingefroren', und es wird die Coroutine aktiviert, der die Variable an der Adresse to zugeordnet ist. Die beiden Argumente können identisch sein, da to gelesen und danach erst from gesetzt wird.
Um die Funktionsweise der beiden Funktionen zu verstehen, sind einige grundsätzliche Kenntnisse über den Stack nötig. Der Stack liegt adressenmäßig über dem Programm und wächst in Richtung der Adresse 0, also auf das Programm zu. Wenn im C-Programm ein Funktionsaufruf erfolgt, werden in umgekehrter Reihenfolge die Funktionsparameter auf dem Stack abgelegt, das heißt, der Stackpointer zeigt anschließend auf den ersten Übergabewert. Nachdem nun noch der Wert des Programmzählers als Rücksprungadresse auf den Stack gerettet wurde, erfolgt die eigentliche Verzweigung zum Unterprogramm (s. Abb. 1). Dort wird auf Assembler-Ebene ein LINK-Befehl ausgeführt, der unter anderem Platz für die lokalen Variablen schafft (s. Abb. 2). Das beim LINK angegebene Adreßregister, im allgemeinen handelt es sich um A6, wird auf den Stack gerettet und dient fortan als Basisregister zur Adressierung der Parameter und lokalen Daten.
Der vorletzte Befehl der Funktion ist dementsprechend ein UNLinK-Befehl, der dafür sorgt, daß der alte Inhalt des angesprochenen Registers wiederhergestellt und der Stackpointer korrigiert wird. An oberster Stelle liegt nun die Rückkehradresse auf dem Stack, so daß der abschließende RTS-Befehl einen Sprung zur nächsten Anweisung des aufrufenden Programmes ausführt, das nun fortgesetzt wird.
Wie sind die beiden Funktionen implementiert? Newprocess baut im Datenbereich einer Coroutine einen lokalen Stack auf, der zusätzlich die Werte der Register enthält, so daß ein Coroutinen-Beschreibungsblock entsteht. Zuerst einmal ist es nötig, den lokalen Stackpointer auf das Ende des Datenbereichs zu richten, weil der Stack, wie bereits angesprochen, nach ’unten’ wächst. Sämtliche Elemente des Stacks müssen an einer geraden Adresse liegen. Zuunterst liegt der Zeiger auf die Coroutine, der nichts weiter als die Einsprungadresse der Funktion ist, auf dem lokalen Stack. Es folgen der Inhalt des Registers A6 aus einem simuliertem Maschinenbefehl vom Format LINK A6,#0 und die Registerinhalte. Der lokale Stackpointer wird in der Coroutinen-Variable gemerkt.
Beim erstmaligen Antreffen von Transfer werden die aktuellen Registerinhalte auf den System-Stack kopiert (s. Abb. 3) und der Wert des Stackpointers in der Coroutinen-Variablen from gespeichert. Der Stackpointer wird auf den lokalen Stack umgebogen, von wo die durch Newprocess abgelegten Registerinhalte zurückgeholt werden. Die obligatorische UNLK A6-Anweisung sorgt dafür, daß nur noch die Adresse der neu zu startenden Coroutine auf dem neuen System-Stack liegt. Das abschließende RTS verzweigt zur Coroutine, die nun ihrerseits einen LINK-Befehl ausführt.
Wird in dieser Coroutine ein Transfer angetroffen, das zum Beispiel die Kontrolle an die aufrufende Funktion zurückgibt, werden Register und Stackpointer gerettet. Der System-Stackpointer wird geholt und die Inhalte der Register zurückgeladen. Der beim ersten Transfer-Aufruf ausgeführte LINK-Befehl wird 'rückgängig' gemacht, und die auf dem Stack liegende Rücksprungadresse dient als Ziel der RTS-Anweisung. Zurück in der aufrufenden Funktion, erfolgt implizit eine Stack-Korrektur, um die Parameter des ersten Transfer-Aufrufs vom Stack zu entfernen, und alles ist wieder beim alten.
Das vorliegende Beispiel gibt nur einen sehr kleinen Einblick in die Möglichkeiten, die sich mit konkurrenter Programmierung eröffnen. Es macht jedoch deutlich, wie einfach es ist, beim Anwender den Eindruck der ’Parallelverarbeitung' zu erzeugen.
Literatur
Mario Dal Cin, "Grundlagen der systemnahen Programmierung", Teubner
/* *\
colines.c - Demo für die
Verwendung von NEWPROCESS
und TRANSFER unter C
(c) MAXON Computer 1991
\* */
#include <stdio.h> /* Nur für 'calloc' */
#include <osbind.h>
#include <gembind.h>
#include "process.h"
#define MIN_Y 10 /* Arbeitsbereich: */
#define MAX_Y 390 /* Kann der aktuellen Bild- */
#define MIN_X 10 /* schirmauflösung angepaßt */
#define MAX_X 630 /* werden */
#define STACKSIZE 2048 /* Größe der lokalen Stacks */
#define STACK int /* Typ des Stacks */
#define MAXLINES 70 /* Anzahl der Linie in proc1 */
#define LINE_COOR 4 /* /2: Anzahl der Eckpunkte */
#define FIGR_COOR 8 /* in proc1 bzw. proc2 */
#define MIN_SPEED 2 /* Geschwindigkeit der */
#define MAX_SPEED 4 /* Objekte */
#define COOR(x) ((x&1)<<1) /* Einige Makros */
#define SIGN(x) (x<0 ? -1:1)
#define NEXT(x) ((x+1)%MAXLINES)
#define SPEED (Random()%MAX_SPEED+MIN_SPEED)
int contrl [12],
intin [128],
ptsin [128],
intout[128],
ptsout[128];
int handle,
ap_id;
long mainprocess, /* Coroutinen-Variablen */
process1,
process2;
int limits1[4] = {MIN_X, MAX_X, MIN_Y,
MAX_Y/2-1], /* Arbeitsbereich für */
limits2[4] = {MIN_X, MAX_X, MAX_Y/2+1,
MAX_Y}; /* proc1 bzw. proc2 */
proc1()
{
int lines[MAXLINES][LINE_COOR];
int vel [LINE_COOR]; /* Geschwindigkeitsvektoren */
register int act_line;
for (act_line=0; act_line<MAXLINES*LINE_COOR;
act_line++)*((int *)lines+act_line) = limits1[COOR(act_line)];
for (act_line=0; act_line<LINE_COOR; vel[act_line++]=-1);
act_line=0;
for (;;) /* Normales Prozedurende darf */
{ /* nicht erreicht werden */
check_line (lines[act_line],lines[NEXT(act_line)], vel);
act_line = NEXT(act_line);
v_pline (handle, LINE_COOR/2, lines[act_line]);
v_pline (handle, LINE_COOR/2, lines[NEXT(act_line)]);
if (act_line&1) /* Nach jedem 2.Durchlauf */
{ /* Kontrolle abgeben: */
if (Cconis()) /* Taste gedrückt? */
TRANSFER (&process1,(mainprocess);
/* => ENDE */
else /* sonst */
TRANSFER (&process1, &process2) ;
/* —> proc2 bearbeiten */
}
}
}
check_line (actl, newl, vel) /* Berechnet Koordinaten der */
int actl[], /* nächsten Linie */
newl[],
vel[];
{
register int i, j;
for (i=0; i<LINE_COOR; i++)
{
newl[i] = actl[i]+vel[i]; /* Neue Koordinate berechnen */
j = COOR(i); /* Index für min.x bzw. min.y */
if (newl[i]>limits1[j+1]) /* Neue Koordinate zu groß? */
{
newl[i] = limits1[j+1];
vel [i] = -SPEED;
continue;
}
if (newl[i]<limits1[j]) /* Neue Koordinate zu klein? */
{
newl[i] = limits1[j];
vel [i] = SPEED;
}
}
}
proc2()
{
int figure[FIGR_COOR+2]; /* Erster und letzter Punkt */
int vel [FIGR_COOR]; /* des Objekts sind identisch*/
register int i, j;
for (i=0; i<FIGR_COOR;figure[i]=limits2[COOR(i)], vel[i++]=1);
for (;;)
{
if (!(figure[0]%100 && figure[1]%100)) /* Ab und zu ... */
for (i=0; i<FIGR_COOR; i++) /* neue Geschwindigkeits- */
vel[i] = SPEED * SIGN(vel[i]); /* vektoren berechnen */
v_pline (handle, FIGR_COOR/2+1, figure);
for (i=0; i<FIGR_COOR; i++)
{
j = COOR(i);
figure[i] += vel[i];
if (figure[i]>limits2[j+1] || figure[i]<limits2[j])
figure[i] += vel[i] = -vel[i];
}
figure[FIGR_COOR] = figure[0];
figure[FIGR_COOR+1] = figure[1];
v_pline (handle, FIGR_COOR/2+1, figure);
TRANSFER (&process2, &process1); /* Kontrolle zurück an proc1 */
}
}
int *edge (lim) /* Ändern der Koordinaten- */
int lim[]; /* darstellung von */
{ /* (minx, maxx, miny, maxy) */
static int xyarray[4]; /* in */
/* (minx, miny, maxx, maxy) */
xyarray[0] = lim[0];
xyarray(1] = lim[2];
xyarray[2] = lim[1];
xyarray[3] = lim[3];
return (xyarray);
}
main ()
{
int work_in[11],
work_out[57],
pxyarray[4];
int i;
STACK stack1[STACKSIZE], /* Zwei Möglichkeiten Platz */
*stack2; /* für den lokalen Stack zu */
/* reservieren */
ap_id = appl_init();
handle = graf_handle (&i, &i, &i, &i);
for (i=0; i<10; work in[i++]=1);
work_in[10]=2;
v_opnvwk (workin, &handle, work_out);
graf_mouse (256);
v_clrwk (handle);
vswr_mode (handle, 3); /* VDI-Schreibmodus auf XOR */
pxyarray[0] = MIN_X;
pxyarray[1] = MIN_Y;
pxyarray[2] = MAX_X;
pxyarray[3] = MAX_Y;
vs_clip (handle, 1, pxyarray); /* Cipping auf Arbeitsbe- */
/* reich setzen */
vsf_interior (handle, 3); /* 'Box' für proc1 ... */
vsf_style (handle, 9);
v_rfbox (handle, edge(limits1));
vsf_interior (handle, 1); /* und proc2 zeichnen */
v_rfbox (handle, edge(limits2));
/* Coroutinen erzeugen */
NEWPROCESS (proc1, stack1, STACKSIZE, &process1);
stack2 = (STACK *)calloc (STACKSIZE, sizeof(STACK));
NEWPROCESS (proc2, stack2, STACKSIZE, &process2);
TRANSFER (&mainprocess, &process1); /* Kontrolle an proc1 geben */
v_clsvwk (handle);
graf_mouse (257);
appl_exit();
}
/* *\
process.h
\* */
extern NEWPROCESS();
extern TRANSFER();
/* *\
process.c - Modul stellt die
Funktionen NEWPROCESS und
TRANSFER bereit
(c) MAXON Computer 1991
\* */
#define ENSMEM -39 /* TOS-Fehlercode für Speicherplatzmangel */
#define MIN_STACKSIZE 256 /* minimale Größe des Stacks (in Bytes) */
/* Wandelt die ’func' in eine Coroutine um */
NEWPROCESS (func, loc_stack, stacksize, cor var)
long func; /* Adresse der Funktion */
long loc_stack; /* Zeiger auf den lokalen Coroutinen-Stack */
int stacksize; /* Größe des lokalen Stacks in Bytes */
long *cor_var; /* Zeiger auf die Coroutinen-Variable */
{
/* höchste gerade Adresse des lokalen Stacks bestimmen */
register long *stackadr = (long *)((loc_stack + (long)stacksize) & -2L),
*coradr = cor_var;
if (stacksize<MIN_STACKSIZE) /* lokaler Stack zu klein? */
exit (ENSMEM);
asm
{ /* Einsprung-Adresse der Coroutine */
move.l func(A6),-(stackadr) /* auf den lokalen Stack */
move.l A6,-(stackadr) /* link A6, #0 auf lokalem Stack */
movea.l stackadr, A6 /* simulieren */
movem.l D0-A6,-(stackadr) /* Register auf lokalen Stack */
movea.l 60(stackadr),A6 /* alten Wert von A6 holen */
move.l stackadr,(coradr) /* Wert des lokalen Stackpointers */
} /* in die Coroutinen-Variable */
}
/* übergibt 'to' die Kontrolle, aktueller */
/* Kontext wird in 'from' gemerkt */
TRANSFER (from to)
long from, /* Adresse der Coroutinen-Variablen, in die der */
to; /* aktuelle SP geschrieben bzw. */
{ /* a.d.d.neue SP gelesen wird */
asm
{ /* Register d.akt.Coroutine auf */
movem.l D0-A6,-(A7) /* ihren Stack retten */
movea.l to(A6),A0 /* Wert des neuen SP lesen, damit from und */
movea.l (A0),A0 /* to identisch sein können, ... */
movea.l from(A6),A1 /* dann den akt.SP in der Coroutinen-*/
move.l A7,(A1) /* Variablen merken und ... */
movea.l A0, A7 /* den neuen SP zum Aktuellen machen */
movem.l (A7)+, D0-A6 /* Register der nun aktuellen Coroutine */
(Anm.: Listing war im Heft nicht vollständig und wurde auch später nicht vervollständigt.)