next up previous
Next: Netzwerkdämonen Up: Parallelität und Synchronisierung Previous: Die Implementierung von Rendezvous

Kooperativität

Wenn Parallelität auf der Basis von Koroutinen erreicht wird, ist es notwendig, daß sich die einzelnen Koroutinen kooperativ zueinander verhalten. Da Koroutinen sich selbst nur freiwillig suspendieren können, würde eine Dauerschleife (oder auch eine sehr lang anhaltende Schleife) oder ein blockierender Systemaufruf alle anderen Koroutinen benachteiligen.

Natürlich wäre es möglich, ein Zeitscheibensystem einzuführen. In bestimmten Zeitabständen würde dann ein asynchrones Signal eintreffen und zum Wechsel der Koroutine führen. Dies ist jedoch vergleichsweise kostspielig und sorgt dafür, daß fast alle Koroutinen sich im unterbrochenen Zustand befinden. Dies verträgt sich leider nicht mit einer automatischen Speicherverwaltung, die die lebenden Objekte kopiert, da eine unterbrochene Koroutine ungleich schwerer zu analysieren ist als eine Koroutine, die normal suspendiert worden ist oder die Speicherverwaltung über NEW aufgerufen hat. Ein copying garbage collector muß alle existierenden Verweise genau kennen. Bei unterbrochenen Koroutinen wäre also nur ein konservativer Ansatz bei der Speicherverwaltung denkbar.[*]

Bei länger dauernden Berechnungen, wie z.B. bei diversen numerischen Algorithmen, würde es sich empfehlen, an geeigneten Stellen ein Tasks.Suspend einzufügen. Wenn keine anderen Tasks aktiv sind, ist der dadurch verursachte Zusatzaufwand außerordentlich gering. Im Rahmen der Ulmer Oberon-Bibliothek gibt es bislang keine Stelle, wo dies notwendig wäre.

Anders sieht es aus bei den Systemaufrufen, die blockieren könnten. Unter UNIX lassen sich viele blockierende Systemaufrufe entweder unterbrechen oder in nicht blockierender Form verwenden.[*] In der Ulmer Oberon-Bibliothek bieten die Sys-Module Systemaufrufe in der Originalform an, wobei - soweit notwendig - die Option zur Verfügung steht, den Systemaufruf bei Unterbrechungen automatisch wiederholen zu lassen.

Die Module, die die Schnittstellen zum Betriebssystem in Form allgemeiner Abstraktionen zur Verfügung stellen, sind dann dafür verantwortlich, daß es zu keinen unnötigen Blockierungen kommt. Zwar wäre es möglich, diese Aufgabe in die Anwendungsprogramme zu verlagern. Dies wäre allerdings weniger effizient und führt zu relativ umständlichem Programmtext. Bei der Ulmer Oberon-Bibliothek braucht man also nur auf Tasks.WaitFor zurückzugreifen, wenn man auf mehrere Ereignisse gleichzeitig oder auf ein internes Ereignis warten möchte.

Folgender Ausschnitt aus UnixFiles zeigt, wie dies in sehr effizienter Weise für Ein- und Ausgabe-Operationen geschieht:

PROCEDURE ReadBuf(s: Streams.Stream; buf: SysTypes.Address;
                  cnt: SysTypes.Count) : SysTypes.Count;
   VAR
      count: SysTypes.Count;
BEGIN
   WITH s: Stream DO
      IF s.blocking THEN
         Tasks.WaitFor(s.readCond);
      END;
      LOOP
         count := SysIO.Read(s.file, buf, cnt, s, s.retry, s.interrupted);
         IF (count >= 0) OR ~WouldBlock(s) THEN
            RETURN count
         END;
         Tasks.WaitFor(s.readCond);
      END;
   END;
END ReadBuf;

ReadBuf ist die Schnittstellenprozedur für addrread von Streams. Dateideskriptoren, die UnixFiles selbst eröffnet, werden in nicht-blockierenden Modus versetzt. Bei anderen Dateideskriptoren wird dies unterlassen, da es bei vererbten Dateideskriptoren zu Problemen beim vererbenden Prozeß führen kann.[*] Die Komponente s.blocking gibt somit an, ob der zugehörige Dateideskriptor sich im blockierenden Modus befindet. Im nicht-blockierenden Modus reagiert der Systemaufruf read mit einer Fehlerindikation, wenn die Eingabe noch nicht zur Verfügung steht.

Entsprechend wird beim nicht-blockierenden Modus sofort SysIO.Read abgesetzt. Wenn die Lese-Operation blockieren würde, kommt es zu einem Fehler. WouldBlock stellt fest, ob dies der Fall ist, und eliminiert ggf. das entsprechende Fehlerereignis (das aufrufende Module nur verwirren würde). Danach wird mit Tasks.WaitFor die Koroutine auf kooperative Weise blockiert, bis die Eingabe soweit ist.

Die beiden letzten Parameter von SysIO.Read beziehen sich auf Unterbrechungen. Im Normalfall ist s.retry wahr, und SysIO.Read startet dann die Operation sofort erneut, falls es zu einer Unterbrechung kam. In der Komponente s.interrupted wird notiert, ob eine Unterbrechung vorlag oder nicht.

Das Beispiel zeigt, daß für die Kooperativität hier kein Effizienzverlust hingenommen werden muß. Bei Lese-Operationen, die sich auf Dateien beziehen (und die weder unterbrechbar sind noch lange dauern), wird genau ein Systemaufruf abgesetzt.


next up previous
Next: Netzwerkdämonen Up: Parallelität und Synchronisierung Previous: Die Implementierung von Rendezvous
Andreas Borchert
2/2/1998