Informatik

Nebenläufigkeit in der Informatik verstehen

Nebenläufigkeit in der Informatik verstehen
Nebenläufigkeit in der Informatik verstehen
Für Quiz, Lückentext, Lernkarten und Fortschritt ist JavaScript nötig. Alle Inhalte und Lösungen bleiben direkt lesbar.

Bei Nebenläufigkeit können mehrere Aufgaben zeitlich überlappend bearbeitet werden. Sie müssen dabei nicht wirklich gleichzeitig laufen: Auf einem Prozessorkern kann das System rasch zwischen ihnen wechseln. Sobald die Aufgaben gemeinsame Daten oder Betriebsmittel nutzen, können Race Conditions und Deadlocks entstehen.

Auf dieser Seite lernst du, Abläufe zu untersuchen, kritische Abschnitte zu erkennen und eine passende Synchronisation zu begründen.

Deine Lernziele

Hake ab, was du schon kannst — und komm am Ende hierher zurück!

Nebenläufig oder wirklich parallel?

Stell dir einen Musikplayer vor: Er spielt Musik ab, reagiert auf deine Eingaben und lädt weitere Titel. Diese Tätigkeiten können sich zeitlich überlappen. Genau das ist die Grundidee der Nebenläufigkeit.

Definition

Nebenläufigkeit

Mehrere Aufgaben sind nebenläufig, wenn sich ihre Ausführungszeiträume überlappen können. Auf einem Kern wechseln sich die Aufgaben ab; auf mehreren Kernen können einige Schritte tatsächlich gleichzeitig stattfinden.

Parallelität bezeichnet in der engeren Bedeutung die echte gleichzeitige Ausführung auf mehreren Recheneinheiten. Nebenläufigkeit beschreibt dagegen vor allem die Organisation mehrerer möglicher Abläufe.

Ein Prozess ist ein laufendes Programm mit eigenen verwalteten Ressourcen. Ein Thread ist ein Ausführungsablauf innerhalb eines Prozesses. Mehrere Threads desselben Prozesses können gemeinsame Objekte und denselben Speicher nutzen.

Beispiel

Ein Webserver bearbeitet Anfrage A und Anfrage B. Während A auf eine Datenbankantwort wartet, kann der Server an B weiterarbeiten. Das ist auch auf einem einzelnen Kern nebenläufig möglich. Werden A und B gleichzeitig auf verschiedenen Kernen berechnet, liegt zusätzlich Parallelität vor.

Nicht jeder Ablauf lässt sich beliebig überlappen. Benötigt Schritt B das Ergebnis von Schritt A, muss A zuerst abgeschlossen sein. Unabhängige Schritte sind dagegen parallelisierbar, wenn ihre Reihenfolge das Ergebnis nicht verändert.

Teste dich
Frage 1 von 2LeichtWelche Situation ist sicher ein Beispiel für Nebenläufigkeit, aber nicht zwingend für Parallelität?
Lösung: Ein Kern wechselt zwischen zwei noch nicht abgeschlossenen Aufgaben. — Nebenläufigkeit verlangt überlappende Abläufe, aber keine echte Gleichzeitigkeit.
Frage 2 von 2MittelAufgabe B benötigt das Ergebnis von Aufgabe A. Welche Aussage stimmt?
Lösung: A muss bezüglich dieser Abhängigkeit vor B ausgeführt werden. — Abhängigkeiten begrenzen die mögliche Umordnung. Andere, unabhängige Teile können trotzdem nebenläufig sein.
Wie entsteht eine Race Condition?

Gemeinsam genutzte Daten werden gefährlich, wenn mehrere Threads sie verändern. Eine scheinbar einfache Anweisung wie zaehler = zaehler + 1 kann intern aus drei Schritten bestehen:

  1. Wert lesen.
  2. Eins addieren.
  3. Ergebnis zurückschreiben.

Diese zusammengehörenden Schritte sind nicht automatisch atomar. Atomar bedeutet, dass andere Threads keine Zwischenzustände beobachten oder dazwischen eingreifen können.

Definition

Race Condition

Eine Race Condition liegt vor, wenn das Ergebnis eines Programms von der zufälligen oder unerwarteten Reihenfolge nebenläufiger Schritte abhängt.

Beispiel

Der gemeinsame Zähler startet bei 0. Thread A und Thread B sollen ihn jeweils einmal erhöhen.

Eine fehlerhafte Verschachtelung kann so aussehen:

  1. A liest 0.
  2. B liest ebenfalls 0.
  3. A berechnet 1 und schreibt 1.
  4. B berechnet aus seinem alten Lesewert ebenfalls 1 und schreibt 1.

Der Endwert ist 1, obwohl zwei Erhöhungen stattgefunden haben. Bei einer seriellen Ausführung — erst A vollständig, dann B — wäre der Endwert 2.

