Konzept: Parallelität
Parallelität beschreibt die Tendenz, dass Dinge in einem System zur selben Zeit passieren. Bei der Behandlung der Parallelitätsproblematik in Softwaresystemen sind im Allgemeinen zwei Aspekte von Bedeutung: die Fähigkeit, externe Ereignisse, die in Zufallsreihenfolge auftreten, erkennen und auf diese zu reagieren zu können, und die Gewährleistung, dass auf diese Ereignisse innerhalb eines geforderten Zeitraums reagiert wird.
Beziehungen
Hauptbeschreibung
Anmerkung: Parallelität wird hier im Allgemeinen behandelt, weil sie in jedem System auftreten kann. Parallelität ist jedoch besonders wichtig in Systemen, die auf externe Ereignisse innerhalb enger Grenzen in Echtzeit reagieren müssen. Um den Bedarf solcher Systeme zu decken, stellt Rational Unified Process (RUP) echtzeitorientierte (reaktive) Systemerweiterungen zur Verfügung. Weitere Informationen zu diesem Thema finden Sie im Abschnitt Echtzeitsysteme.

Was ist Parallelität?

Parallelität beschreibt die Tendenz, dass Dinge in einem System zur selben Zeit passieren. Parallelität ist ein natürliches Phänomen. Im echten Leben gibt es zu jeder Zeit viele Dinge, die gleichzeitig passieren. Beim Design von Software für die Überwachung und Steuerung realistischer Systeme muss diese natürliche Parallelität berücksichtigt werden.

Bei der Behandlung der Parallelitätsproblematik in Softwaresystemen sind im Allgemeinen zwei Aspekte von Bedeutung: die Fähigkeit, externe Ereignisse, die in Zufallsreihenfolge auftreten, erkennen und auf diese zu reagieren zu können, und die Gewährleistung, dass auf diese Ereignisse innerhalb eines geforderten Zeitraums reagiert wird.

Wenn jede parallele Aktivität unabhängig, d. h. wirklich parallel auftreten würde, wäre dies relativ einfach: Man könnte einfach separate Programme für jede dieser Aktivitäten erstellen. Die Herausforderungen beim Design paralleler Systeme ergeben sich zum größten Teil aufgrund der Interaktionen zwischen parallelen Aktivitäten. Für die Interaktion zwischen parallelen Aktivitäten ist eine gewisse Koordination erforderlich.

Abbildung ist im Inhalt beschrieben.

Abbildung 1:  Beispiel für Parallelität in der Praxis: Bei parallelen Aktivitäten, die nicht interagieren, ist die Parallelitätsproblematik einfach. Die Parallelitätsproblematik kommt erst dann zum Tragen, wenn parallele Aktivitäten interagieren oder dieselben Ressourcen verwenden.

Der Straßenverkehr ist in diesem Zusammenhang eine hilfreiche Analogie. Parallele Verkehrsströme auf verschiedenen Straßen, die nur selten zusammentreffen, verursachen nur wenig Probleme. Parallele Verkehrsströme auf benachbarten Spuren erfordern für eine sichere Interaktion ein gewisses Maß an Koordination. Eine sorgfältige Koordination ist hingegen erforderlich, wenn Verkehrsströme an einer Kreuzung aufeinandertreffen.

Warum ist Parallelität so interessant?

Der Bedarf an Parallelität kommt häufig von außen, d. h. er wird einem System durch die Anforderungen der Umgebung auferlegt. In realistischen Systemen passieren viele Dinge gleichzeitig und müssen von der Software in Echtzeit behandelt werden. Zu diesem Zweck müssen viele echtzeitorientierte Softwaresysteme "reaktiv" sein. Sie müssen auf extern generierte Ereignisse reagieren, die zu einem zufälligen Zeitpunkt und/oder in zufälliger Reihenfolge auftreten.

Das Design eines konventionellen prozedurgesteuerten Programms für solche Situationen ist extrem komplex. Es kann sehr viel einfacher sein, das System in parallele Softwareelemente zu partitionieren, die sich jeweils mit diesen Ereignissen beschäftigen. Der Schlüsselausdruck ist hier "kann sein", da die Komplexität auch durch das Ausmaß der Interaktionen zwischen den Ereignissen beeinflusst wird.

Außerdem kann es interne Motivationen für Parallelität geben [LEA97]. Die parallele Ausführung von Aufgaben kann die Rechenarbeiten eines Systems erheblich beschleunigen, wenn mehrere CPUs verfügbar sind. Selbst in einem einzelnen Prozessor können Arbeiten durch Multitasking erheblich beschleunigt werden, indem beispielsweise verhindert wird, dass eine Aktivität beim Warten auf Eingabe/Ausgabe eine andere blockiert. Eine anschauliche Situation hierfür ist das Starten eines Systems. Beim Systemstart gibt es häufig viele Komponenten, die jeweils eine gewisse Zeit brauchen, um für den Betrieb vorbereitet zu werden. Wenn diese Operationen nacheinander ausgeführt werden, kann dies schmerzlich langsam sein.

Auch die Steuerbarkeit des Systems kann durch Parallelität verbessert werden. Beispielsweise kann eine Funktion mittendrin von anderen parallelen Funktionen gestartet, gestoppt oder anderweitig beeinflusst werden, eine Aufgabe, die ohne parallele Komponenten nur sehr schwer zu bewältigen ist.

Was ist an paralleler Software so schwierig?

Warum wird parallele Programmierung nicht überall eingesetzt, wenn sie doch so viele Vorteile hat?

Die meisten Computer und Programmiersprachen sind von Natur aus sequenziell. Eine Prozedur oder ein Prozessor führt eine Anweisung nach der anderen aus. In einem einzelnen sequenziellen Prozessor muss die Illusion von Parallelität erzeugt werden, indem die Ausführung der unterschiedlichen Aufgaben verzahnt wird. Die Schwierigkeiten liegen nicht so sehr in der Mechanik, sondern in der Festlegung, wann und wie Programmsegmente, die möglicherweise miteinander interagieren, verzahnt werden.

Obwohl die Verwirklichung von Parallelität mit mehreren Prozessoren einfach ist, werden die Interaktionen komplexer. Zuerst stellt sich die Frage der Kommunikation zwischen Aufgaben, die in unterschiedlichen Prozessoren ausgeführt werden. Normalerweise sind verschiedene Softwareschichten involviert, was die Komplexität und den Zeitaufwand erhöht. Determinismus ist in Systemen mit mehreren CPUs reduziert, da Taktgeber und Taktung voneinander abweichen können und Komponenten unabhängig voneinander fehlschlagen können.

