Skip to main content.

Navigation:

Betriebssysteme I

C++ und Koroutinen

Prinzip der Koroutinen

asymmetrisches Aufrufmodellsymmetrisches Aufrufmodell
ProzedurKoroutine
Prozedur Koroutine

Beispiel:

class Loop : public Coroutine
{
private:
    Coroutine* nextone;
    int my_id;
public:
    Loop (void *tos, int id) : Coroutine (tos), my_id (id) {}
    void action ();
    void remember (Coroutine* next) { nextone = next; }
};

void Loop::action ()
 {
   unsigned int i = 0;

   while (true)
    {
      i++;
      kout << "Coroutine " << my_id << ": " << i << endl;
      resume (*nextone);
    }
 }


unsigned char stack[2][1024];

int main ()
 {
   Loop a (stack[0] + 1024, 1);
   Loop b (stack[1] + 1024, 2);

   a.remember (&b);
   b.remember (&a);

   a.go ();
}

Ergebnis:
  Coroutine 1: 0
  Coroutine 2: 0
  Coroutine 1: 1
  Coroutine 2: 1
  Coroutine 1: 2
  Coroutine 2: 2
  Coroutine 1: 3
  Coroutine 2: 3
  ...
Wenn die Ausgaben nicht jeweils auf die nächste Zeile, sondern mit direkter Positionierung auf getrennte Bildschirmbereiche erfolgt, entsteht der Eindruck, daß die beiden Koroutinen gleichzeitig (parallel) ausgeführt werden.

Wozu Koroutinen?

Wenn ein Programm quasi mehrere Dinge gleichzeitig tun soll, kann dies mit Koroutinen häufig sehr viel einfacher und eleganter gelöst werden, als wenn nur ein einziger Kontrollfluß angegeben werden kann. Beispiel: Päckmännchen.

Realisierung

Prozeduren

        push [Par2]
        push [Par1]
        call proc
        ...

proc:   push ebp
        mov ebp,esp
        sub esp,12           ; Platz fuer lokale Variablen

        mov eax,[ebp+8]      ; Parameter 1
        mov ebx,[ebp+12]     ; Parameter 2

        mov [ebp-4], eax     ; lokale Variable 1
        mov [ebp-8], eax     ; lokale Variable 2

        ...
        ret
activation record:
...
...
Parameter 2
Parameter 1
Rücksprungadresse
Base-Pointer
lokale Variable 1
lokale Variable 2
lokale Variable 3
Bei geschachtelten oder rekursiven Prozeduraufrufen wird der Stack mit jedem Prozeduraufruf voller und erst beim Verlassen der Prozeduren wieder abgebaut.

Koroutinen

Problem:
Wenn Koroutinen wie Prozeduren aufgerufen würden, müßte der Stack bei jedem Aufruf wachsen und nie abgebaut werden. Ein Sprung mitten in eine Koroutine hinein ginge nicht, da die Rücksprungadresse von anderen Stackeinträgen verdeckt worden wäre.
Lösungsidee:
Verwirklichung:
Koroutinenwechsel: Coroutine::resume (Coroutine& next)
Speichern der nicht-flüchtigen Register (inklusive esp), Laden der nicht-flüchtigen Register des neuen Kontextes (inklusive esp), ret
Initialisierung: Coroutine::Coroutine (void* tos)
Nichtflüchtige Register (sofern eine Initialisierung sinnvoll ist) in den Laufzeitkontext eintragen, den esp auf den Stack verweisen lassen, auf den Stack die Anfangsadresse der Funktion schreiben, mit der die Ausführung beginnen soll (kickoff (Coroutine* object)). Damit kickoff (Coroutine* object) die action() Methode der Koroutine aufrufen kann, muß der this Zeiger der Koroutine als Parameter für kickoff (...) ebenfalls auf den Stack geschrieben werden.
erste Aktivierung der ersten Coroutine: Coroutine::go()
Stackpointer laden, ret