Die Reihenfolge zeigt den Fehler als einfache Ablaufspur. In einem Sequenzdiagramm würdest du dieselben Lese-, Rechen- und Schreibschritte zeitlich untereinander anordnen.

Merke

Wenn mehrere Threads denselben veränderlichen Zustand lesen und schreiben, untersuche nicht nur die Programmzeilen, sondern die möglichen Einzelschritte und ihre Verschachtelungen.

Teste dich
Frage 1 von 2MittelDer Zähler steht auf 5. Zwei Threads lesen beide 5, erhöhen ihren gelesenen Wert und schreiben nacheinander. Welcher Endwert ist möglich?
Lösung: 6 — Beide Threads können auf Grundlage desselben alten Werts 5 jeweils 6 schreiben. Eine Erhöhung geht dann verloren.
Frage 2 von 2SchwerBei welchem Zugriff droht im beschriebenen Modell keine Race Condition am gemeinsamen Zähler?
Lösung: Jeder Thread bearbeitet ausschließlich einen eigenen, nicht gemeinsam genutzten Zähler. — Ohne gemeinsam genutzten veränderlichen Zustand können sich die beiden Zähleroperationen nicht gegenseitig verfälschen.
Kritische Abschnitte richtig schützen

Der Teil eines Programms, in dem ein Thread auf gemeinsam genutzte Daten oder Betriebsmittel zugreift, heißt kritischer Abschnitt. Im Zählerbeispiel gehören Lesen, Erhöhen und Zurückschreiben gemeinsam in diesen Abschnitt.

Definition

Synchronisation

Synchronisation koordiniert nebenläufige Abläufe. Sie kann festlegen, welcher Thread wann auf eine Ressource zugreifen darf oder wann ein Thread auf eine Bedingung warten muss.

Eine Sperre oder ein Lock sorgt typischerweise dafür, dass nur ein Thread gleichzeitig den geschützten Abschnitt ausführt. Dieser gegenseitige Ausschluss wird auch Mutex genannt.

Der sichere Ablauf lautet:

  1. Sperre erwerben.
  2. Gemeinsamen Wert lesen, verändern und zurückschreiben.
  3. Sperre zuverlässig freigeben.

In Java kann eine synchronized-Methode den Zugriff auf ein Objekt gegenseitig ausschließen. Ein expliziter Lock wird mit lock() erworben und mit unlock() freigegeben. Die Freigabe sollte auch bei einem Fehler sichergestellt sein.

Weitere Mechanismen erfüllen verwandte Aufgaben:

  • Ein Semaphor verwaltet mit einem Zähler, wie viele gleichzeitige Zugriffe erlaubt sind.
  • Ein Monitor kapselt gemeinsame Daten und die synchronisierten Operationen darauf.
  • Ein Read-Write-Lock kann mehrere gleichzeitige Lesezugriffe erlauben, während ein Schreibzugriff exklusiv bleibt.
Beispiel

Beim Zähler muss die Sperre die gesamte Folge aus Lesen, Erhöhen und Schreiben umfassen. Würde nur das Schreiben geschützt, könnten A und B weiterhin denselben alten Wert lesen. Die Sperre wäre dann zu klein gewählt.

Eine einzige Sperre für viele voneinander unabhängige Daten wäre dagegen unnötig grob: Threads müssten warten, obwohl sie sich nicht beeinflussen. Die passende Granularität schützt alle zusammengehörenden Schritte, blockiert aber möglichst wenig unabhängige Arbeit.

Synchronisation garantiert nicht automatisch ein korrektes Gesamtsystem. Zu kleine kritische Abschnitte lassen Race Conditions zu. Zu große oder widersprüchlich geordnete Sperrbereiche können unnötige Wartezeiten oder Deadlocks erzeugen.

Teste dich
Frage 1 von 2MittelWelcher Bereich muss beim gemeinsamen Zähler durch eine Sperre geschützt werden?
Lösung: Lesen, Erhöhen und Zurückschreiben als zusammengehörende Operation — Der Schutz muss die vollständige Lese-Ändere-Schreib-Folge atomar erscheinen lassen.
Frage 2 von 2SchwerZwei voneinander unabhängige Datenobjekte besitzen jeweils eine eigene Sperre. Was ist der wichtigste Vorteil gegenüber einer einzigen globalen Sperre?
Lösung: Unabhängige Zugriffe können sich weiterhin überlappen. — Getrennte Sperren können die Nebenläufigkeit erhalten. Werden mehrere Sperren kombiniert, muss ihre Reihenfolge dennoch sorgfältig geplant werden.
Wann blockieren sich Threads dauerhaft?

Eine Sperre lässt einen Thread warten, bis ein anderer sie freigibt. Problematisch wird es, wenn mehrere Threads in einem geschlossenen Kreis aufeinander warten.

Definition