Schlussendlich können parallele Systeme schwieriger zu verstehen sein, weil ihnen ein expliziter Systemzustand fehlt. Der Zustand eines parallelen Systems ergibt sich aus den Zuständen der Einzelkomponenten.

Beispiel für ein paralleles Echtzeitsystem: Aufzugsystem

Zur Veranschaulichung der Konzepte wird ein Aufzugsystem verwendet. Genauer gesagt, ist damit ein Computersystem gemeint, das eine Gruppe von Aufzügen eines Standorts in einem Gebäude steuert. Es ist klar, dass in einer Gruppe von Aufzügen viele Dinge gleichzeitig ablaufen müssen, oder es läuft gar nichts. Zu jedem beliebigen Zeitpunkt kann jemand auf einem Stockwerk einen Aufzug anfordern, und weitere Anforderungen können anstehen. Einige Aufzüge sind möglicherweise im Leerlauf, während andere Aufzüge Personen transportieren, einen Ruf beantworten oder beides. Türen müssen zur richtigen Zeit geöffnet und geschlossen werden. Personen können die Türen blockieren, Knöpfe zum Öffnen und Schließen der Türen drücken, das Stockwerk wählen und dann ihre Meinung wieder ändern. Anzeigen müssen aktualisiert werden, Motoren müssen angesteuert werden usw. All diese Aktivitäten werden unter der Überwachung des Aufzugsteuersystems ausgeführt. Schlussendlich ist es ein gutes Modell. um parallele Konzepte zu erforschen. Gleichzeitig ist allgemein weitverbreitetes Verständnis und Vokabular für diesen Bereich vorhanden.


Abbildung ist im Inhalt beschrieben.
Abbildung 2:   Ein Szenario mit zwei Aufzügen und fünf potenziellen Passagieren, die auf 11 Stockwerke verteilt sind.

Da potenzielle Passagiere zu unterschiedlichen Zeiten Anforderungen an das System stellen, versucht das System, einen insgesamt guten Service zu liefern, indem es basierend auf dem aktuellen Zustand und den vorhergesagten Antwortzeiten der einzelnen Aufzüge die Aufzüge auswählt, die am besten für die Beantwortung der Rufe geeignet sind. Beispiel: Der erste potenzielle Passagier, Andy, ruft einen Aufzug, um nach unten zu fahren. Beide Aufzüge sind im Leerlauf. Deshalb antwortet der Aufzug, der Andy am nächsten ist, nämlich Aufzug 2, obwohl er erst nach oben fahren muss, um Andy abzuholen. Wenn wenige Momente später der zweite potenzielle Passagier, Bob, einen Aufzug anfordert, um nach oben zu fahren, antwortet der weiter entfernte Aufzug 1, da bekannt ist, dass Aufzug 2 nach unten zu einem noch unbekannten Ziel fahren muss, bevor er von dort aus wieder einen Ruf zum Hochfahren beantworten kann.

Parallelität als vereinfachende Strategie

Wenn das Aufzugsystem nur einen Aufzug hätte und dieser Aufzug jeweils nur einen Passagier bedienen müsste, wäre man versucht zu denken, man könne diese Situation mit einem herkömmlichen sequenziellen Programm bewältigen. Selbst für diesen "einfachen" Fall müsste das Programm viele Verzweigungen enthalten, um die unterschiedlichen Bedingungen unterzubringen. Wenn der Passagier beispielsweise niemals einsteigt und ein Stockwerk auswählt, sollte man die Möglichkeit haben, den Aufzug zurückzusetzen, damit er einen anderen Ruf beantworten kann.

Die ganz normale Anforderung, Aufrufe mehrerer potenzieller Passagiere und Anforderungen mehrerer Passagiere zu bearbeiten, veranschaulicht die Motivationen für Parallelität, die zuvor beschrieben wurden. Da die potenziellen Passagiere eigene parallele Leben haben, stellen sie zu nicht vorhersehbaren Zeiten Anforderungen an den Aufzug, unabhängig davon, welchen Zustand der Aufzug hat. Es ist äußerst schwierig, ein sequenzielles Programm zu entwerfen, das auf alle diese externen Ereignisse reagieren kann und gleichzeitig den Aufzug auf der Basis vorheriger Entscheidungen weiter steuert.

Parallelität abstrahieren

Um parallele Systeme effektiv entwerfen zu können, müssen Sie in der Lage sein, sich über die Rolle der Parallelität im System klar zu werden. Zu diesem Zweck ist eine Abstraktion der Parallelität selbst erforderlich.

Die grundlegenden Bausteine paralleler Systeme sind "Aktivitäten", die mehr oder weniger unabhängig voneinander ausgeführt werden. Eine hilfreiche grafische Abstraktion, um über solche Aktivitäten nachzudenken, sind Buhrs "Timethreads". [BUH96] Tatsächlich wird im Aufzugszenario in Abbildung 3 eine Form dieser Timethreads verwendet. Jede Aktivität wird mit einer Linie dargestellt, deren Ablauf dieser Linie folgt. Die großen Punkte repräsentieren Stellen, an denen eine Aktivität beginnt oder darauf wartet, dass ein Ereignis eintritt, bevor sie fortgesetzt wird. Eine Aktivität kann die Fortsetzung einer anderen Aktivität anstoßen, was in der Timethread-Notation so dargestellt wird, dass die Wartestelle eines anderen Timethread berührt wird.

Abbildung ist im Inhalt beschrieben.

Abbildung 3:   Visualisierung der Ausführungs-Threads

Die Grundbausteine von Software sind Prozeduren und Datenstrukturen, aber diese allein reichen für die logische Erfassung von Parallelität nicht aus. Während ein Prozessor eine Prozedur ausführt, folgt er einen bestimmten Pfad, der sich nach den aktuellen Bedingungen richtet. Dieser Pfad kann als "Ausführungs-Thread" oder "Steuer-Thread" bezeichnet werden. Dieser Steuer-Thread kann verschiedene Verzweigungen oder Schleifen nehmen, je nach den Bindungen, die zu der jeweiligen Zeit herrschen. In Echtzeitsystemen kann der Steuer-Thread für eine bestimmte Zeit angehalten und nach einer geplanten Wartezeit fortgesetzt werden.

Aus der Sicht des Programmdesigners wird der Steuer-Thread von der Logik im Programm kontrolliert und vom Betriebssystem zeitlich eingereiht. Wenn der Softwaredesigner festlegt, dass eine Prozedur andere aufruft, springt der Ausführungs-Thread von einer Prozedur zu anderen und bei einer Rückkehranweisung wieder zurück, um die Verarbeitung dort fortzusetzen, wo er die ursprüngliche Prozedur verlassen hat.

