Nebenläufigkeit in der Informatik verstehen
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.
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.
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.
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.
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:
- Wert lesen.
- Eins addieren.
- 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.
Race Condition
Eine Race Condition liegt vor, wenn das Ergebnis eines Programms von der zufälligen oder unerwarteten Reihenfolge nebenläufiger Schritte abhängt.
Der gemeinsame Zähler startet bei 0. Thread A und Thread B sollen ihn jeweils einmal erhöhen.
Eine fehlerhafte Verschachtelung kann so aussehen:
- A liest
0. - B liest ebenfalls
0. - A berechnet
1und schreibt1. - B berechnet aus seinem alten Lesewert ebenfalls
1und schreibt1.
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.
Wenn mehrere Threads denselben veränderlichen Zustand lesen und schreiben, untersuche nicht nur die Programmzeilen, sondern die möglichen Einzelschritte und ihre Verschachtelungen.
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.
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:
- Sperre erwerben.
- Gemeinsamen Wert lesen, verändern und zurückschreiben.
- 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.
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.
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.
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.
Es gibt die Sperren L1 und L2.
- Thread A erwirbt
L1. - Thread B erwirbt
L2. - A wartet auf
L2. - 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:
- Exklusiver Zugriff: Eine Ressource kann nicht gleichzeitig von allen genutzt werden.
- Halten und Warten: Ein Thread hält bereits eine Ressource und wartet auf eine weitere.
- Kein Entzug: Eine Ressource wird nicht zwangsweise entzogen, sondern vom Besitzer freigegeben.
- 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.
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.
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.
Beim Erzeuger-Verbraucher-Problem produziert ein Thread Daten, ein anderer verarbeitet sie. Eine begrenzte BlockingQueue verbindet beide:
putfügt ein Element ein und wartet kontrolliert, wenn die Queue voll ist.takeentfernt 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.
Karteikasten
Überlege zuerst selbst und drehe die Karte anschließend zum Prüfen um.
Alles auf einen Blick
- 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
Wenn du einen neuen nebenläufigen Ablauf beurteilst, gehe in dieser Reihenfolge vor:
- Markiere gemeinsam genutzte veränderliche Daten und Ressourcen.
- Zerlege Zugriffe in ihre tatsächlichen Einzelschritte.
- Suche nach fehlerhaften Verschachtelungen und Race Conditions.
- Lege den kleinsten vollständigen kritischen Abschnitt fest.
- Prüfe bei mehreren Sperren alle vier Coffman-Bedingungen.
- Teste verschiedene Reihenfolgen sowie leere, volle und blockierte Zustände.
Mit Google fortfahren