Deadlock

Ein Deadlock oder eine Verklemmung ist ein Zustand, in dem beteiligte Threads nicht weiterarbeiten können, weil jeder auf eine Ressource wartet, die von einem anderen beteiligten Thread gehalten wird.

Beispiel

Es gibt die Sperren L1 und L2.

  1. Thread A erwirbt L1.
  2. Thread B erwirbt L2.
  3. A wartet auf L2.
  4. B wartet auf L1.

A kann L1 erst nach seinem nächsten Schritt freigeben, erreicht diesen aber ohne L2 nicht. Für B gilt das Umgekehrte. Beide bleiben stehen.

Für einen Deadlock müssen die vier Coffman-Bedingungen gemeinsam vorliegen:

  1. Exklusiver Zugriff: Eine Ressource kann nicht gleichzeitig von allen genutzt werden.
  2. Halten und Warten: Ein Thread hält bereits eine Ressource und wartet auf eine weitere.
  3. Kein Entzug: Eine Ressource wird nicht zwangsweise entzogen, sondern vom Besitzer freigegeben.
  4. Zyklisches Warten: Es gibt eine geschlossene Kette wartender Threads.

Die vier Bedingungen zeigen, wann ein Deadlock möglich ist. Sie bedeuten nicht, dass jede Programmausführung tatsächlich in einem Deadlock endet.

Ein Betriebsmittelzuteilungsgraph stellt Threads, Ressourcen sowie Halte- und Anforderungsbeziehungen dar. Ein Kreis im Graphen macht zyklisches Warten sichtbar. Bei mehreren Exemplaren einer Ressource muss zusätzlich geprüft werden, ob der Kreis wirklich eine Verklemmung verursacht.

Eine häufige Gegenmaßnahme ist eine feste globale Sperrreihenfolge: Wenn alle Threads zuerst L1 und danach L2 erwerben, kann der gezeigte Wartekreis nicht entstehen. Auch kürzeres Halten, das gemeinsame Anfordern benötigter Ressourcen oder der Entwurf ohne eine der Coffman-Bedingungen kann Deadlocks verhindern.

Gut zu wissen

Ein Deadlock ist ein Lebendigkeitsproblem: Das System erreicht keinen weiteren Fortschritt. Eine Race Condition gefährdet dagegen vor allem die Sicherheit eines Ergebnisses, weil ein unerlaubter oder falscher Zustand entstehen kann.

Teste dich
Frage 1 von 3LeichtWelche Beobachtung kennzeichnet den gezeigten Deadlock mit L1 und L2?
Lösung: A wartet auf B und B zugleich auf A. — Entscheidend ist die geschlossene Kette aus gehaltenen und angeforderten Ressourcen.
Frage 2 von 3MittelWelche Coffman-Bedingung wird durch eine verbindliche Reihenfolge aller Sperren gezielt verhindert?
Lösung: Zyklisches Warten — Wenn alle Threads Sperren in derselben Reihenfolge erwerben, kann keine kreisförmige Sperrabhängigkeit entstehen.
Frage 3 von 3SchwerIn einem Zuteilungsgraphen erscheint ein Kreis. Welche Schlussfolgerung ist sicher?
Lösung: Es liegt zyklisches Warten vor oder kann durch die dargestellten Anforderungen entstehen; die Ressourcenanzahl muss für die Deadlockaussage mitgeprüft werden. — Der Kreis ist ein wichtiges Warnzeichen. Für die genaue Deadlockdiagnose zählen zusätzlich Haltezustand und verfügbare Ressourcenexemplare.
Wie warten Threads kontrolliert?

Aktives Warten, auch Busy Waiting genannt, prüft eine Bedingung ständig in einer Schleife. Hält der wartende Thread dabei die benötigte Sperre, kann er gerade den Thread aussperren, der die Bedingung ändern müsste.

Beim Bankkonto wäre das fatal: withdraw wartet bei zu geringem Guthaben, behält aber die Kontosperre. deposit kann die Sperre nicht erhalten und deshalb kein Geld einzahlen.

Kontrolliertes Warten gibt die Sperre während der Wartezeit frei. In Java bewirkt wait(), dass der Thread wartet und die Objektsperre freigibt. Nach einer Zustandsänderung kann notifyAll() die wartenden Threads benachrichtigen. Anschließend konkurrieren sie erneut um die Sperre und prüfen die Bedingung nochmals. Deshalb wird die Bedingung mit einer Schleife geprüft, nicht nur einmal.

Beispiel

Beim Erzeuger-Verbraucher-Problem produziert ein Thread Daten, ein anderer verarbeitet sie. Eine begrenzte BlockingQueue verbindet beide:

  • put fügt ein Element ein und wartet kontrolliert, wenn die Queue voll ist.
  • take entfernt ein Element und wartet kontrolliert, wenn die Queue leer ist.