Aus der Sicht der CPU gibt es nur einen Ausführungs-Thread, der sich durch die Software schlängelt und der durch kurze separate Threads ergänzt wird, die als Reaktion auf Hardwareunterbrechungen ausgeführt werden. Da sich alles andere auf dieses Modell stützt, ist es von entscheidender Bedeutung, dass die Designer sich mit diesem Modell auskennen. Die Designer von Echtzeitsystemen müssen im Vergleich mit Designern anderer Typen von Software ein sehr detailliertes Wissen über die Funktionsweise eines Systems besitzen. Dieses Modell ist jedoch auf solch niedriger Abstraktionsebene, dass Parallelität nur sehr grob erfasst werden kann, nämlich nur die CPU. Für das Design komplexer Systeme ist es hilfreich, auf verschiedenen Abstraktionsebenen arbeiten zu können. Abstraktion ist natürlich das Erstellen einer Sicht oder eines Modells, die bzw. das unnötige Details unterdrückt, so dass man sich auf die wichtigen Aspekte des vorliegenden Problems konzentrieren kann.

Um eine Abstraktionsstufe höher zu gehen, denken wir bei Software gewöhnlich in Schichten. Die grundlegende Schicht bildet das Betriebssystem, das zwischen der Hardware und der Anwendungssoftware angesiedelt ist. Das Betriebssystem stellt der Anwendung hardwarebasierte Services wie Hauptspeicher, Taktung und E/A bereit, abstrahiert aber die CPU zu einer virtuellen Maschine, die von der eigentlichen Hardwarekonfiguration unabhängig ist.

Parallelität realisieren: Mechanismen

Steuer-Threads verwalten

Für die Parallelität muss ein System mehrere Steuer-Threads unterstützen. Die Abstraktion eines Steuer-Thread kann von Hardware und Software auf verschiedene Arten implementiert werden. Die gebräuchlichsten Mechanismen sind Varianten einer der folgenden Referenzen ([DEI84], [TAN86]):

  • Mehrprozessorbetrieb - Es werden mehrere CPUs gleichzeitig betrieben.
  • Multitasking - Das Betriebssystem simuliert Parallelität in einer CPU durch
    Verzahnung der unterschiedlichen ausgeführten Aufgaben.
  • Anwendungsbasierte Lösungen - Die Anwendungssoftware übernimmt die Verantwortung für
    das Umschalten zwischen den verschiedenen Codezweigen zu den entsprechenden Zeiten.

Multitasking

Bei einem Betriebssystem, das Multitasking beherrscht, bildet der Prozess die Parallelitätseinheit. Ein Prozess ist eine Entität, die vom Betriebssystem bereitgestellt, unterstützt und verwaltet wird und deren einziger Zweck es ist, eine Umgebung zur Verfügung zu stellen, in der ein Programm ausgeführt wird. Der Prozess stellt einen Speicherbereich zur exklusiven Verwendung seines Anwendungsprogramms, einen Ausführungs-Thread für die Ausführung des Programms und unter Umständen verschiedene Mittel bereit, mit denen Nachrichten an andere Prozesse gesendet und von diesem empfangen werden können. Der Prozess ist im Wesentlichen eine virtuelle CPU für die Ausführung eines parallelen Teils einer Anwendung.

Jeder Prozess kann drei Status annehmen:

  • blockiert - Der Prozess wartet auf den Empfang einer Eingabe oder auf die Übernahme der Kontrolle einer Ressource.
  • bereit - Der Prozess wartet darauf, dass das Betriebssystem ihm das OK für die Ausführung gibt.
  • aktiv - Der Prozess verwendet die CPU.

Prozessen werden häufig auch relative Prioritäten zugeordnet. Der Betriebssystem-Kernel bestimmt basierend auf Zustand, Prioritäten und Planungsrichtlinien, welcher Prozess zu einer bestimmten Zeit ausgeführt wird. Multitasking-Betriebssysteme teilen sich einen einzelnen Steuer-Thread für alle ihre Prozesse.

Anmerkung: Die Begriffe 'Aufgabe' (engl. Task) und 'Prozess' werden häufig synonym verwendet. Leider wird der Begriff 'Multitasking' in generellem Sinne verwendet, um die Fähigkeit zu bezeichnen, mehrere Prozesse gleichzeitig ausführen zu können, wohingegen 'Mehrprozessorbetrieb' auf ein System mit mehreren Prozessoren (CPUs) verweist. In RUP wird diese Konvention eingehalten, weil sie am weitesten verbreitet ist. Allerdings wird in RUP der Begriff 'Aufgabe' nur selten verwendet und dann auch nur, um eine feine Unterscheidung zwischen der ausgeführten Arbeitseinheit (der Aufgabe) und der Entität, die die Ressourcen und die Umgebung für diese Aufgabe bereitstellt (der Prozess), zu treffen.

Wie bereits erwähnt, gibt es aus der Sicht der CPU nur einen Ausführungs-Thread. Ebenso wie ein Anwendungsprogramm durch den Aufruf von Unterroutinen von einer Prozedur zur anderen springen kann, kann das Betriebssystem bei einer Unterbrechung, beim Abschluss einer Prozedur oder bei einem anderen Ereignis die Steuerung von einem Prozess an einen anderen übergeben. Aufgrund des Speicherschutzes, den ein Prozess leistet, kann dieses "Aufgabenumschaltung" einen erheblichen Systemaufwand mit sich bringen. Weil die Planungsrichtlinien und Prozesszustände wenig mit dem Blickpunkt der Anwendung zu tun haben, ist die Verzahnung von Prozessen in der Regel zu wenig abstrakt, um über die Art der Parallelität nachzudenken, die für die Anwendung von Bedeutung ist.

Um Parallelität wirklich zu verstehen, ist eine klare Abgrenzung zwischen dem Konzept von Ausführungspfaden und dem Konzept von Aufgabenwechseln (Task-Switching) vorzunehmen. Sie können sich vorstellen, dass jeder Prozess einen eigenen Ausführungspfad darstellt. Wenn das Betriebssystem zwischen Prozessen umschaltet, wird ein Ausführungspfad vorübergehend unterbrochen und ein anderer gestartet oder an der Stelle fortgesetzt, wo er zuvor unterbrochen wurde.

Multithreading

Viele Betriebssysteme, insbesondere solche, die für Echtzeitanwendungen verwendet werden, bieten eine "weniger gewichtige" Alternative zu Prozessen, die "Threads" oder "Lightweight-Threads".

