Nebenläufigkeit in der Informatik verstehen
Unterscheide verzahnte und gleichzeitige Abläufe, schütze gemeinsame Daten und erkläre Verklemmungen sowie das Warten auf freie Plätze oder Elemente.
Kurz gesagt
Nebenläufigkeit bedeutet, dass mehrere Abläufe zeitlich überlappend bearbeitet werden. Auf einem einzelnen Rechenkern wechseln sie sich ab; auf mehreren Kernen können sie gleichzeitig laufen.
Ein Thread ist ein einzelner Ausführungsablauf innerhalb eines Programms. Mehrere Threads können dieselben Daten nutzen und ihre Arbeitsschritte zeitlich verzahnen. Du untersuchst, wann dadurch Fehler oder Wartesituationen entstehen und wie sich die Zusammenarbeit regeln lässt.
Deine Lernziele
Ein Prozessor führt die Arbeitsschritte eines Programms aus. Ein Rechenkern ist eine Ausführungseinheit des Prozessors; ein Prozessor kann mehrere Kerne besitzen. In unserem Ablaufmodell führt ein einzelner Kern zu einem Zeitpunkt einen Schritt eines Threads aus. Er kann zwischen Threads wechseln, bevor deren Aufgaben abgeschlossen sind. Das wirkt gleichzeitig und heißt hier scheinbare Nebenläufigkeit. Auf mehreren Kernen können Schritte verschiedener Threads tatsächlich gleichzeitig ausgeführt werden. Dafür wird auch die Bezeichnung echte Nebenläufigkeit verwendet.
Die Begriffe werden unterschiedlich weit gebraucht: Im engeren Sprachgebrauch bezeichnet Nebenläufigkeit die Organisation überlappender Abläufe. Parallelität bezeichnet dann deren tatsächlich gleichzeitige Ausführung. Nebenläufigkeit setzt nach diesem Verständnis keine mehreren Kerne voraus.
Ein Zugriff auf einen gemeinsamen Wert kann mehrere Schritte umfassen: den Wert lesen, eine eigene Kopie verändern und das Ergebnis zurückschreiben. Zurückschreiben ersetzt den bisherigen gemeinsamen Wert; es addiert ihn nicht zum neuen Wert. Zwei Threads können denselben alten Wert lesen und anschließend mit getrennten Kopien weiterrechnen. Schreibt einer später sein Ergebnis zurück, kann er das Ergebnis des anderen überschreiben. Dafür genügt ein Wechsel zwischen Threads auf einem einzigen Kern.
Die folgenden Abläufe sind ausdrücklich vereinfachte Modelle mit vollständig angegebenen Schritten. Sie beschreiben keine bestimmte Programmiersprache. Der gezeigte fehlerhafte Ablauf ist eine Möglichkeit: Bei anderer Reihenfolge können dieselben Änderungen auch ohne verlorenen Beitrag enden.
Race Condition
Eine Race Condition ist ein Fehler, bei dem das Ergebnis vom zeitlichen Zusammenspiel konkurrierender Abläufe abhängt. Hier greifen mehrere Threads ungeschützt auf denselben Wert zu, und mindestens einer verändert ihn.
Beispiel: Zwei Erhöhungen eines Zählers
- 1Ausgedachtes Ablaufmodell: Der gemeinsame Zähler beginnt bei 8. Die Threads A und B sollen ihn jeweils um 1 erhöhen.
- 2A liest 8 in seine eigene Kopie. Bevor A zurückschreibt, liest auch B den gemeinsamen Wert 8.
- 3A erhöht seine Kopie auf 9. B erhöht seine eigene Kopie ebenfalls auf 9.
- 4A schreibt 9 zurück. Danach schreibt B ebenfalls 9 zurück.
- 5Der Endwert ist 9. Bei vollständig nacheinander ausgeführten Erhöhungen wäre er 10. Ein Beitrag ist durch das Überschreiben verloren gegangen.
Teste dich
Ein kritischer Abschnitt ist ein Teil eines Ablaufs, in dem ein ungeschützter Zugriff auf gemeinsame Daten zu einem fehlerhaften Zustand führen kann. Beim Lesen, Verändern und Zurückschreiben eines gemeinsamen Wertes gehört der gesamte zusammenhängende Ablauf zum Schutzbereich. Wird bloß das Lesen oder bloß das Schreiben geschützt, können zwei Threads weiterhin mit demselben alten Wert rechnen.
Ein Mutex ist eine Sperre für wechselseitigen Ausschluss: Unter den Threads, die dieselbe Sperre verwenden, darf jeweils ein Thread den geschützten Abschnitt ausführen. Ein anderer Thread muss auf die Freigabe warten. Blockiert zu sein bedeutet hier, an dieser Stelle nicht weiterarbeiten zu können, bis die benötigte Freigabe vorliegt. Alle beteiligten Zugriffe müssen dieselbe Schutzregel beachten. Der Mutex verhindert keinen Wechsel des laufenden Threads. Er verhindert, dass ein anderer Thread inzwischen denselben geschützten Abschnitt betritt.
Ein Semaphor verwaltet Freigaben. Wir verwenden dafür die Operationen P und V. P reserviert eine verfügbare Freigabe; ist keine verfügbar, blockiert der Aufrufer. V gibt eine Freigabe hinzu und kann dadurch einen Wartenden wieder bereit machen. Die Prüfung und Reservierung bei P werden als unteilbarer Schritt behandelt: Zwei Threads können nicht dieselbe Freigabe erhalten.
Zum Schutz eines kritischen Abschnitts kann ein binärer Semaphor verwendet werden. Binär bedeutet hier, dass für den Schutz höchstens eine Freigabe vorhanden ist. Er startet mit einer Freigabe. Jeder Thread führt vor dem gesamten Abschnitt P und danach genau einmal V aus. Mit zwei anfänglichen Freigaben könnten zwei Threads eintreten; damit wäre dieser wechselseitige Ausschluss nicht erreicht. Unsere Modelle setzen voraus, dass ein eingetretener Thread seinen Abschnitt beendet und die Sperre anschließend freigibt.
Wechselseitiger Ausschluss
Von den Threads, die dieselbe Schutzsperre benutzen, befindet sich jeweils höchstens einer im geschützten Abschnitt.
Beispiel: Geschütztes Anhängen an eine Auftragsliste
- 1Ausgedachtes Modell: Zwei Threads hängen Auftragsnamen an dieselbe Liste an. Das Anhängen besteht hier aus mehreren Schritten.
- 2Die folgenden Zeilen sind Pseudocode: eine Beschreibung des Ablaufs, kein direkt ausführbares Programm. schutz bezeichnet einen gemeinsamen binären Semaphor mit anfangs einer Freigabe.
- 3P(schutz)
- 4 Auftragsnamen vollständig an die Liste anhängen
- 5V(schutz)
- 6Jeder beteiligte Thread verwendet diese Reihenfolge. Während einer die Liste verändert, kann der andere P(schutz) nicht erfolgreich beenden.
Merke: Die Sperre muss den gesamten gefährdeten Zugriff umfassen. Ein Wechsel der Rechenzeit gibt eine gehaltene Sperre nicht frei.
Teste dich
Ein Deadlock, auch Verklemmung genannt, ist eine festgefahrene zyklische Wartesituation. Zyklisch bedeutet: Die Abhängigkeiten bilden einen geschlossenen Kreis. Jeder beteiligte Thread wartet auf eine Ressource, die ein anderer beteiligter Thread festhält. Eine Ressource, auch Betriebsmittel genannt, ist etwas, das ein Thread für seine Arbeit benötigt. In unserem Modell sind das exklusiv gehaltene Sperren. Exklusiv bedeutet, dass eine Sperre jeweils einem Besitzer zugeteilt ist.
Die vier Coffman-Bedingungen beschreiben Voraussetzungen für eine solche Verklemmung. Erstens gilt wechselseitiger Ausschluss: Eine benötigte Ressource kann nicht gleichzeitig von mehreren Threads genutzt werden. Zweitens gilt Halten und Warten: Ein Thread behält eine Ressource, während er auf eine weitere wartet. Drittens gibt es keinen erzwungenen Entzug: Eine gehaltene Ressource wird ihrem Besitzer nicht gegen dessen Willen weggenommen. Viertens besteht kreisförmiges Warten: Die Warteabhängigkeiten schließen sich zu einem Kreis.
Ausgedachtes Modell: Thread M hält die Sperre für eine Bilddatei und wartet auf die Sperre für eine Tondatei. Thread N hält die Tonsperre und wartet auf die Bildsperre. Beide geben ihre Sperren erst frei, nachdem sie beide Dateien bearbeiten konnten. Die Sperren sind exklusiv und werden nicht entzogen. Damit stehen beide fest. Ein bloßer Wechsel der Rechenzeit löst die Sperren nicht und unterbricht diesen Wartekreis nicht.
Die Bedingungen beschreiben die Möglichkeit einer Verklemmung; eine gefährliche Anforderungsweise führt nicht bei jeder zeitlichen Anordnung dazu. Bearbeitet M beide Dateien und gibt die Sperren frei, bevor N beginnt, entsteht dieser Kreis nicht. Für das Modell mit diesen beiden Sperren verhindert eine einheitliche Anforderungsreihenfolge den Kreis: Beide Threads fordern zuerst die Bildsperre und danach die Tonsperre an. Auch das Freigeben einer gehaltenen Sperre vor dem Warten kann einen vorhandenen Wartekreis durchbrechen. Zusätzliche Anforderungen geben dagegen keine schon gehaltene Sperre frei.
| Coffman-Bedingung | Im Beispiel mit Bild- und Tondatei |
|---|---|
| Wechselseitiger Ausschluss | Die Bildsperre gehört M, die Tonsperre gehört N. |
| Halten und Warten | Beide behalten ihre erste Sperre, während sie die zweite anfordern. |
| Kein erzwungener Entzug | Die gehaltenen Sperren werden den Besitzern nicht weggenommen. |
| Kreisförmiges Warten | M wartet auf N, und N wartet auf M. |
Merke: Das Unterbrechen eines Threads entzieht ihm keine Ressource. Entscheidend ist, wer welche Sperre hält, worauf er wartet und wann er freigibt.
Teste dich
Ein Erzeuger stellt Elemente bereit, ein Verbraucher entnimmt sie zur weiteren Verarbeitung. Beide nutzen einen gemeinsamen Puffer. Ein Puffer ist hier eine Datenstruktur, also eine geordnete Sammlung von Daten, mit einer begrenzten Zahl von Ablageplätzen. Ein Verbraucher darf kein Element aus einem leeren Puffer entnehmen. Ein Erzeuger darf kein Element in einen vollen Puffer einfügen. In diesen Fällen sollen die Threads blockieren, statt mit einem ungültigen Zugriff fortzufahren.
Wir verwenden drei gemeinsame Semaphore. frei verwaltet Freigaben für freie Plätze, belegt Freigaben für verfügbare Elemente und schutz den wechselseitigen Ausschluss beim Verändern des Puffers. Für einen anfangs leeren Puffer mit drei Plätzen startet frei mit drei Freigaben, belegt mit null und schutz mit einer. P reserviert die jeweilige Freigabe. Während eine Operation noch läuft, können Freigaben bereits reserviert sein; die Zähler müssen dann nicht genau der sichtbaren Belegung entsprechen.
Der Erzeuger reserviert zuerst mit P(frei) einen Platz. Erst danach erwirbt er mit P(schutz) den Schutz, fügt sein Element ein, gibt schutz frei und meldet mit V(belegt) ein verfügbares Element. Der Verbraucher reserviert zuerst mit P(belegt) ein Element. Danach erwirbt er schutz, entnimmt das Element, gibt schutz frei und meldet mit V(frei) einen freien Platz. Das Einfügen meldet somit ein Element, das Entnehmen einen freien Platz.
Auf ein Element oder einen freien Platz wird vor dem Erwerb von schutz gewartet. Hielte ein Verbraucher beim Warten auf ein Element bereits schutz fest, könnte ein Erzeuger den Schutz zum Einfügen nicht erhalten. Ein Signal kann einen passenden Wartenden wieder bereit machen. Aufwecken garantiert aber keine sofortige Ausführung: Der Thread braucht anschließend Rechenzeit und muss weitere benötigte Sperren erwerben.
Bei unbegrenzter Pufferkapazität entfällt das Warten auf freie Plätze; der Semaphor frei wird dann nicht benötigt. Sind die Änderungen der Datenstruktur tatsächlich keine kritischen Aktionen, kann schutz entfallen. In unserem mehrschrittigen Änderungsmodell ist dieser Schutz erforderlich. Auch ein unbegrenzter Puffer kann leer sein, sodass Verbraucher weiterhin auf Elemente warten müssen.
Beispiel: Abläufe am Puffer mit drei Plätzen
- 1Ausgedachtes Modell: Der Puffer ist anfangs leer. frei startet mit 3, belegt mit 0 und schutz mit 1 Freigabe.
- 2Die folgenden Abläufe sind Pseudocode. Das Semikolon trennt nacheinander auszuführende Schritte.
- 3Erzeuger: P(frei); P(schutz); Element einfügen; V(schutz); V(belegt).
- 4Verbraucher: P(belegt); P(schutz); Element entnehmen; V(schutz); V(frei).
- 5Beginnt der Verbraucher am leeren Puffer, blockiert er bei P(belegt), ohne schutz zu halten.
- 6Ein Erzeuger kann ein Element einfügen und mit V(belegt) die benötigte Freigabe liefern. Der Verbraucher kann danach wieder bereit werden.
Merke: Der Schutz verhindert konkurrierende Änderungen. Die Freigaben für Plätze und Elemente regeln zusätzlich, wann Einfügen und Entnehmen möglich sind.
Teste dich
Alles auf einen Blick
Nebenläufigkeit
Ein einzelner Kern verzahnt Schritte. Mehrere Kerne können Schritte gleichzeitig ausführen.
Ungeschützte Zugriffe können abhängig von ihrer Reihenfolge Ergebnisse überschreiben.
Derselbe Mutex oder ein passend verwendeter binärer Semaphor schützt den gesamten kritischen Abschnitt.
Ausschluss, Halten und Warten, fehlender Entzug und ein Wartekreis ermöglichen eine festgefahrene Wartesituation.
Ein leerer Puffer lässt Verbraucher warten, ein voller begrenzter Puffer lässt Erzeuger warten.
Zuerst auf Element oder Platz warten, danach den Pufferschutz erwerben. Aufwecken verspricht keine sofortige Ausführung.
Einen Ablauf am Puffer mit fünf Plätzen verfolgen
- 1Ausgedachtes Modell: Ein Puffer hat fünf Plätze und enthält vier Elemente. Keine Operation läuft gerade. frei hat eine Freigabe, belegt hat vier und schutz hat eine. Beteiligt sind die Erzeuger E und F sowie der Verbraucher G.
- 2E führt P(frei) und P(schutz) aus, fügt sein Element ein und führt V(schutz) sowie V(belegt) aus. Jetzt enthält der Puffer fünf Elemente; frei hat null und belegt fünf Freigaben.
- 3F versucht P(frei) und blockiert. Er hat schutz noch nicht erworben und hält den Pufferschutz deshalb nicht fest.
- 4G führt P(belegt) und P(schutz) aus und entnimmt ein Element. Der Puffer enthält nun vier Elemente. G führt V(schutz) und V(frei) aus und liefert damit eine Platzfreigabe für F.
- 5Wenn F wieder läuft und sein P(frei) erfolgreich beendet ist, kann er P(schutz) ausführen und sein Element einfügen. Nach V(schutz) und V(belegt) enthält der Puffer wieder fünf Elemente.
Fehler finden
Fehler in Zeile 3Die Sperre umfasst hier den Schreibschritt, während Lesen und Berechnen außerhalb liegen. Richtig: Der Mutex muss vor dem Lesen erworben und nach dem Zurückschreiben freigegeben werden, damit die gesamte Verdopplung geschützt ist.
Lückentext
Ein einzelner Rechenkern führt abwechselnd Schritte zweier noch nicht abgeschlossener Threads aus. Das ist . Zwei Threads sollen einen kritischen Abschnitt wechselseitig ausschließend betreten. Der gemeinsame Schutzsemaphor startet dafür mit . Ein begrenzter Puffer ist voll, und ein Erzeuger wartet auf einen Platz. Das eines Elements schafft einen freien Platz, der anschließend mit V(frei) gemeldet wird.