Bei einer Kapazität von zwei Plätzen kann der Erzeuger zwei Elemente ablegen. Vor einem dritten Element wartet er, bis der Verbraucher eines entnommen hat. Die Queue kapselt Synchronisation und Wartebedingung; eine selbst geschriebene leere Warteschleife ist nicht nötig.

Zum Testen reicht ein einzelner erfolgreicher Durchlauf nicht. Sinnvolle Prüfungen umfassen verschiedene Ablaufreihenfolgen und kontrollierte Grenzfälle:

  • Verbraucher startet bei leerer Queue und wartet.
  • Erzeuger füllt die Queue vollständig und wartet vor dem nächsten Element.
  • Nach einer Entnahme kann der Erzeuger fortfahren.
  • Kein Element geht verloren und kein Element wird doppelt verarbeitet.
Teste dich
Frage 1 von 2MittelWarum ist Busy Waiting innerhalb einer gehaltenen Kontosperre problematisch?
Lösung: Der einzahlende Thread kann die Sperre nicht erwerben und die Wartebedingung nicht ändern. — Wer auf eine Zustandsänderung wartet, darf den dafür benötigten Änderungsweg nicht durch eine gehaltene Sperre blockieren.
Frage 2 von 2SchwerEine BlockingQueue ist leer und der Verbraucher ruft take() auf. Was soll geschehen?
Lösung: Der Verbraucher wartet kontrolliert, bis ein Element verfügbar ist. — take() verbindet die Entnahme mit der Bedingung, dass ein Element vorhanden sein muss.
Karteikasten
Karteikasten

Überlege zuerst selbst und drehe die Karte anschließend zum Prüfen um.

Alles auf einen Blick
Mindmap
  • Nebenläufigkeit
    • Ausführung: verzahnt auf einem Kern oder parallel auf mehreren Kernen
    • Gemeinsamer Zustand: mögliche Race Condition
    • Schutz: kritischer Abschnitt mit Lock, Semaphor oder Monitor
    • Lebendigkeit: möglicher Deadlock durch zyklisches Warten
    • Kommunikation: kontrolliertes Warten und Benachrichtigen
    • Anwendung: Erzeuger und Verbraucher mit BlockingQueue

Die zentrale Entwurfsfrage lautet: Welche Schritte dürfen sich überlappen, und welche müssen als unteilbare oder geordnete Einheit behandelt werden? Erst wenn du gemeinsame Daten, Abhängigkeiten und Ressourcen erkennst, kannst du die passende Synchronisation wählen.

Abschluss-Check
Teste dich
Frage 1 von 4LeichtWas unterscheidet Nebenläufigkeit von Parallelität im engeren Sinn?
Lösung: Nebenläufigkeit erlaubt zeitlich überlappende Abläufe auch auf einem Kern; Parallelität verlangt echte Gleichzeitigkeit. — Die Begriffe beschreiben unterschiedliche Aspekte: Organisation überlappender Abläufe und tatsächliche Gleichzeitigkeit.
Frage 2 von 4MittelZwei Threads erhöhen einen gemeinsamen Zähler. Warum muss die gesamte Lese-Ändere-Schreib-Folge geschützt werden?
Lösung: Sonst können beide denselben alten Wert lesen und eine Erhöhung überschreiben. — Die Operation muss gegenüber anderen Threads atomar erscheinen, damit keine Aktualisierung verloren geht.
Frage 3 von 4MittelA hält L1 und wartet auf L2; B hält L2 und wartet auf L1. Welche Änderung verhindert genau diesen Wartekreis?
Lösung: Beide Threads müssen Sperren immer in derselben festgelegten Reihenfolge erwerben. — Eine einheitliche Sperrreihenfolge durchbricht die Coffman-Bedingung des zyklischen Wartens.
Frage 4 von 4SchwerEin Erzeuger und ein Verbraucher teilen einen begrenzten Puffer. Welche Lösung koordiniert beide ohne unkontrolliertes Warten?
Lösung: Eine BlockingQueue, deren put bei vollem und deren take bei leerem Puffer wartet — Eine blockierende Warteschlange kapselt atomare Zugriffe und die beiden Wartebedingungen des Puffers.

Wenn du einen neuen nebenläufigen Ablauf beurteilst, gehe in dieser Reihenfolge vor:

  1. Markiere gemeinsam genutzte veränderliche Daten und Ressourcen.
  2. Zerlege Zugriffe in ihre tatsächlichen Einzelschritte.
  3. Suche nach fehlerhaften Verschachtelungen und Race Conditions.
  4. Lege den kleinsten vollständigen kritischen Abschnitt fest.
  5. Prüfe bei mehreren Sperren alle vier Coffman-Bedingungen.
  6. Teste verschiedene Reihenfolgen sowie leere, volle und blockierte Zustände.

Passend dazu