Threads sind eine Möglichkeit, eine geringfügig feinere Granularität der Parallelität in einem Prozess zu erreichen. Jeder Thread gehört zu einem einzelnen Prozess, und alle Threads in einem Prozess verwenden denselben Hauptspeicherbereich und andere Ressourcen, die von diesem Prozess gesteuert werden.

Normalerweise wird jedem Thread eine auszuführende Prozedur zugeordnet.

Anmerkung: Es ist schade, dass der Begriff "Thread" so überladen ist. Wenn das Wort 'Thread' wie hier eigenständig verwendet wird, bezeichnet es einen 'physischen Thread', der vom Betriebssystem bereitgestellt und verwaltet wird. Mit den Wörtern 'Ausführungs-Thread', 'Steuer-Thread' und 'Timethread', die in der vorherigen Beschreibung verwendet wurde, ist eine Abstraktion gemeint, die nicht unbedingt etwas mit einem physischen Thread zu tun haben muss.

Mehrprozessorbetrieb

Natürlich bieten mehrere Prozessoren die Möglichkeit einer echten parallelen Ausführung. Meistens werden die Aufgaben einem Prozess in einem bestimmten Prozessor permanent zugeordnet. Unter bestimmten Umständen jedoch können Aufgaben dem nächsten verfügbaren Prozessor zugewiesen werden. Die zugänglichste Methode hierfür ist vielleicht die Verwendung eines "symmetrischen Multiprozessors". In einer solchen Hardwarekonfiguration können mehrere CPUs über einen gemeinsamen Bus auf den Hauptspeicher zugreifen.

Betriebssysteme, die symmetrische Multiprozessoren unterstützen, können Threads dynamisch jeder verfügbaren CPU zuordnen. Beispiele für Betriebssysteme, die symmetrische Multiprozessoren unterstützen, sind SUN Solaris und Microsoft Windows NT.

Grundlegende Aspekte zu paralleler Software

Zuvor haben wir die scheinbar paradoxe Aussage getroffen, dass Parallelität die Komplexität von Software sowohl steigert als auch verringert. Parallele Software vereinfacht die Lösung komplexer Probleme hauptsächlich dadurch, dass sie eine "Verteilung der Problemstellungen" auf parallele Aktivitäten ermöglicht. In dieser Hinsicht ist Parallelität nichts anderes als ein weiteres Werkzeug, mit dem die Modularität der Software erhöht werden kann. Wenn ein System vorrangig voneinander unabhängige Aktivitäten ausführen oder auf vorrangig voneinander unabhängige Ereignisse reagieren muss, vereinfacht die Zuordnung dieser Aktivitäten zu einzelnen parallelen Komponenten natürlich das Design.

Die zusätzliche Komplexität von paralleler Software entsteht fast ausschließlich aus den Situationen, in denen diese parallelen Aktivitäten nahezu, aber nicht ganz unabhängig sind. Anders ausgedrückt, Komplexität entsteht aus Interaktionen. Interaktionen zwischen asynchronen Aktivitäten beinhalten praktisch immer den Austausch irgendeiner Form von Signalen oder Informationen. Aus den Interaktionen zwischen parallelen Steuer-Threads entsteht eine Reihe von Problemen, die in allen parallelen Systeme gleich sind und die behoben werden müssen, um sicherzustellen, dass sich ein System ordnungsgemäß verhält.

Asynchrone und synchrone Interaktionen

Obwohl es viele unterschiedliche Realisierungen von Mechanismen für die Kommunikation zwischen Prozessen bzw. die Kommunikation zwischen Threads gibt, können diese letztendlich in zwei Kategorien unterteilt werden:

Bei der asynchronen Kommunikation leitet die sendende Aktivität ihre Informationen weiter, unabhängig davon, ob der Empfänger für den Empfang dieser Informationen bereit ist oder nicht. Nachdem die Informationen auf den Weg geschickt wurden, fährt der Sender mit seinem nächsten Schritt fort. Falls der Empfänger für den Empfang der Informationen nicht bereit ist, werden die Informationen in eine Warteschlange gestellt, aus der sie der Empfänger später abrufen kann. Sender und Empfänger arbeiten asynchron voneinander und kennen deshalb den Zustand des jeweils anderen nicht. Die asynchrone Kommunikation wird auch als Nachrichtenübergabe bezeichnet.

Synchrone Kommunikation umfasst neben dem Austausch von Informationen die Synchronisation von Sender und Empfänger. Während des Informationsaustauschs verschmelzen die beiden parallelen Aktivitäten miteinander und führen tatsächlich ein gemeinsames Codesegment aus. Wenn die Kommunikation beendet ist, trennen sich die beiden Aktivitäten wieder. In diesem Zeitraum sind die beiden Aktivitäten also miteinander synchron und immun gegen Parallelitätskonflikte untereinander. Wenn eine Aktivität (Sender oder Empfänger) vor der anderen für die Kommunikation bereit ist, wird diese Aktivität so lange ausgesetzt, bis die andere ebenfalls bereit ist. Aus diesem Grund wird dieser Kommunikationsmodus manchmal auch als Rendezvous bezeichnet.

Bei der synchronen Kommunikation gibt es ein potenzielles Problem: Während eine Aktivität auf die Bereitschaftsmeldung ihres Partners wartet, kann sie nicht auf andere Ereignisse reagieren. In vielen Echtzeitsystemen ist dies in manchen Situationen nicht akzeptabel, weil dann nicht mehr garantiert werden kann, dass rechtzeitig auf eine wichtige Situation reagiert wird. Ein weiterer Nachteil ist die Neigung zu Deadlocks. Ein Deadlock tritt auf, wenn mindestens zwei Aktivitäten sich in einem Teufelskreis befinden und aufeinander warten.

Wenn Interaktionen zwischen parallelen Aktivitäten erforderlich sind, muss der Designer zwischen synchronem und asynchronem Stil wählen. Mit synchron ist gemeint, dass mindestens zwei parallele Steuer-Threads zu einem bestimmten Zeitpunkt ein Rendezvous haben müssen. Dies bedeutet in der Regel, dass ein Steuer-Thread auf einen anderen warten muss, um eine Anforderung zu beantworten. Die einfachste und gebräuchlichste Form synchroner Interaktion ist, wenn die parallele Aktivität A Informationen von der parallelen Aktivität B benötigt, um mit ihrer eigentlichen Arbeit fortfahren zu können.

Synchrone Interaktionen sind natürlich die Norm für nicht parallele Softwarekomponenten. Gewöhnliche Prozeduraufrufe sind ein Hauptbeispiel für eine synchrone Interaktion: Wenn eine Prozedur eine andere aufruft, überträgt der Aufrufende die Steuerung sofort an die aufgerufene Prozedur und "wartet" in der Tat darauf, dass sie die Steuerung zurück erhält. In der parallelen Welt ist jedoch ein zusätzlicher Apparat erforderlich, um die ansonsten unabhängigen Steuer-Threads zu synchronisieren.

Asynchrone Interaktionen erfordern kein Rendezvous, dafür aber einen zusätzlichen Apparat, der die Kommunikation zwischen zwei Steuer-Threads unterstützt. Häufig besteht dieser Apparat aus Kommunikationskanälen mit Nachrichtenwarteschlangen, so dass Nachrichten asynchron gesendet und empfangen werden können.

Beachten Sie, dass in einer Anwendung synchrone und asynchrone Kommunikation gemischt verwendet werden können, je nachdem, ob die Anwendung auf eine Antwort warten muss oder andere Arbeiten ausführen kann, während der Nachrichtenempfänger die Nachricht verarbeitet.

Denken Sie daran, dass eine echte Parallelität von Prozessen und Threads nur in Multiprozessoren mit gleichzeitiger Ausführung von Prozessen bzw. Threads möglich ist. In einem Einzelprozessor wird die Illusion simultaner Ausführung von Threads und Prozessen vom Scheduler des Betriebssystems erzeugt, der die verfügbaren Verarbeitungsressourcen in kleine Blöcke aufteilt, damit es so scheint, als würden mehrere Threads oder Prozesse gleichzeitig ausgeführt. Ein schlechtes Design macht die Vorteile dieses Zeitscheibenverfahrens zunichte, indem es mehrere Prozesse oder Threads erstellt, die häufig und synchron miteinander kommunizieren, und dadurch bewirkt, dass Prozesse oder Threads ihre "Zeitscheibe" vergeuden und die meiste Zeit blockiert sind, während sie auf eine Antwort von einem anderen Prozess oder Thread warten.

Konkurrenzsituationen um gemeinsam genutzte Ressourcen

Parallele Aktivitäten sind möglicherweise auf knappe Ressourcen angewiesen, die sie sich untereinander teilen müssen. Typische Beispiele sind E/A-Einheiten. Wenn eine Aktivität eine Ressource benötigt, die gerade von einer anderen Aktivität verwendet wird, muss sie warten, bis sie an der Reihe ist.

Wettbewerbssituationen: Problematik des konsistenten Zustands

Das möglicherweise grundlegendste Problem beim Design paralleler Systeme ist die Vermeidung von "Wettbewerbssituationen". Wenn ein Teil eines Systems zustandsabhängige Funktionen (d. h. Funktionen, deren Ergebnisse vom aktuellen Zustand des Systems abhängen) ausführen muss, muss sichergestellt sein, dass dieser Zustand während der gesamten Operation stabil bleibt. Anders ausgedrückt, bestimmte Operationen müssen "atomar" sein. Wenn zwei oder mehr Steuer-Threads Zugriff auf dieselben Zustandsinformationen haben, ist eine Form der "Parallelitätssteuerung" erforderlich, um sicherzustellen, dass ein Thread den Zustand nicht ändert, während der andere eine atomare zustandsabhängige Operation durchführt. Simultane Versuche, auf dieselben Zustandsinformationen zuzugreifen, die zu einem inkonsistenten internen Zustand führen können, werden als "Wettbewerbssituationen" bezeichnet.

Eine Wettbewerbssituation lässt sich beispielhaft an unserem Aufzugsystem verdeutlichen, wenn ein Passagier ein Stockwerk auswählt. Der Aufzug arbeitet mit Listen, in denen die Stockwerke aufgeführt sind, in denen beim Hoch- bzw. Herunterfahren angehalten werden muss. Wenn der Aufzug in einem Stockwerk ankommt, entfernt ein Steuer-Thread dieses Stockwerk von der entsprechenden Liste und ruft das nächste Ziel aus der Liste ab. Wenn die Liste leer ist, ändert der Aufzug entweder die Fahrtrichtung, sofern die andere Liste Stockwerke enthält. Andernfalls wechselt er in den Leerlauf. Ein anderer Steuer-Thread ist dafür verantwortlich, die Stockwerksanforderungen in die entsprechende Liste zu stellen, wenn die Passagiere ihre Stockwerke auswählen. Jeder Thread führt Kombinationen von Operationen in der Liste aus, die eigentlich nicht atomar sind, z. B. den nächsten freien Platz ermitteln und diesen dann belegen. Wenn die Threads ihre Operationen verzahnen, kann es leicht vorkommen, dass sie denselben Platz in der Liste überschreiben.

Deadlock

Ein Deadlock bezeichnet einen Zustand, in dem sich zwei Threads blockieren und beide darauf warten, dass der jeweils andere eine Aktion ausführt. Ironischerweise entstehen Deadlocks häufig, wenn ein Synchronisationsmechanismus zur Vermeidung von Wettbewerbssituationen angewendet wird.

Die anhand des Aufzugbeispiels veranschaulichte Wettbewerbssituation könnte relativ leicht zu einem Deadlock führen. Der Steuer-Thread für den Aufzug denkt, die Liste ist leer und fährt deshalb kein anderes Stockwerk mehr an. Der Thread für die Stockwerksanforderungen denkt, dass der Aufzug mit der Abarbeitung der Liste beschäftigt ist, und sieht deshalb keine Veranlassung, den Aufzug aufzufordern, seinen Leerlaufzustand zu verlassen.

Andere Probleme in der Praxis

Neben den grundsätzlichen Problemen gibt es verschiedene Umsetzungsprobleme, an die beim Design paralleler Software gedacht werden muss.

Leistungsabwägungen

Die Mechanismen, die innerhalb einer einzelnen CPU Parallelität simulieren, indem sie zwischen Aufgaben hin- und herschalten, benötigen für ihre Arbeit CPU-Zyklen, die sonst der Anwendung zu Gute kommen könnten. Wenn Software beispielsweise auf E/A-Einheiten warten muss, macht auf der anderen Seite die Leistungssteigerung, die durch Parallelität erzielt werden kann, diesen Nachteil mehr als wett.

Komplexitätsabwägungen

Parallele Software benötigt Steuer- und Koordinationsmechanismen, die in sequenzieller Software nicht benötigt werden. Dadurch wird parallele Software komplexer, und das Risiko von Programmierfehlern erhöht sich. Probleme in parallelen Systemen sind natürlich schwieriger zu diagnostizieren, weil es mehrere Steuer-Threads gibt. Wenn die externen Einflüsse selbst parallel sind, kann andererseits, wie bereits gezeigt, parallele Software, die unterschiedliche Ereignisse voneinander unabhängig verarbeitet, weit einfacher als sequenzielle Software zu erstellen sein, die die in willkürlicher Reihenfolge auftretende Ereignisse verarbeiten muss.

Indeterminismus

Da viele Faktoren Einfluss auf die Ausführungsverzahnung paralleler Systeme haben, könnte dieselbe Software in unterschiedlicher Reihenfolge auf dieselbe Folge von Ereignissen reagieren. Solche Änderungen der Reihenfolge könnten abhängig vom Design unterschiedliche Ergebnisse liefern.

Die Rolle von Anwendungssoftware bei der Steuerung von Parallelität

Anwendungssoftware kann möglicherweise bei der Implementierung der Steuerung von Parallelität involviert sein. Es gibt ein weitreichendes Spektrum von Möglichkeiten, sie mit einzubeziehen. Nachfolgend werden die Möglichkeiten bezüglich des Beteiligungsgrades aufsteigend aufgeführt:

  1. Anwendungsaufgaben können jederzeit durch das Betriebssystem unterbrochen werden (präemptives Multitasking).
  2. Anwendungsaufgaben können atomare Abarbeitungseinheiten definieren (kritische Abschnitte), die nicht unterrochen werden dürfen, und bei deren Eintritt bzw. Verlassen das Betriebssystem benachrichtigt wird.
  3. Anwendungsaufgaben können die CPU freiwillig freigeben und anderen Aufgaben zur Verfügung stellen (kooperatives Multitasking).
  4. Anwendungssoftware kann die vollständige Kontrolle für die Ausführungssteuerung und -planung anderer Aufgaben übernehmen.

Diese Möglichkeiten sind weder vollständig noch schließen sie sich gegenseitig aus. In einem echten System können durchaus Kombinationen dieser Möglichkeiten auftreten.

Parallelität abstrahieren

Ein weitverbreiteter Fehler beim Design von parallelen Systemen besteht darin, zu früh im Designprozess die spezifischen Mechanismen festzulegen, die für die Parallelität verwendet werden sollen. Jeder Mechanismus bringt eine Reihe von Vor- und Nachteilen mit sich und die Auswahl des "besten" Mechanismus kann oftmals nur durch sorgfältiges Abwägen und eingehen von Kompromissen getroffen werden. Je früher der Mechanismus ausgewählt wird, desto weniger Informationen hat man darüber, auf welcher Basis die Auswahl getroffen wurde. Den Mechanismus fest zu verankern reduziert tendenziell die Flexibilität und die Fähigkeit, das Design an verschiedene Situationen anzupassen.

Wie bei den meisten komplexen Designaufgaben wird Parallelität am besten durch Einführung mehrerer Abstraktionsebenen verstanden. Zuerst müssen die funktionalen Anforderungen bezüglich des erwarteten Verhaltens vollständig erforscht werden. Als nächstes müssen die möglichen Rollen der Parallelität erforscht werden. Dies wird am besten erreicht, wenn die Threads abstrahiert werden, ohne sich auf eine bestimmte Implementierung festzulegen. Um das maximal Mögliche zu erreichen, sollte die abschließende Festlegung auf die Mechanismen zur Implementierung der Parallelität offen bleiben, damit eine Optimierung der Leistung möglich ist und die Flexibilität erhalten bleibt, Komponenten unterschiedlich für verschiedene Produktkonfigurationen auszuliefern.

Eine der größten Schwierigkeiten im Systemdesign ist der konzeptionelle Unterschied zwischen dem Problemraum (Aufzugsteuerung) und dem Lösungsraum (Softwarestrukturen). "Visuelle Formalismen" sind für das Verständnis und die Kommunikation komplexer Ideen wie paralleles Verhalten extrem hilfreich und überbrücken in der Tat diese "konzeptionelle Lücke". Zu den Werkzeugen, die sich für diesen Problembereich als hilfreich erwiesen haben, gehören unter anderem folgende:

  • Moduldiagramme, die parallel agierende Komponenten veranschaulichen.
  • Timethreads, die parallele und interaktive Aktivitäten (die orthogonal zu den Komponenten sein können) veranschaulichen.
  • Sequenzdiagramme, die Interaktionen zwischen Komponenten darstellen.
  • Statusdiagramme, um die Status sowie statusabhängiges Verhalten der Komponenten festzulegen.

Objekte als parallele Komponenten

Um ein paralleles Softwaresystem entwerfen zu können, müssen wir die Bausteine der Software (Prozeduren und Datenstrukturen) mit den Bausteinen der Parallelität (Steuer-Threads) verbinden. Wir haben zwar das Konzept einer parallelen Aktivität besprochen, aber niemand kann ein System aus Aktivitäten bauen. Man setzt ein System aus Komponenten zusammen und deshalb liegt es nahe, ein paralleles System aus parallelen Komponenten zusammenzusetzen. Weder Komponenten noch Datenstrukturen noch Steuer-Threads bilden an sich ein natürliches Modell für parallele Komponenten. Demgegenüber scheinen Objekte eine sehr natürliche Möglichkeit darzustellen, all diese notwendigen Elemente in ein geordnetes Paket zu packen.

Ein Objekt verbindet Prozeduren und Datenstrukturen zu einer geschlossenen Komponente mit eigenem Zustand und Verhalten. Es kapselt die jeweilige Implementierung dieses Zustands und Verhaltens und bietet eine Schnittstelle, mit der andere Objekte oder Programme interagieren können. Im Allgemeinen bilden Objekte Entitäten oder Konzepte der wahren Welt ab und interagieren durch den Austausch von Nachrichten mit andern Objekten. Mittlerweile sind sie allgemein als beste Möglichkeit anerkannt, komplexe Systeme aufzubauen.

Abbildung ist im Inhalt beschrieben.

Abbildung 4:   Ein einfaches Objektmodell für das Aufzugssystem


Stellen Sie sich ein Objektmodell für das Aufzugsystem vor. Ein Rufstationsobjekt in jedem Stockwerk überwacht die Aufwärts- und Abwärtstasten dieses Stockwerks. Wenn ein potenzieller Passagier eine Taste drückt, sendet das Rufstationsobjekt eine Nachricht an das Aufzugverteilerobjekt, das den Aufzug auswählt, der den Auftrag wahrscheinlich am schnellsten bedienen kann, teilt den Aufzug zu und bestätigt den Aufruf. Jedes Aufzugobjekt steuert parallel und unabhängig seinen physischen Aufzug und reagiert auf die Stockwerksauswahl von Passagieren und Aufrufe des Verteilers.

In einem solchen Objektmodell kann die Parallelität zwei Formen annehmen. Eine Parallelität zwischen Objekten entsteht, wenn mindestens zwei Objekte Aktivitäten unabhängig voneinander über gesonderte Steuer-Threads ausführen. Eine objektinterne Parallelität entsteht, wenn mehrere Steuer-Threads in einem einzigen Objekt aktiv sind. In den meisten heutigen objektorientierten Sprachen sind Objekte "passiv" und haben keinen eigenen Steuer-Thread. Die Steuer-Threads müssen von einer externen Umgebung bereitgestellt werden. In den meisten Fällen ist die Umgebung ein Standardbetriebssystemprozess, der für die Ausführung eines objektorientierten "Programms" erstellt wird und in einer Sprache wie C++ oder Smalltalk geschrieben ist. Wenn das Betriebssystem Multithreading unterstützt, können mehrere Threads in demselben oder in unterschiedlichen Objekten aktiv sein.

In der folgenden Abbildung sind die passiven Objekte als kreisförmige Elemente dargestellt. Der schattierte innere Bereich jedes Objekts steht für die Zustandsinformationen des Objekts, und der unterteilte äußere Ring für die Prozeduren (Methoden), die das Verhalten des Objekts definieren.

Abbildung ist im Inhalt beschrieben.
Abbildung 5:   Abbildung der Objektinteraktionen.

Objektinterne Parallelität bringt alle Herausforderungen mit sich, die an parallele Software gestellt werden, z. B. das potenzielle Risiko von Wettbewerbssituationen, wenn mehrere Steuer-Threads Zugriff auf denselben Hauptspeicherbereich haben, in diesem Fall die in dem Objekt gekapselten Daten. Man könnte denken, dass Datenkapselung eine Lösung dieses Problems ist. Problematisch ist jedoch, dass das Objekt nicht den Steuer-Thread kapselt. Obwohl diese Probleme bei der Parallelität zwischen Objekten zum größten Teil vermieden werden, gibt es trotzdem ein Problem, dessen Lösung sehr mühevoll ist. Damit zwei parallele Objekt durch den Austausch von Nachrichten miteinander kommunizieren können, müssen mindestens zwei Steuer-Threads die Nachricht bearbeiten und auf denselben Hauptspeicherbereich zugreifen, um die Nachricht zu übergeben. Ein damit in Zusammenhang stehendes (aber ungleich schwierigeres) Problem ist die Verteilung von Objekten auf die unterschiedlichen Prozesse oder sogar Prozessoren. Für den Austausch von Nachrichten zwischen Objekten in unterschiedlichen Prozessen ist eine Interprozesskommunikation erforderlich. Hierfür muss die Nachricht in der Regel in Daten verwandelt werden, die über die Prozessgrenzen hinaus übergeben werden können.

Keines dieser Probleme ist natürlich unlösbar. Wie bereits im vorherigen Abschnitt beschrieben, muss sich jedes parallele System mit diesen Problemen auseinandersetzen, also gibt es auch bewährte Lösungen. Es ist nur so, dass die "Parallelitätssteuerung" zusätzliche Arbeit verursacht und zusätzliche Fehlerrisiken mit sich bringt. Außerdem verdeckt sie das eigentliche Anwendungsproblem. Aus all diesen Gründen verfolgt RUP das Ziel, dem Anwendungsprogrammierer so weit wie möglich zu ersparen, sich mit diesem Problem explizit auseinandersetzen zu müssen. Eine Möglichkeit ist, eine objektorientierte Umgebung einzurichten, die die Nachrichtenübergabe zwischen parallelen Objekten (einschließlich Parallelitätssteuerung) unterstützt, und die Verwendung mehrerer Steuer-Threads in einem einzelnen Objekt auf ein Minimum zu reduzieren bzw. ganz darauf zu verzichten. Damit wird der Steuer-Thread zusammen mit den Daten gekapselt.

Aktives Objektmodell

Objekte mit eigenen Threads werden als "aktive Objekte" bezeichnet. Für die Unterstützung asynchroner Kommunikation mit anderen aktiven Objekten erhält jedes aktive Objekt eine Nachrichtenwarteschlange oder "Mailbox". Wenn ein Objekt erstellt wird, erhält es von der Umgebung einen eigenen Steuer-Thread, den das Objekt für seine gesamte Lebensdauer kapselt. Wie ein passives Objekt ist das aktive Objekt so lange im Leerlauf, bis eine Nachricht von außen ankommt. Das Objekt führt den jeweiligen, für die Verarbeitung der Nachricht erforderlichen Code aus. Alle Nachrichten, die ankommen, während das Objekt beschäftigt ist, werden in die Mailbox gestellt. Sobald das Objekt die Verarbeitung einer Nachricht abschließt, kehrt es zurück und wählt die nächste anstehende Nachricht in der Mailbox aus bzw. wartet auf die Ankunft einer neuen Nachricht. Der Aufzug selbst, die Rufstationen der Stockwerke sowie der Disponent sind gute Kandidaten für aktive Objekte.

Aktive Objekte können abhängig von ihrer Implementierung sehr effizient sein. Sie haben jedoch etwas mehr Overhead als passive Objekte. Da nicht jede Operation parallel ausgeführt werden muss, ist es gebräuchlich, aktive und passive Objekt in demselben System zu verwenden. Wegen ihrer unterschiedlichen Kommunikationsstile ist es schwierig, sie zu Peers zu machen, aber ein aktives Objekt stellt eine ideale Umgebung für passive Objekte dar, die den zuvor verwendeten Betriebssystemprozess ersetzen kann. Wenn das aktive Objekt alle Arbeiten an passive Objekte delegiert, entspricht es im Wesentlichen einem Betriebssystem-Thread oder einem Thread mit Einrichtungen für Interprozesskommunikation. Interessantere aktive Objekte haben jedoch ein eigenes Verhalten für einen Teil der Arbeit, während sie andere Teile an passive Objekte delegieren.

Abbildung ist im Inhalt beschrieben.

Abbildung 6:   Ein 'aktives' Objekt stellt eine Umgebung für passive Klassen dar

Geeignete Kandidaten für passive Objekte in einem aktiven Aufzugobjekt sind eine Liste der Stockwerke, in denen der Aufzug bei der Aufwärtsfahrt anhalten muss, und eine weitere Liste für Haltestellen bei der Abwärtsfahrt. Der Aufzug muss in der Lage sein, die nächste Haltestelle aus der Liste abzufragen, neue Haltestellen zur Liste hinzuzufügen und Haltestellen, die bereits angefahren wurden, aus der Liste zu entfernen.

Da sich komplexe Systeme fast immer aus Subsystemen auf mehreren Ebenen zusammensetzen, bevor man zu den eigentlichen Komponenten auf Blattebene gelangt, ist eine selbstverständliche Erweiterung des aktiven Objektmodells zuzulassen, dass aktive Objekte andere aktive Objekte enthalten.

Obwohl ein aktives Objekt mit nur einem einzigen Thread keine echte objektinterne Parallelität unterstützt, ist das Delegieren von Arbeit an die enthaltenen aktiven Objekte für viele Anwendungen eine angemessene Alternative. Bei dieser Lösung bleibt der wichtige Vorteil der vollständigen Kapselung von Zustand, Verhalten und Steuer-Thread auf Objektbasis erhalten, was die Problematik der Parallelitätssteuerung vereinfacht.

Abbildung ist im Inhalt beschrieben.

Abbildung 7:   Das Aufzugsystem mit verschachtelten aktiven Objekten

Sehen Sie sich als Beispiel das in Teilen abgebildete Aufzugsystem an. Jeder Aufzug hat Türen, einen Hebezug und eine Steuerkonsole. Jede dieser Komponenten wird durch ein paralleles aktives Objekt modelliert. Das Türobjekt steuert das Öffnen und Schließen der Aufzugtüren, das Hebezugobjekt steuert die Positionierung des Aufzugs mit dem mechanischen Hebezug, und die Steuerkonsole überwacht die Knöpfe für die Stockwerksauswahl und die Knöpfe zum Öffnen und Schließen. Wenn die parallelen Steuer-Threads als aktive Objekte gekapselt werden, ergibt sich eine viel einfachere Software, als es möglich wäre, wenn diese Verhalten von einem einzigen Steuer-Thread verwaltet werden würde.

Problematik des 'konsistenten Zustands' in Objekten

Wie bereits bei der Diskussion der Wettbewerbssituationen erwähnt, müssen bestimmte zustandsabhängige Operationen atomar sein, damit sich ein System korrekt und vorhersehbar verhält.

Damit sich ein Objekt korrekt verhält, ist es natürlich entscheidend, dass sein Zustand vor und nach der Verarbeitung einer Nachricht intern konsistent ist. Während der Verarbeitung einer Nachricht kann sich das Objekt in einem Übergangszustand befinden und unbestimmt sein, weil die Operationen nur teilweise abgeschlossen sind.

Wenn ein Objekt seine Antwort auf eine Nachricht abschließt, bevor es auf eine andere Nachricht antwortet, ist dieser Übergangszustand kein Problem. Auch die Unterbrechung eines Objekts, um ein anderes auszuführen, stellt kein Problem dar, weil sich jedes Objekt an die strikte Kapselung seines Zustands hält. (Um ehrlich zu sein, stimmt dies nicht ganz genau, aber das wird später noch erläutert.)

Alle Umstände, unter denen ein Objekt die Verarbeitung einer Nachricht unterbricht, um eine andere Nachricht zu bearbeiten, öffnet Wettbewerbssituationen Tür und Tor. In diesem Fall ist die Verwendung einer Parallelitätssteuerung erforderlich. Dies öffnet jedoch die Tür für Deadlocks.

Paralleldesign ist deshalb im Allgemeinen einfacher, wenn Objekte eine Nachricht vollständig verarbeiten, bevor sie eine weitere akzeptieren. In der speziellen Form aktiver Objekte, die wir gezeigt haben, wird dieses Verhalten impliziert.

Die Problematik des konsistenten Zustands kann sich bei parallelen Systemen in zwei Formen zeigen und ist wahrscheinlich mit Hilfe objektorientierter paralleler Systeme einfacher zu verstehen. Die erste Form haben wir bereits besprochen. Wenn der Zustand eines einzigen Objekts (aktiv oder passiv) mehr als einem Steuer-Thread zugänglich ist, müssen atomare Operationen entweder durch natürlicherweise atomare CPU-Instruktionen, oder durch einen Steuermechanismus für Parallelität geschützt werden.

Die zweite Form der Problematik des konsistenten Zustands ist etwas subtiler. Falls mehrere Objekte (aktiv oder passiv) dieselben Zustandsinformationen haben, wird es zwangsläufig einen, wenn auch kurzen, Zeitraum geben, in dem die Objekte bezüglich Ihres Zustands nicht übereinstimmen. Bei einem schlechten Design könnte dieser Zeitraum auch länger dauern und möglicherweise sogar für immer. Mathematisch gesehen ist diese Form der Problematik ein "Doppelgänger" der ersten Form.

Die Bewegungssteuerung des Aufzugs (die Winde) muss beispielsweise sicherstellen, dass die Türen geschlossen sind, bevor sich der Aufzug in Bewegung setzt. Ein Design ohne ausreichende Sicherheitsvorkehrungen könnte als Antwort auf den Druck der Taste "Türen öffnen" durch einen Passagier die Türen genau in dem Moment öffnen, in dem sich der Aufzug in Bewegung setzt.

Eine einfache Lösung des Problems scheint darin zu bestehen, Zustandsinformationen nur in einem der beteiligten Objekte zu erlauben. Obwohl dies helfen kann, könnte diese Maßnahme nachteilige Auswirkungen auf die Leistung, vor allem bei verteilten Systemen, haben. Außerdem ist diese Lösung fehleranfällig. Auch wenn nur ein Objekt bestimmte Zustandsinformationen speichert, könnten zu einem bestimmten Zeitpunkt andere, parallele Objekte Entscheidungen aus den Zustandsinformationen ableiten, während das Objekt im Begriff steht, seinen Zustand zu ändern und damit die Entscheidungen anderer Objekte falsifiziert.

Es gibt keine Patentlösung für das Problem des konsistenten Zustands. Alle praktischen Lösungen fordern von uns, atomare Operationen zu identifizieren und diese mit Hilfe eines Synchronisationsmechanismus zu schützen, der den parallelen Zugriff für eine vertretbar kurze Zeit verhindert. "Vertretbar kurz" ist sehr kontextabhängig. Es könnte im einen Fall so lange sein, wie die CPU braucht, um alle Bytes einer Gleitkommazahl zu speichern, oder im anderen Fall so lange, wie der Aufzug braucht, um bis zum nächsten Haltepunkt zu fahren.

Echtzeitsysteme

In Echtzeitsystemen empfiehlt RUP die Verwendung von  Kapseln für die Darstellung aktiver Objekte. Kapseln haben eine potente Semantik, mit der die Modellierung von Parallelität vereinfacht werden kann:
  • Sie verwenden asynchrone, nachrichtenbasierte Kommunikation über  Ports mit klar strukturierten  Protokollen.
  • Sie verwenden eine umfassende Semantik für die Nachrichtenverarbeitung.
  • Sie kapseln passive Objekte (und gewährleisten damit, dass keine Thread-Kollisionen auftreten).