Bedingungsvariablen, Semaphore und Verklemmungen

Sperren verhindern, dass Threads einander in die Quere kommen, doch Threads müssen auch aufeinander warten. Dieses Material zeigt, wie Bedingungsvariablen und Semaphore einen Thread blockieren, bis Daten bereitstehen, ein Platz frei ist oder ein anderer Thread seine Arbeit beendet hat. Damit löst es das Erzeuger-Verbraucher-Problem und das Leser-Schreiber-Problem. Anschließend geht es um Fehler in realem nebenläufigem Code: Atomaritätsverletzungen, Reihenfolgeverletzungen und Verklemmungen sowie darum, wie sich Verklemmungen vorbeugen, vermeiden und erkennen lassen. Den Abschluss bildet die ereignisbasierte Nebenläufigkeit, bei der ein einzelner Thread eine Ereignisschleife ausführt, statt viele Threads zu verwenden. Die Beispiele verwenden C mit POSIX-Threads und werden Schritt für Schritt verfolgt.

MittelEinheit 6 von 9 in Kurs: Betriebssysteme – Prozesse, Speicher, Threads und Dateisysteme

Was du lernst

  1. Blockiere einen Thread mithilfe einer Bedingungsvariable zusammen mit einer Sperre und einer Zustandsvariable, bis eine Bedingung für gemeinsam genutzte Daten erfüllt ist, und implementiere mit diesem Muster einen korrekten beschränkten Puffer.
    • Warte blockierend auf eine Bedingung
      Erkläre, warum eine Sperre allein einen Thread nicht auf ein Ereignis warten lassen kann, und nutze Warten und Signalisieren mit einer Sperre und einer Zustandsvariable, sodass kein Wecksignal verloren geht.
    • Prüfe Bedingungen erneut und wähle zwischen Signalisierung und Broadcast
      Wende die Mesa-Semantik an, indem du die Bedingung in einer while-Schleife erneut prüfst, entscheide zwischen Signalisierung und Broadcast und erkenne dieselben Regeln in Monitoren und Java.
    • Löse das Erzeuger-Verbraucher-Problem mit Bedingungsvariablen
      Implementiere einen beschränkten Puffer, den Erzeuger- und Verbraucherthreads gemeinsam nutzen, verfolge, warum naive Varianten scheitern, und korrigiere sie mit while-Schleifen und zwei Bedingungsvariablen.
  2. Nutze den Semaphor als einzelnes Grundelement für eine Sperre, zur Signalisierung zwischen Threads und zum Zählen von Ressourcen und wende ihn auf den beschränkten Puffer und eine Lese-Schreib-Sperre an.
    • Definiere einen Semaphor und nutze ihn als Sperre
      Beschreibe, wie sich der Wert eines Semaphors bei P- und V-Operationen ändert, wähle seinen Anfangswert und nutze einen binären Semaphor für wechselseitigen Ausschluss.
    • Lege mit Semaphoren die Ausführungsreihenfolge von Threads fest und begrenze die Nebenläufigkeit
      Nutze einen Semaphor mit Anfangswert null, damit ein Thread auf einen anderen wartet, und einen zählenden Semaphor, um die Anzahl der Threads zu begrenzen, die gleichzeitig eine Ressource nutzen.
    • Implementiere einen beschränkten Puffer mit Semaphoren
      Löse das Erzeuger-Verbraucher-Problem mit zwei zählenden Semaphoren und einem Mutex und erkläre, warum die Reihenfolge ihres Erwerbs darüber entscheidet, ob der Puffer funktioniert oder eine Verklemmung entsteht.
    • Nutze Daten mithilfe einer Lese-Schreib-Sperre gemeinsam
      Erlaube gleichzeitig viele Leser oder einen Schreiber, implementiere eine solche Sperre mit Semaphoren und erkläre das Problem des Verhungerns und den Aufwand.
  3. Erkenne Atomaritäts- und Reihenfolgeverletzungen, die beiden Muster hinter fast allen in einer Studie realer Software gefundenen Nebenläufigkeitsfehlern ohne Verklemmung, und behebe beide mit dem jeweils passenden Mittel.
    • Finde und behebe Atomaritätsverletzungen
      Erkenne Code, der annimmt, dass eine Gruppe von Zugriffen ohne Unterbrechung ausgeführt wird, zeige eine Verzahnung, die dies verletzt, und schütze die ganze Gruppe mit einer Sperre.
    • Finde und behebe Reihenfolgeverletzungen
      Erkenne Code, der annimmt, dass ein Zugriff vor einem anderen erfolgt, und erzwinge diese Reihenfolge mit einer Bedingungsvariable, einem Semaphor oder Join statt durch zeitliche Abstimmung.
  4. Erkläre, wie eine Verklemmung aus vier Bedingungen entsteht, beuge ihr vor, indem du eine davon durch den Entwurf ausschließt, und beschreibe, wie Systeme Verklemmungen mit Vorabwissen vermeiden oder erkennen und beheben.
    • Erkläre, wie eine Verklemmung entsteht
      Definiere eine Verklemmung, zeige die Verzahnung, die sie verursacht, nenne die vier notwendigen Bedingungen und entscheide anhand eines Wartegraphen, ob Threads verklemmt sind.
    • Beuge Verklemmungen vor, indem du eine notwendige Bedingung ausschließt
      Entwirf Code so, dass eine der vier Bedingungen nie erfüllt sein kann: Lege eine Sperrreihenfolge fest, erwirb alle Sperren auf einmal, gib Sperren bei einem fehlgeschlagenen Trylock auf oder vermeide Sperren durch atomare Befehle.
    • Vermeide Verklemmungen durch Ablaufplanung und den Bankieralgorithmus
      Nutze Vorabwissen darüber, was Threads anfordern können, um nur Anfragen zu bewilligen, die das System in einem sicheren Zustand halten, und gehe den Bankieralgorithmus mit konkreten Zahlen durch.
    • Erkenne und behebe Verklemmungen
      Lass Verklemmungen zu, finde sie durch die Suche nach Zyklen, behebe sie durch Beenden oder Zurücksetzen eines Threads und wäge dies gegen Vorbeugung, Vermeidung und Nichtstun ab.
  5. Strukturiere ein Programm um eine Ereignisschleife, die auf Ein- und Ausgabebereitschaft reagiert, vermeide Blockieren durch asynchrone Ein-/Ausgabe und gespeicherten Zustand und wäge Ereignisse gegen Threads ab.
    • Betreibe eine Ereignisschleife mit select oder poll
      Schreibe eine Ereignisschleife mit einem Thread, die das Betriebssystem nach bereiten Deskriptoren fragt, jedes Ereignis vollständig bearbeitet und nie blockiert.
    • Vermeide Blockieren durch asynchrone Ein-/Ausgabe und gespeicherten Zustand
      Starte Ein-/Ausgabe, ohne darauf zu warten, stelle ihren Abschluss fest und bewahre den Zustand, den eine Behandlungsroutine über diese Zeit hinweg benötigt.
    • Wähle zwischen Ereignissen und Threads
      Vergleiche ereignis- und threadbasierte Entwürfe hinsichtlich Kontrolle, Speicher, Nutzung mehrerer Kerne und Wartbarkeit und erkenne die Fehler, die in Ereigniscode bestehen bleiben.

Semaphore und Verklemmungen einfach erklärt

5 Kapitel

1

Warte auf eine Änderung des gemeinsamen Zustands

Ein Thread braucht oft mehr als exklusiven Zugriff: Er braucht einen anderen Thread, der eine Arbeit beendet oder Daten bereitstellt. Bedingungsvariablen ermöglichen es ihm, währenddessen blockierend zu warten. Ihre korrekte Verwendung beruht auf einer gemeinsamen Zustandsvariable, einem Mutex und einem sorgfältigen Übergang zwischen Prüfen und Warten. Beginne mit einem Elternthread, der auf einen Kindthread wartet, und nutze dann dasselbe Muster, um Erzeuger und Verbraucher zu koordinieren, ohne CPU-Zeit zu verschwenden oder Wecksignale zu verlieren.

1.1 Zustand, ein Mutex und ein Ort zum Warten

Ein Mutex beantwortet die Frage „Wer darf jetzt auf diese Daten zugreifen?“. Er beantwortet nicht die Frage „Ist die Arbeit beendet?“. Um auf ein Ereignis zu warten, braucht ein Thread eine Aufzeichnung des Ereignisses und eine Möglichkeit, blockierend zu warten, bis sich diese Aufzeichnung ändert.

Warum aktives Warten ungünstig ist

Die Schleife while (done == 0) ; verbringt CPU-Zeit damit, wiederholt ein Flag zu prüfen. Auf einer CPU verzögert diese Arbeit den Kindthread, der das Flag setzen muss. In C ist das unsynchronisierte Lesen einer gewöhnlichen Variable, während ein anderer Thread sie beschreibt, außerdem ein Datenwettlauf. Der Compiler muss das Flag nicht bei jedem Durchlauf neu laden.

Eine Bedingungsvariable gibt Threads eine Warteschlange. POSIX stellt pthread_cond_t bereit, initialisiert mit PTHREAD_COND_INITIALIZER oder pthread_cond_init(). Die Warteschlange ist vom geprüften Zustand getrennt: done hält den Abschluss fest; die Bedingungsvariable bietet einen Ort zum blockierenden Warten.

Prüfung und Warten zusammenhalten

pthread_cond_wait(&c, &m) setzt voraus, dass der aufrufende Thread den Mutex m hält. Der Aufruf gibt diesen Mutex atomar frei und beginnt zu warten. Vor der Rückkehr erwirbt er den Mutex erneut. Das schließt die gefährliche Lücke, in der ein anderer Thread den Zustand ändern und signalisieren könnte, bevor der wartende Thread tatsächlich zu warten begonnen hat.

Hier wartet ein Elternthread auf den Abschluss eines Kindthreads. Die Namen lock, wait, signal und unlock kürzen die entsprechenden Aufrufe der Threadbibliothek ab.

/* Parent */
lock(&m);
while (done == 0)
    wait(&c, &m);
unlock(&m);

/* Child */
lock(&m);
done = 1;
signal(&c);
unlock(&m);

Anfangs ist done 0. Läuft der Elternthread zuerst, erwirbt er die Sperre, stellt fest, dass die Arbeit nicht abgeschlossen ist, und wartet. Das Warten gibt m frei, sodass der Kindthread den Mutex erwerben, done setzen und signalisieren kann. Der Elternthread erwirbt schließlich m erneut, prüft done noch einmal und fährt fort.

Läuft der Kindthread zuerst, hält er den Abschluss fest, bevor der Elternthread eintrifft. Sein Signal weckt niemanden auf, aber der später eintreffende Elternthread sieht done == 1 und überspringt das Warten. Die Lösung muss bei beiden Reihenfolgen korrekt sein.

Signale speichern keine Ereignisse

pthread_cond_signal(&c) hebt die Blockierung mindestens eines wartenden Threads auf, wenn Threads warten; POSIX erlaubt, dass mehr als einer aufwacht. Wartet niemand, hat der Aufruf keine Wirkung. pthread_cond_broadcast(&c) weckt alle derzeit wartenden Threads auf. Keiner der beiden Aufrufe hinterlegt eine Benachrichtigung für spätere Threads.

Wird done weggelassen, funktioniert das Join-Muster daher nicht: Ein Kindthread kann signalisieren, bevor sein Elternthread wartet. Danach blockiert sich der Elternthread, ohne dass noch ein Signal folgen wird. Das ist ein verlorenes Wecksignal. Wird done außerhalb des Mutex geprüft, entsteht eine weitere Lücke für ein verlorenes Wecksignal: Der Elternthread prüft und erhält falsch, der Kindthread setzt den Zustand auf wahr und signalisiert, dann beginnt der Elternthread zu warten.

Schütze die Zustandsprüfung und jede Zustandsänderung mit demselben Mutex. Halte ihn von der Prüfung bis zum Warteaufruf. Ein einfacher Signalisierungspfad ändert den Zustand und signalisiert, während er diesen Mutex noch hält. POSIX erlaubt das Signalisieren ohne gehaltenen Mutex, doch das Protokoll für den gemeinsamen Zustand muss trotzdem ein verlorenes Wecksignal verhindern.

Wichtige Erkenntnisse

  • Der Zustand hält das Ereignis fest; die Bedingungsvariable stellt eine Warteschlange bereit.
  • Rufe wait auf, während der Mutex gehalten wird; wait gibt ihn atomar frei und erwirbt ihn vor der Rückkehr erneut.
  • Schütze die Prüfung und die Zustandsänderung mit demselben Mutex.
  • Ein Signal, das gesendet wird, während niemand wartet, wird nicht gespeichert.

1.2 Prüfe nach dem Aufwachen erneut

Die Rückkehr aus einem Warteaufruf bedeutet, dass der Thread es erneut versuchen darf. Sie bedeutet nicht, dass die benötigte Bedingung noch erfüllt ist. Dieser Unterschied ist wichtig, wenn mehrere Threads um dieselben Daten konkurrieren.

Mesa- und Hoare-Semantik
SemantikWas ein Signal dem wartenden Thread gibt
MesaAusführungsbereitschaft; der wartende Thread muss den Mutex erneut erwerben und den Zustand erneut prüfen.
HoareEine sofortige Übergabe der Sperre und der Ausführung, bei der die signalisierte Bedingung bei der Übergabe erhalten bleibt.

POSIX, Java, C++-Bedingungsvariablen und Python-Bedingungsvariablen folgen dem Mesa-Verhalten. Hoares Monitore führten das stärkere Übergabemodell ein; Mesas Ansatz ist die übliche praktische Wahl.

Angenommen, C1 wartet darauf, dass ein Puffer mit einem Platz nicht mehr leer ist. Ein Erzeuger füllt ihn und signalisiert C1. C1 ist nun bereit, aber C2 kann den Mutex zuerst erwerben und das Element entnehmen. Wenn C1 schließlich den Mutex erneut erwirbt, ist der Puffer wieder leer.

lock(&m);
while (count == 0)
    wait(&filled, &m);
v = get();
unlock(&m);

Das while wiederholt die Prüfung nach jeder Rückkehr. Ein if prüft nur vor dem ersten Warten und würde C1 erlauben, get() auf einem leeren Puffer aufzurufen. Verwende auch bei einem Erzeuger und einem Verbraucher while: POSIX erlaubt außerdem spontanes Aufwachen, bei dem der Warteaufruf ohne zugehöriges Signal zurückkehrt.

Einen Thread oder alle aufwecken?

Ein Signal eignet sich, wenn die wartenden Threads dieselbe Bedingung benötigen und jeder die Änderung nutzen kann. Ein Broadcast ist nützlich, wenn mehrere fortfahren können oder wenn verschiedene wartende Threads unterschiedliche Mengen benötigen und der signalisierende Thread nicht den richtigen auswählen kann.

Betrachte einen Allokator mit 0 freien Bytes. Ta benötigt 100 Bytes und Tb 10. Ein anderer Thread gibt 50 Bytes frei. Ein Signal könnte Ta aufwecken, der weiterhin nicht fortfahren kann, sodass Tb blockiert bleibt, obwohl seine Anfrage erfüllt werden könnte. Ein Broadcast weckt beide auf. Sie erwerben den Mutex nacheinander erneut; Tb kann Speicher belegen, während Ta erneut wartet. Dies ist eine überdeckende Bedingung: Die Benachrichtigung deckt jeden wartenden Thread ab, der möglicherweise fortfahren kann.

Ein Broadcast kann zusätzliche Weckvorgänge, Kontextwechsel und Konkurrenz um den Mutex verursachen. Getrennte Bedingungsvariablen für verschiedene Prädikate können die Signalisierung gezielter machen. In einem Puffer sollten Erzeuger und Verbraucher nicht eine einzige undifferenzierte Signalwarteschlange gemeinsam nutzen.

Der Zusammenhang mit Monitoren

Ein Monitor bündelt gemeinsam genutzte Daten mit Operationen, die jeweils einen Thread zulassen, und Bedingungsvariablen zum Warten. Java-Objekte stellen eine Sperre und eine Wartemenge bereit. Innerhalb von synchronized kann ein Thread wait(), notify() oder notifyAll() aufrufen; werden diese aufgerufen, ohne den Monitor des Objekts zu besitzen, wird IllegalMonitorStateException ausgelöst.

Das Warten gibt nur den zugehörigen Mutex oder die Sperre des Java-Objekts frei. Hält ein Thread zusätzlich eine andere Sperre, bleibt diese zweite Sperre gehalten. Ein Erzeuger, der sie benötigt, kann dann möglicherweise die Bedingung nicht erfüllen. Prüfe beim Warten alle gehaltenen Sperren, nicht nur die im Aufruf genannte.

Wichtige Erkenntnisse

  • Bei Mesa-Semantik macht das Aufwecken einen Thread bereit; er konkurriert weiterhin um den Mutex.
  • Warte in einer while-Schleife, um Zustandsänderungen und spontanes Aufwachen zu behandeln.
  • Verwende Broadcast, wenn ein einzelnes Signal einen Thread blockiert lassen könnte, der fortfahren kann.
  • Für getrennte Wartebedingungen können getrennte Bedingungsvariablen verwendet werden.
  • Das Warten gibt nur die zugehörige Sperre frei.

1.3 Implementiere einen beschränkten Puffer mit zwei Warteschlangen

Erzeuger fügen einem Puffer fester Größe Elemente hinzu; Verbraucher entnehmen sie. Ein korrekter beschränkter Puffer verhindert das Überschreiben eines vollen Puffers, das Entnehmen aus einem leeren Puffer und gleichzeitige Änderungen an seinen Verwaltungsdaten. Sowohl die Anfragenwarteschlange eines Webservers als auch die Pipe zwischen grep error log.txt und wc -l passen zu diesem Muster.

Verfolge die Ringindizes

Ein Ringpuffer nutzt ein Array und zwei Indizes. fill_ptr bezeichnet den nächsten Schreibplatz; use_ptr bezeichnet den nächsten Leseplatz. Beide werden modulo MAX weitergesetzt. Ein separates count unterscheidet leer von voll, auch wenn die beiden Indizes gleich sind.

void put(int v) {
    buffer[fill_ptr] = v;
    fill_ptr = (fill_ptr + 1) % MAX;
    count++;
}
int get(void) {
    int v = buffer[use_ptr];
    use_ptr = (use_ptr + 1) % MAX;
    count--;
    return v;
}

Beginne mit Kapazität 3 und beiden Indizes bei 0. Das Einfügen von 7, 8 und 9 füllt die Plätze 0, 1 und 2. Der Schreibindex springt auf 0 zurück und count erreicht 3. Das Entnehmen gibt 7 zurück und setzt den Leseindex auf 1. Das Einfügen von 10 nutzt Platz 0 erneut. Das Array ist nun [10, 8, 9], aber die FIFO-Entnahme folgt dem Leseindex: Als Nächstes kommt 8, dann 9, dann 10.

Jedes Signal der entgegengesetzten Rolle zuordnen

Verwende einen Mutex, empty für Erzeuger, die auf Platz warten, und fill für Verbraucher, die auf ein Element warten. Initialisiere beide Bedingungsvariablen und den Mutex, bevor du Threads startest.

/* Producer: repeat for each item v */
lock(&m);
while (count == MAX)
    wait(&empty, &m);
put(v);
signal(&fill);
unlock(&m);

/* Consumer */
lock(&m);
while (count == 0)
    wait(&fill, &m);
v = get();
signal(&empty);
unlock(&m);

Die Prüfung und die Aktualisierung des Puffers bleiben in einem kritischen Abschnitt. Andernfalls könnten zwei Erzeuger beide einen freien Platz sehen und die Arbeit des jeweils anderen überschreiben. Das Warten gibt m frei, sodass die entgegengesetzte Rolle den Puffer ändern kann.

Warum eine Warteschlange ins Stocken geraten kann

Bei einer Bedingungsvariable und Signalen können Erzeuger und Verbraucher die falsche Rolle aufwecken. Bei Kapazität 1 blockieren sich C1 und C2 zunächst, weil der Puffer leer ist. P fügt ein Element ein, weckt C1 auf und wartet dann, weil der Puffer voll ist. C1 entnimmt das Element und signalisiert, aber das Signal kann C2 statt P aufwecken. C1 wartet erneut; auch C2 findet den Puffer leer vor und wartet. P bleibt blockiert, obwohl Platz verfügbar ist. Zwei Warteschlangen leiten jedes Signal an die Rolle, die nun fortfahren kann. Ein Broadcast auf einer gemeinsamen Warteschlange kann dieses Auswahlproblem vermeiden, verursacht aber zusätzliche Weckvorgänge.

Eine größere Kapazität fängt Lastspitzen ab und ermöglicht es, Erzeugungs- und Verbrauchsarbeit außerhalb des kurzen kritischen Abschnitts des Puffers zu überlappen. Die Aktualisierungen des Puffers selbst bleiben sequenziell. Bei Kapazität 1 müssen sich jedes Einfügen und jede Entnahme abwechseln.

Warte für ein sauberes Herunterfahren zunächst, bis jeder Erzeuger seine regulären Elemente abgelegt hat. Stelle dann pro Verbraucher ein reserviertes Datenendelement in die Warteschlange, etwa -1, wenn reguläre Elemente nicht negativ sind. Jeder Verbraucher beendet sich, nachdem er eines entnommen hat. Werden diese Marken gesendet, während Erzeuger noch aktiv sind, können reguläre Elemente ohne Verbraucher zurückbleiben.

Wichtige Erkenntnisse

  • Setze Ringindizes modulo Kapazität weiter; der Zähler unterscheidet leer von voll.
  • Erzeuger warten auf Platz und signalisieren Elemente; Verbraucher warten auf Elemente und signalisieren Platz.
  • Verwende while-Schleifen und einen Mutex für das Prädikat und die Pufferaktualisierung.
  • Eine gemeinsame Signalwarteschlange kann die falsche Rolle aufwecken und alle Threads blockiert lassen.
  • Warte auf den Abschluss aller Erzeuger, bevor du pro Verbraucher eine Endmarke in die Warteschlange stellst.
  • Die FIFO-Reihenfolge folgt dem Leseindex, nicht der physischen Reihenfolge im Array.
2

Zähle Erlaubnisse mit Semaphoren

Ein Semaphor erfasst Erlaubnisse, die Threads erwerben und zurückgeben. Sein Anfangswert ermöglicht es, mit demselben Grundelement exklusiven Zugriff durchzusetzen, ein Ereignis zu speichern oder einen Ressourcenpool zu begrenzen. Verfolge seinen Wert bei P- und V-Operationen und kombiniere dann die Anzahl der Erlaubnisse mit einem Mutex, um einen beschränkten Puffer zu implementieren. Ein Lese-Schreib-Entwurf bietet eine weitere Anwendung: Leser teilen sich den Zugriff, während Schreiber exklusiven Zugriff erhalten. Dabei ist auch der Preis für Fairness zu betrachten.

2.1 Wähle den Anfangswert eines Semaphors

Ein Semaphor enthält eine Ganzzahl, die durch atomare P- und V-Operationen geändert wird. Dijkstra führte sie in den 1960er-Jahren als P und V ein; POSIX nennt die Operationen sem_wait() und sem_post(). Atomar bedeutet, dass zwei Threads dieselbe Zähleroperation nicht gleichzeitig teilweise ausführen können.

Verfügbare Einheiten bestimmen den Anfangswert

sem_init(&s, 0, X) erzeugt einen unbenannten Semaphor mit dem Wert X. Das zweite Argument 0 wählt die gemeinsame Nutzung durch Threads eines Prozesses. Für die gemeinsame Nutzung zwischen Prozessen erfordert ein Argument ungleich null, dass sich der Semaphor im gemeinsam genutzten Speicher befindet.

ZweckAnfangswertBedeutung
Sperre1Ein Thread darf sofort eintreten.
Ereignisreihenfolge0Das Ereignis ist noch nicht eingetreten.
RessourcenpoolNN Einheiten können jetzt erworben werden.

Ein als Sperre verwendeter binärer Semaphor umschließt einen kritischen Abschnitt mit einer P- und einer V-Operation. Der Anfangswert 0 blockiert den ersten eintretenden Thread. Der Anfangswert 2 lässt zwei Threads eintreten und gewährleistet keinen wechselseitigen Ausschluss.

Zwei gleichwertige Zählermodelle

In Dijkstras Definition bleibt der Wert nicht negativ: Die P-Operation wartet blockierend, bis sie den Wert verringern kann, ohne unter null zu fallen, und die V-Operation fügt eine Einheit hinzu. Linux folgt dieser Konvention mit nicht negativen Werten.

Verwende für Ablaufverfolgungen von Hand das Zählermodell mit Vorzeichen: Die P-Operation verringert den Wert und blockiert, wenn der neue Wert negativ ist; die V-Operation erhöht ihn und weckt einen wartenden Thread auf, wenn einer blockiert ist. Der Betrag eines negativen Werts zählt in diesem Modell die blockiert wartenden Threads. Das ist eine Konvention zum Nachvollziehen, kein negativer Wert, den du von der Semaphorschnittstelle von Linux erwarten solltest.

Verfolge eine Sperre mit Anfangswert 1:

  1. T0 führt eine P-Operation aus: 1 wird zu 0, sodass T0 eintritt.
  2. T1 führt eine P-Operation aus: 0 wird zu −1, sodass T1 blockiert wird.
  3. T0 führt eine V-Operation aus: −1 wird zu 0, sodass T1 fortfahren kann.
  4. T1 wird fertig und führt eine V-Operation aus: 0 wird zu 1.

Das Wecksignal überträgt die vorhandene Erlaubnis an T1; eine weitere P-Operation ist nicht nötig, bevor T1 in seinen kritischen Abschnitt eintritt.

Eine V-Operation wird gespeichert

Wartet kein Thread, erhöht eine V-Operation die Anzahl der verfügbaren Einheiten. Eine spätere P-Operation kann diese Erlaubnis verbrauchen. Das ist nützlich, wenn ein Thread fertig wird, bevor der auf ihn wartende Thread zu warten begonnen hat. Das Signal einer Bedingungsvariable hat kein solches Gedächtnis.

Semaphore haben keinen Besitzer: Ein Thread kann eine P-Operation und ein anderer eine V-Operation ausführen. Ein Mutex gehört dem Thread, der ihn gesperrt hat; die Freigabe eines POSIX-Mutex mit Standardeinstellungen durch einen anderen Thread ist undefiniertes Verhalten. Der besitzerlose Semaphor unterstützt die Signalisierung, macht aber auch eine zusätzliche V-Operation schwerer zu diagnostizieren.

Ein Semaphor kann aus einem Mutex, einer Bedingungsvariable und einer nicht negativen Ganzzahl aufgebaut werden. Die P-Operation erwirbt die Sperre, wiederholt eine Schleife, solange value <= 0 gilt, wartet, verringert den Wert und gibt die Sperre frei. Die V-Operation erwirbt die Sperre, erhöht den Wert, signalisiert und gibt die Sperre frei. Der Mutex macht die Zähleroperationen atomar; die Schleife behandelt Weckvorgänge, nach denen keine Einheit verfügbar ist. Sperren mit Bedingungsvariablen und Semaphore können einander implementieren, obwohl die korrekte Implementierung einer Bedingungsvariable aus Semaphoren aufwendiger ist.

Wichtige Erkenntnisse

  • Initialisiere einen Semaphor mit der Anzahl der sofort verfügbaren Einheiten.
  • Ein Sperrsemaphor beginnt bei 1 und verbindet eine P-Operation vor dem Abschnitt mit einer V-Operation danach.
  • Das Zählermodell mit Vorzeichen nutzt negative Werte für blockiert wartende Threads; Linux-Werte bleiben nicht negativ.
  • V-Operationen werden als Erlaubnisse gespeichert, auch wenn gerade niemand wartet.
  • Ein Semaphor hat keinen Besitzer; ein Mutex schon.

2.2 Merke dir Ereignisse und begrenze die Ressourcennutzung

Dasselbe Paar aus P- und V-Operation kann ein Ereignis oder einen begrenzten Vorrat darstellen. Entscheide, was eine Erlaubnis bedeutet, bevor du den Anfangswert und die Stelle jeder Operation wählst.

Abschluss vor Verwendung sicherstellen

Wenn ein Elternthread auf einen Kindthread wartet, initialisiere finished mit 0. Der Kindthread führt nach Abschluss seiner Arbeit eine V-Operation aus. Der Elternthread führt eine P-Operation aus, bevor er das Ergebnis verwendet.

/* Child */
produce_result();
sem_post(&finished);

/* Parent */
sem_wait(&finished);
use_result();

Trifft der Elternthread zuerst ein, ändert sich der Wert mit Vorzeichen von 0 auf −1 und er blockiert sich. Die V-Operation des Kindthreads setzt den Wert auf 0 zurück und weckt ihn auf. Trifft der Kindthread zuerst ein, ändert seine V-Operation 0 zu 1; die spätere P-Operation ändert 1 zu 0, ohne zu blockieren. Ein abgeschlossenes Ereignis ermöglicht eine erfolgreiche P-Operation.

Einen Ressourcenpool zählen

Ein Pool aus vier Datenbankverbindungen nutzt einen mit 4 initialisierten Semaphor. Jeder Client führt eine P-Operation aus, bevor er eine Verbindung entleiht, und eine V-Operation, nachdem er sie zurückgegeben hat. Ein fünfter gleichzeitiger Entleiher blockiert sich. Der Semaphor zählt die Zulassungen; ein separater Mutex schützt weiterhin die gemeinsame Liste, aus der hervorgeht, welche Verbindung frei ist. Zwei zugelassene Threads dürfen nicht denselben Listeneintrag entfernen.

Drosselung wendet dieselbe Idee auf eine aufwendige Phase an. Angenommen, 100 Threads benötigen während der Verarbeitung jeweils 500 MB. Zusammen könnten sie 50.000 MB benötigen. Ein Rechner mit 8 GB hat bei dezimalen SI-Einheiten 8.000 MB. Die Zulassung von höchstens 12 Workern begrenzt diese Phase auf 6.000 MB und lässt 2.000 MB für andere Zwecke frei.

sem_wait(&processing_slots);
run_memory_heavy_phase();
sem_post(&processing_slots);

Jeder Pfad nach einer erfolgreichen P-Operation muss seine Erlaubnis zurückgeben, auch ein Fehlerpfad. Eine vergessene V-Operation verringert allmählich die Kapazität. Zwei V-Operationen erzeugen eine zusätzliche Erlaubnis und lassen mehr gleichzeitige Nutzer als vorgesehen zu.

Bei einem Rendezvous zusammentreffen

Ein Rendezvous erfordert, dass zwei Threads eintreffen, bevor einer von ihnen fortfährt. Initialisiere a_arrived und b_arrived mit 0.

/* A */
sem_post(&a_arrived);
sem_wait(&b_arrived);

/* B */
sem_post(&b_arrived);
sem_wait(&a_arrived);

Trifft A zuerst ein, hält es seine Ankunft fest und blockiert sich, um auf B zu warten. B hält seine Ankunft fest und kann die gespeicherte Erlaubnis von A verbrauchen. A kann dann die Erlaubnis von B verbrauchen. Werden beide Folgen umgekehrt, sodass vor der V-Operation gewartet wird, blockiert sich jeder Thread, bevor er etwas meldet.

Diese beiden Semaphore ermöglichen ein Treffen. Damit die Synchronisierung in wiederholten Runden erneut genutzt werden kann, muss verhindert werden, dass eine Runde Ankünfte einer anderen verbraucht; ein erfolgreicher Ablauf ist noch kein Entwurf für eine wiederverwendbare Barriere.

Ein Semaphor eignet sich gut für einen Zähler oder ein einfaches Ereignis. Eine Bedingungsvariable eignet sich für ein komplexeres Prädikat, etwa das Warten darauf, dass eine Warteschlange mehrere Elemente eines benötigten Typs enthält. In diesem Fall schützt der Mutex den Zustand, aus dem die Bedingung berechnet wird.

Wichtige Erkenntnisse

  • Initialisiere einen Semaphor zur Festlegung der Reihenfolge mit 0 und führe die V-Operation erst nach dem Ereignis aus, das er darstellt.
  • Ein zählender Semaphor begrenzt die Zulassung, schützt aber nicht die Ressourcenliste.
  • Drosselung begrenzt den gleichzeitigen Bedarf an Speicher oder einer anderen begrenzten Ressource.
  • Melde bei einem Rendezvous mit zwei Threads die eigene Ankunft, bevor du auf den anderen Thread wartest.
  • Eine zusätzliche V-Operation schafft Kapazität; eine fehlende V-Operation nimmt Kapazität weg.

2.3 Reserviere einen Platz, bevor du den Puffer sperrst

Ein Puffer mit Semaphoren trennt zwei Fragen: „Ist ein Element oder ein Platz verfügbar?“ und „Wer darf das Array jetzt aktualisieren?“. Zwei zählende Semaphore beantworten die erste Frage, ein Mutex die zweite.

Drei verschiedene Steuerungen einrichten

Initialisiere bei Kapazität MAX den Semaphor empty mit MAX, full mit 0 und den Mutex als freigegeben. Hier sind empty und full Semaphore, keine Bedingungsvariablen. Eine Erlaubnis von empty reserviert einen Platz; eine Erlaubnis von full reserviert ein Element.

/* Producer */
sem_wait(&empty);
lock(&mutex);
put(v);
unlock(&mutex);
sem_post(&full);

/* Consumer */
sem_wait(&full);
lock(&mutex);
v = get();
unlock(&mutex);
sem_post(&empty);

Der Erzeuger erwirbt Platz, bevor er auf den Ring zugreift, und gibt dann das fertige Element durch eine V-Operation auf full bekannt. Der Verbraucher erwirbt ein Element, entnimmt es unter dem Mutex und gibt durch eine V-Operation auf empty Platz zurück. Eine erfolgreiche P-Operation reserviert die Ressource bereits, daher brauchen diese Aufrufe keine Prädikatschleife wie bei einer Bedingungsvariable.

Einen Platz verfolgen

Anfangs ist empty 1 und full 0. Ein Verbraucher wartet auf full, ändert den Wert im Modell mit Vorzeichen auf −1 und blockiert sich. Der Erzeuger wartet auf empty und ändert den Wert von 1 auf 0. Er erwirbt den Mutex, fügt ein Element ein, gibt den Mutex frei und führt eine V-Operation auf full aus. Dadurch wird −1 zu 0 und die Blockierung des Verbrauchers aufgehoben.

Die nächste P-Operation des Erzeugers auf empty ändert 0 zu −1 und blockiert ihn. Der Verbraucher entnimmt das Element unter dem Mutex und führt eine V-Operation auf empty aus. Dadurch wird −1 zu 0 und die Blockierung des Erzeugers aufgehoben. Die Semaphorwerte übernehmen die Übergabe; für die Synchronisierung ist kein separates count nötig.

Bei einem größeren Puffer können mehrere Erzeuger den Freiplatzsemaphor passieren. Ohne den Mutex könnten beide denselben fill_ptr lesen und denselben Platz beschreiben. Eine Reservierung ist kein exklusiver Zugriff auf die Indizes des Arrays.

Warum Sperren vor dem Warten zu einer Verklemmung führen kann

Angenommen, der Verbraucher erwirbt mutex, bevor er auf full wartet. Ist der Puffer leer, blockiert er sich, während er den Mutex noch hält. Der Erzeuger braucht diesen Mutex, um das Element einzufügen, dessen V-Operation die Blockierung des Verbrauchers aufheben würde. Keiner kann seine Arbeit beenden.

Eine P-Operation gibt keinen Mutex frei. Der Warteaufruf einer Bedingungsvariable gibt ihren zugehörigen Mutex gezielt frei. Die Reihenfolge in diesem Puffer ist daher wichtig: zuerst reservieren, dann den Mutex erwerben, den Puffer ändern, den Mutex freigeben und anschließend die entgegengesetzte Erlaubnis bereitstellen.

Die allgemeinere Regel lautet, nicht blockierend zu warten, während man eine Sperre hält, die für die Operation benötigt wird, die die Blockierung aufheben kann. Diese Regel erklärt, warum beim Lese-Schreib-Algorithmus eine andere Anordnung sicher sein kann: Sein Schreiber benötigt die Sperre des Leserzählers nicht, um seine Ressource freizugeben.

Der Mutex kann selbst ein binärer Semaphor mit Anfangswert 1 sein. Seine Aufgabe bleibt der wechselseitige Ausschluss; die beiden anderen Semaphore zählen Ressourcen.

Wichtige Erkenntnisse

  • Initialisiere empty mit der Kapazität, full mit 0 und den Mutex als freigegeben.
  • Reserviere Platz oder ein Element, bevor du den Puffermutex erwirbst.
  • Schütze Ringindizes und Array-Aktualisierungen auch dann, wenn die Erlaubnisse korrekt gezählt werden.
  • Das Warten auf einen Semaphor gibt keinen Mutex frei.
  • Führe nach dem Einfügen eine V-Operation auf full und nach dem Entnehmen eine V-Operation auf empty aus.
  • Ein blockiert wartender Thread darf keine Sperre halten, die der Thread benötigt, der seine Blockierung aufheben kann.

2.4 Lass Leser teilen und Schreiber allein arbeiten

Strukturen mit vielen Lesezugriffen können mehrere Leser gleichzeitig zulassen, weil Lesen die Daten nicht ändert. Eine Lese-Schreib-Sperre lässt entweder eine Gruppe von Lesern oder einen Schreiber zu. Jede Änderung an den geschützten Daten erfordert Schreibzugriff.

Die Lesergruppe aufbauen

Verwende readers = 0, einen mit 1 initialisierten Semaphor lock zum Schutz dieses Zählers und einen mit 1 initialisierten Semaphor writelock zum Schutz der gemeinsam genutzten Ressource.

/* Acquire read access */
sem_wait(&lock);
readers++;
if (readers == 1)
    sem_wait(&writelock);
sem_post(&lock);

/* Release read access */
sem_wait(&lock);
readers--;
if (readers == 0)
    sem_post(&writelock);
sem_post(&lock);

Ein Schreiber wartet auf writelock, ändert die Daten und führt dann eine V-Operation auf writelock aus. Die Sperre des Zählers erwirbt er nicht. Der erste Leser erwirbt writelock für die Gruppe, und der letzte Leser gibt ihn zurück. Ein Semaphor eignet sich für diese Übergabe, weil der Thread, der die V-Operation ausführt, nicht der Thread sein muss, der ursprünglich gewartet hat.

Verfolge den Eintritt von R1: R1 erhöht readers auf 1 und erwirbt writelock. R2 erhöht den Zähler auf 2, ohne eine weitere Erlaubnis zu erwerben. Schreiber W wird nun an writelock blockiert. R1 verlässt den Abschnitt und senkt den Zähler auf 1. R2 verlässt ihn, senkt den Zähler auf 0 und führt eine V-Operation aus, sodass W allein schreiben kann.

Der erste Leser kann auf writelock warten, während er lock hält. Das erzeugt nicht die Verklemmung des beschränkten Puffers: Der aktuelle Schreiber benötigt die Sperre des Zählers nicht, um fertig zu werden und eine V-Operation auf writelock auszuführen.

Fairness ist eine eigene Eigenschaft

Dieser Entwurf bevorzugt Leser. Solange der Leserzähler positiv bleibt, kann sich ein weiterer Leser vor einem wartenden Schreiber der Gruppe anschließen. Ein ständiger Strom von Lesern, deren Zugriffe sich überlappen, kann den Schreiber unbegrenzt warten lassen: Verhungern von Schreibern.

StrategieEntscheidung über den ZugangRisiko
LeserbevorzugungNeue Leser dürfen sich aktiven Lesern anschließen.Schreiber können verhungern.
SchreiberbevorzugungWartende Schreiber verhindern den Eintritt neuer Leser.Leser können verhungern.
Fairer ZugangEintreffende Threads erhalten in fairer Reihenfolge Zugriff.Mehr Verwaltungsaufwand beim Zugang.

Der grundlegende Entwurf gewährleistet Sicherheit, begrenzt aber nicht die Wartezeit eines Schreibers. Ein höherer Leserdurchsatz ist nicht gleichbedeutend mit fairer Bedienung.

Den richtigen Modus verwenden

Eine Lesesperre kann einen Schreibvorgang nicht vor anderen Lesern schützen. Selbst die Aktualisierung eines Cache-Zählers unter Lesezugriff kann einen Datenwettlauf mit einem anderen Leser verursachen. Um in diesem grundlegenden Entwurf die Sperre aufzuwerten, gib den Lesezugriff frei, erwirb Schreibzugriff und prüfe die Bedingung erneut. Wartet ein Thread auf Schreibzugriff, während er noch als Leser gezählt wird, kann er für immer auf sich selbst warten.

POSIX stellt pthread_rwlock_t, pthread_rwlock_rdlock(), pthread_rwlock_wrlock() und pthread_rwlock_unlock() bereit. Java bietet ReentrantReadWriteLock; C++17 bietet std::shared_mutex. Diese Sperren verursachen mehr Verwaltungsaufwand als ein einfacher Mutex. Miss die Arbeitslast: Kurze Abschnitte oder häufige Schreibzugriffe können dazu führen, dass ein einfacher Mutex genauso schnell oder schneller ist.

Wichtige Erkenntnisse

  • Eine Lese-Schreib-Sperre erlaubt viele Leser oder einen Schreiber mit exklusivem Zugriff.
  • Der erste Leser erwirbt die Erlaubnis für die Gruppe; der letzte Leser gibt sie zurück.
  • Der Leserzähler braucht eine eigene Sperre.
  • Leserbevorzugung kann einen wartenden Schreiber verhungern lassen.
  • Gib den Lesezugriff frei, bevor du Schreibzugriff anforderst, und prüfe dann die Daten erneut.
3

Finde die Annahme über die Reihenfolge hinter einem Fehler

Bei vielen Nebenläufigkeitsfehlern läuft das Programm mit falschen Daten weiter. Zwei wiederkehrende Ursachen sind, mehrere Zugriffe als eine unteilbare Handlung zu behandeln und anzunehmen, dass die Arbeit eines Threads vor der eines anderen erfolgt. Das Verfolgen einiger sorgfältig ausgewählter Zugriffe legt die verborgene Annahme oft offen. Die Korrektur hängt von dieser Annahme ab: Schütze eine ganze Operation, wenn sie zusammenbleiben muss, und lege eine ausdrückliche Abhängigkeit fest, wenn eine Operation zuerst geschehen muss.

3.1 Schütze die gesamte Prüfung und Handlung

Eine Atomaritätsverletzung tritt auf, wenn Code darauf angewiesen ist, dass eine Gruppe von Zugriffen unteilbar ist, aber ein anderer Thread dazwischen handeln kann. Jeder einzelne Zugriff kann sinnvoll aussehen, während die kombinierte Operation scheitert.

Die Lücke nach der Prüfung finden

Betrachte einen gemeinsam genutzten Protokollpuffer:

/* T1 */
if (log->buf != NULL)
    write_out(log->buf);

/* T2 */
log->buf = NULL;

T1 kann einen Zeiger prüfen, der nicht null ist, pausieren und fortfahren, nachdem T2 ihn zurückgesetzt hat. T1 übergibt dann null an write_out(). Die beabsichtigte Garantie war nicht nur eine gültige Prüfung, sondern auch, dass der geprüfte Wert bis zum Aufruf nutzbar blieb.

Schütze Prüfung und Verwendung zusammen und die konkurrierende Zuweisung mit demselben Mutex:

/* T1 */
lock(&m);
if (log->buf != NULL)
    write_out(log->buf);
unlock(&m);

/* T2 */
lock(&m);
log->buf = NULL;
unlock(&m);

T2 läuft nun entweder vor oder nach der gesamten Operation von T1. Eine eigene Sperre für jeden Thread hilft nicht: Beide könnten ihre getrennten Sperren gleichzeitig halten. Wird nur T1 gesperrt, kann T2 weiterhin in der Lücke handeln.

Eine gültige Prüfung kann dennoch zu einem ungültigen Kontostand führen

Beginne mit einem Kontostand von 50. Zwei Threads wollen jeweils 30 abheben, aber nur, wenn genug vorhanden ist. Beide können 50 lesen und balance >= 30 bestehen, bevor einer handelt. Werden die Abhebungen dann ausgeführt, kann der Kontostand −10 betragen. Werden Prüfung und Subtraktion zusammen geschützt, bleiben nach der ersten Abhebung 20; der nächste Thread sieht 20 und lehnt ab.

Auch atomares Laden und atomares Subtrahieren machen dieses Paar nicht zu einer Transaktion. Ein anderer Thread kann zwischen der atomaren Prüfung und der atomaren Änderung handeln. Eine Sperre um die gesamte Entscheidung und Änderung setzt die erforderliche Grenze durch.

Dasselbe Problem tritt bei count++ auf: Laden, Addieren und Speichern sind getrennte Schritte. Zwei Threads können denselben alten Wert laden und denselben erhöhten Wert speichern, sodass ein Update verloren geht. Eine Sperre um die Erhöhung oder ein atomares Fetch-and-Add führt das erforderliche Update sicher aus.

Ergebnisse nutzen, ohne zu stark zu verallgemeinern

Lu und Kollegen untersuchten in einer Studie von 2008 105 behobene Nebenläufigkeitsfehler in MySQL, Apache, Mozilla und OpenOffice. Davon waren 74 Fehler ohne Verklemmung und 31 Verklemmungen. Atomaritäts- und Reihenfolgeverletzungen machten 97 % der untersuchten Fehler ohne Verklemmung aus. Dies sind Ergebnisse für die untersuchte Stichprobe, kein für jede Codebasis garantierter Prozentsatz.

Etwa 92 % der untersuchten Fehler ließen sich auslösen, indem die Reihenfolge von höchstens vier Speicherzugriffen kontrolliert wurde. Eine kleine Tabelle mit verzahnten Zugriffen kann daher aufschlussreicher sein als Tausende erfolgreicher gewöhnlicher Läufe. ThreadSanitizer hilft, ausgeführte Datenwettläufe zu finden, aber ein logischer Atomaritätsfehler kann bestehen bleiben, selbst wenn jeder einzelne Zugriff synchronisiert ist. Teste die Invariante der gesamten Operation, etwa „Eine erfolgreiche Abhebung überzieht den Kontostand nie.“

Wichtige Erkenntnisse

  • Wähle den kritischen Abschnitt so, dass er die ganze Operation umfasst, die unteilbar sein muss.
  • Jeder konkurrierende Zugriff muss demselben Sperrprotokoll folgen.
  • Atomare Einzelzugriffe können dennoch eine Lücke zwischen Prüfung und Handlung lassen.
  • Verfolge eine kurze schädliche Verzahnung, statt dich nur auf wiederholte erfolgreiche Läufe zu verlassen.

3.2 Sorge dafür, dass eine Operation vor einer anderen geschieht

Eine Reihenfolgeverletzung bedeutet, dass Operation A vor Operation B geschehen muss, das Programm aber keine Synchronisierung bereitstellt, die dies garantiert. Wechselseitiger Ausschluss allein kann nicht festlegen, welche Operation zuerst kommt.

Ein Thread kann sofort laufen

Angenommen, ein erzeugender Thread speichert einen neu erzeugten Worker-Datensatz in einem gemeinsam genutzten Zeiger, während der Worker zu Beginn diesen Zeiger verwendet:

/* Creator */
worker = thread_create(worker_main);

/* New worker */
use(worker->state);

Der Worker kann laufen, bevor thread_create() zurückkehrt und bevor die Zuweisung abgeschlossen ist. Die Reihenfolge im Quellcode des erzeugenden Threads sagt nichts darüber aus, wann der neue Thread CPU-Zeit erhält. Ein erfolgreicher Lauf bedeutet lediglich, dass der erzeugende Thread diesen Wettlauf zufällig gewonnen hat.

Bereitschaft nach der Initialisierung bekannt geben

Füge ready = 0, den Mutex m und die Bedingungsvariable c hinzu. Der erzeugende Thread hält den Mutex, während er den Zeiger zuweist und die Bereitschaft bekannt gibt:

/* Creator */
lock(&m);
worker = thread_create(worker_main);
ready = 1;
signal(&c);
unlock(&m);

/* Worker, before using worker */
lock(&m);
while (ready == 0)
    wait(&c, &m);
unlock(&m);
use(worker->state);

Erreicht der Worker den Mutex zuerst, kann er die Bereitschaftsprüfung erst passieren, wenn der erzeugende Thread den Datensatz bekannt gegeben hat. Trifft er später ein, hält ready fest, dass die Initialisierung abgeschlossen ist. Halte den Datensatz am Leben, solange der Worker ihn verwendet, und synchronisiere spätere Änderungen.

Ein mit 0 initialisierter Semaphor bietet eine kürzere Übergabe: Der erzeugende Thread führt nach der Zuweisung des Zeigers eine V-Operation aus, und der Worker wartet vor dessen Verwendung. Existiert ein Ergebnis erst nach dem Abschluss eines Threads, stellt Join auf diesen Thread vor dem Lesen des Ergebnisses die erforderliche Reihenfolge her. Ebenso darf ein Objekt erst freigegeben werden, wenn seine Nutzer fertig sind, zum Beispiel mithilfe von Join oder einem korrekt synchronisierten Referenzzähler.

Warum Änderungen am zeitlichen Ablauf das Problem nicht beheben

Das Hinzufügen von sleep(1) oder sched_yield() ändert nur die Wahrscheinlichkeit des gewünschten Ablaufs. Ein langsameres oder ausgelastetes System kann den Verbraucher weiterhin zu früh fortfahren lassen. Ein Mutex um jede Operation verhindert Überlappungen, erlaubt aber weiterhin, dass die Verwendung den Mutex vor der Initialisierung erwirbt.

Verborgene AnnahmeFehlertypBenötigte Garantie
„Zwischen dieser Prüfung und der Handlung ändert sich nichts.“AtomaritätsverletzungEine geschützte Operation.
„Die Initialisierung ist vor dieser Verwendung abgeschlossen.“ReihenfolgeverletzungEine ausdrückliche Abhängigkeit vom Abschluss oder der Bereitschaft.

In der Studie von Lu und Kollegen war etwa ein Drittel der Fehler ohne Verklemmung auf Reihenfolgeverletzungen zurückzuführen, und etwa 73 % der Fehler ohne Verklemmung wurden nicht allein durch Hinzufügen oder Ändern von Sperren behoben. Bevor du ein Grundelement wählst, benenne die Operation, die abgeschlossen sein muss, und die Operation, die warten muss. Diese Abhängigkeit bestimmt, wohin die V-Operation, das Signal oder Join gehört.

Wichtige Erkenntnisse

  • Eine Reihenfolgeverletzung nimmt an, dass eine frühere Handlung abgeschlossen ist, ohne diese Reihenfolge durchzusetzen.
  • Gib die Bereitschaft erst nach der Initialisierung bekannt und warte, bevor du das Ergebnis verwendest.
  • Eine Sperre verhindert Überlappungen, bestimmt aber nicht, wer zuerst handelt.
  • Blockieren für eine bestimmte Zeit und freiwillige Prozessorabgabe ändern den zeitlichen Ablauf, ohne eine Abhängigkeit zu garantieren.
  • Halte gemeinsam genutzte Objekte am Leben, bis alle Nutzer fertig sind.
4

Analysiere Verklemmungen und Fortschritt

Eine Verklemmung verwandelt eine Gruppe gewöhnlicher Wartevorgänge in einen dauerhaften Stillstand: Jeder Thread benötigt etwas, das ein anderer blockierter Thread hält. Ein Abhängigkeitsgraph macht diesen Zyklus sichtbar. Die vier notwendigen Bedingungen zeigen Möglichkeiten zur Vorbeugung; im Voraus angegebene Ressourcenansprüche ermöglichen Vermeidung; und eine Erkennung kann eine Verklemmung nach ihrer Entstehung finden. Vergleiche diese Strategien anhand des benötigten Wissens, der verlorenen Nebenläufigkeit und der Arbeit, die bei der Behebung verloren gehen kann.

4.1 Verfolge den Zyklus wartender Threads

Eine Gruppe von Threads ist verklemmt, wenn jeder auf etwas wartet, das nur ein anderer Thread der Gruppe bereitstellen kann. Keiner kann fortfahren und die Handlung ausführen, die die anderen freigibt. Das Programm kann bei vielen Abläufen funktionieren, bevor eine unglückliche Reihenfolge den Zyklus offenlegt.

Den Fehler mit zwei Sperren aufbauen

T1 erwirbt L1 und fordert als Nächstes L2 an. T2 erwirbt L2 und fordert als Nächstes L1 an. Erwirbt T1 zuerst L1 und dann T2 L2, blockieren beide beim zweiten Erwerb. Jeder Thread hält genau die Ressource, die der andere benötigt.

Dies kann sich in gewöhnlichen Funktionen verbergen. Wenn transfer(a, b) zuerst a und dann b sperrt, können gleichzeitige Aufrufe von transfer(x, y) und transfer(y, x) die Kontosperren in entgegengesetzter Reihenfolge erwerben. Kapselung macht einen Aufruf bequem, kann aber die dabei erworbenen Sperren verbergen.

Alle vier Bedingungen erkennen
Notwendige BedingungBedeutung im Sperrenbeispiel
Wechselseitiger AusschlussNur ein Thread hält eine bestimmte Sperre.
Belegen und AnfordernJeder hält eine Sperre, während er auf eine andere wartet.
NichtentziehbarkeitEine gehaltene Sperre wird nicht zwangsweise entzogen.
Zyklisches WartenJeder wartet auf die vom nächsten Thread gehaltene Sperre.

Alle vier Bedingungen sind notwendig. Wird eine davon durch den Entwurf ausgeschlossen, werden Ressourcenverklemmungen verhindert. Exklusive Sperren und verschachtelter Erwerb allein belegen nicht, dass ein bestimmter Ablauf einen Zyklus erreicht hat.

Einen Wartegraphen lesen

Zeichne pro Thread einen Knoten. Zeichne T → U, wenn T auf eine Ressource wartet, die U hält. Bei Ressourcen mit jeweils einer Instanz, etwa exklusiven Sperren, zeigt ein Zyklus eine Verklemmung an.

Hält T1 die Ressource A und wartet auf B, hält T2 B und wartet auf C und hält T3 C und wartet auf A, lautet der Graph T1 → T2 → T3 → T1. Alle drei stecken fest. Fordert T3 stattdessen eine freie Sperre D an, kann er fertig werden und C freigeben, sodass T2 und danach T1 fortfahren können. Eine Kette ist kein Zyklus.

Haben Ressourcentypen mehrere austauschbare Instanzen, reicht ein Zyklus allein nicht aus: Eine weitere Instanz könnte verfügbar werden. Nutze Ressourcenanzahlen und ausstehende Anfragen, um diesen Fall zu beurteilen.

Speisende Philosophen und verwandte Fehler

Fünf Philosophen teilen sich fünf kreisförmig angeordnete Gabeln. Jeder benötigt beide benachbarten Gabeln. Nehmen alle zuerst ihre linke Gabel auf, können alle für immer auf ihre rechte Gabel warten. Wird die Erwerbsreihenfolge eines Philosophen umgekehrt, wird der Zyklus unterbrochen. Auch eine Begrenzung auf vier zugelassene Philosophen verhindert, dass alle fünf eine Gabel halten; mindestens ein zugelassener Philosoph kann beide Gabeln erhalten.

Verhungern bedeutet, dass ein Thread unbegrenzt wartet, während andere Fortschritte machen. Livelock bedeutet, dass Threads laufen und reagieren, aber wiederholt keinen Fortschritt erzielen. Eine verklemmte Gruppe ist blockiert. Ein Thread kann sich sogar selbst verklemmen, indem er einen gewöhnlichen, nicht rekursiven Mutex sperrt, den er bereits hält. Auch Semaphore und Bedingungsvariablen können Abhängigkeiten erzeugen: Ein wartender Thread, der eine vom signalisierenden Thread benötigte Sperre hält, kann einen Zyklus schließen, ohne ausdrücklich eine zweite Sperre zu erwerben.

Wichtige Erkenntnisse

  • Eine Verklemmung ist ein dauerhafter Abhängigkeitszyklus zwischen blockierten Threads.
  • Alle vier Bedingungen müssen erfüllt sein: wechselseitiger Ausschluss, Belegen und Anfordern, Nichtentziehbarkeit und zyklisches Warten.
  • Bei Ressourcen mit jeweils einer Instanz zeigt ein Wartezyklus eine Verklemmung an.
  • Ein Zyklus bei Ressourcen mit mehreren Instanzen erfordert weitere Analyse.
  • Verhungern, Livelock und Verklemmung beschreiben unterschiedliche Formen fehlenden Fortschritts.
  • Verborgenes Sperren innerhalb von Funktionen kann die Erwerbsreihenfolge umkehren.

4.2 Schließe eine Bedingung aus, die Verklemmungen ermöglicht

Vorbeugung ändert den Entwurf so, dass eine notwendige Bedingung für Verklemmungen nicht entstehen kann. Eine Sperrreihenfolge ist oft der klarste Ansatz, weil sie die zyklische Abhängigkeit direkt angeht.

Eine Reihenfolge für alle Sperren festlegen

Weise Sperren eindeutige Schlüssel zu und erwirb Sperren mit niedrigeren Schlüsseln immer vor denen mit höheren. Sortiere bei Kontoübertragungen die Kontokennungen vor dem Sperren. Sowohl transfer(x, y) als auch transfer(y, x) erwerben dann zuerst dasselbe Konto. Bezeichnen die Kennungen dasselbe Konto, erwirb seine Sperre einmal.

Ein Zyklus würde erfordern, dass die Schlüssel im Kreis immer weiter steigen und zum Ausgangsschlüssel zurückkehren, was unmöglich ist. Jeder Erwerbspfad muss der Regel folgen. Eine einzige Hilfsfunktion, die die Reihenfolge umkehrt, stellt das Risiko wieder her. Das Linux-Werkzeug lockdep verfolgt beobachtete Sperrabhängigkeiten und meldet möglicherweise unsichere Reihenfolgen.

Alles oder nichts erwerben

Ein strenger Weg, Belegen und Anfordern auszuschließen, besteht darin, einem Thread seine vollständige Ressourcenmenge in einem Schritt oder gar nichts zuzuteilen. Er hält nie eine Teilmenge, während er auf den Rest wartet. Dies setzt voraus, dass die benötigten Ressourcen im Voraus bekannt sind, und kann die Nebenläufigkeit verringern.

Ein verwandtes Protokoll serialisiert Erwerbsfolgen mit einer globalen Vorbeugungssperre:

lock(&prevention);
lock(&L1);
lock(&L2);
unlock(&prevention);
/* use L1 and L2, then release them */

Alle Threads müssen ihre vollständige Menge unter dieser globalen Sperre erwerben, und die Freigabe von Ressourcen darf deren Erwerb nicht erfordern. Ein Thread kann weiterhin L1 halten, während er auf L2 wartet, sodass dies Belegen und Anfordern nicht wörtlich ausschließt. Es verhindert, dass andere Threads einen konkurrierenden teilweisen Erwerb beginnen. Kein Halter kann sich mitten in einer zweiten konkurrierenden Erwerbsfolge befinden. Der Preis besteht darin, Sperren früher zu halten und den Erwerb zu serialisieren.

Aufgeben und erneut versuchen

pthread_mutex_trylock() kehrt sofort mit EBUSY zurück, wenn eine Sperre nicht verfügbar ist. Ein Thread kann L1 erwerben, L2 versuchen und L1 freigeben, wenn der Versuch scheitert. Er muss auch andere teilweise erworbene Ressourcen freigeben, bevor er es erneut versucht.

Zwei Threads können wiederholt kollidieren, zurückweichen und es gleichzeitig erneut versuchen. Das ist ein Livelock: Beide werden ausgeführt, aber keiner wird fertig. Zufälliges Backoff verringert wiederholte Kollisionen; es macht die Wiederholungsschleife nicht zu einer Fairnessgarantie.

Eine Sperre durch eine atomare Operation ersetzen

Compare-and-Swap (CAS) ändert eine Speicherstelle nur, wenn sie noch einem erwarteten Wert entspricht. Gedanklich liest eine Wiederholungsschleife für atomares Addieren den alten Wert, versucht ihn durch den alten Wert plus die Erhöhung zu ersetzen und versucht es erneut, wenn ein anderer Thread ihn zuerst geändert hat. Tatsächliche C-Implementierungen nutzen atomare Typen und Operationen sowohl für das Lesen als auch für die Aktualisierung.

Wird der Erwerb einer Sperre entfernt, kann bei dieser Operation keine Sperrenverklemmung entstehen. Ein sperrfreier Algorithmus garantiert systemweiten Fortschritt, aber ein Thread kann bei seinen wiederholten Versuchen immer wieder unterliegen. Allgemeine sperrfreie Strukturen erfordern außerdem einen sorgfältigen Entwurf der Speicherlebensdauer.

TechnikWichtigste Abwägung
Erwerb in fester ReihenfolgeJeder Codepfad muss die Reihenfolge einhalten.
Gleichzeitiger Erwerb aller RessourcenRessourcen müssen bekannt sein und können ungenutzt bleiben.
Trylock und erneuter VersuchWiederholungsaufwand und Livelock müssen beachtet werden.
Sperrfreie AktualisierungAnspruchsvollere Prüfung der Korrektheit.

Wichtige Erkenntnisse

  • Eine gemeinsame Sperrreihenfolge verhindert zyklisches Warten nur, wenn alle Pfade ihr folgen.
  • Ein echter Alles-oder-nichts-Erwerb schließt Belegen und Anfordern aus.
  • Eine globale Erwerbssperre serialisiert vollständige Erwerbsfolgen.
  • Gib nach einem fehlgeschlagenen Trylock teilweise erworbene Ressourcen frei, bevor du es erneut versuchst.
  • Zufälliges Backoff verringert Kollisionen bei Livelock.
  • Sperrfreier Fortschritt für das System garantiert keinen Fortschritt für jeden Thread.

4.3 Prüfe die Sicherheit einer Ressourcenanfrage

Vermeidung prüft, ob eine vorgeschlagene Anfrage eine Möglichkeit offenlässt, damit jeder Thread fertig wird. Sie erlaubt die üblichen Muster der Ressourcennutzung, verzögert aber Anfragen, die die verbleibenden sicheren Abschlussreihenfolgen zerstören.

Sicher bedeutet eine mögliche Reihenfolge

Eine sichere Folge ordnet Threads so, dass jeder seinen verbleibenden maximalen Bedarf aus den derzeit freien Ressourcen und denen decken kann, die frühere Threads freigeben. Einem unsicheren Zustand fehlt eine solche garantierte Reihenfolge. Er muss noch nicht verklemmt sein: Threads könnten weniger als ihr Maximum anfordern oder Ressourcen frühzeitig freigeben.

Auch Ablaufplanung kann gefährliche Überlappungen vermeiden, indem sie sicherstellt, dass zwei konkurrierende Aufgaben ihre Phasen der Ressourcennutzung nicht gleichzeitig beginnen. Es reicht nicht aus, beide einfach auf eine CPU zu legen, weil sie weiterhin verdrängt werden und sich verzahnen können, während sie Ressourcen halten. Erforderlich ist die Garantie, dass sich die konkurrierenden Phasen nicht überlappen.

Die Tabellen des Bankieralgorithmus aufstellen

Erfasse für jeden Ressourcentyp die Verfügbarkeit Available. Erfasse für jeden Thread das Maximum Max, die Belegung Allocation und Need = Max - Allocation. Vergleiche Vektoren Komponente für Komponente, niemals anhand ihrer aufsummierten Werte.

Verwende die Gesamtmengen (7, 5, 6):

ThreadBelegungMaximumRestbedarf
T0(1, 1, 0)(5, 3, 2)(4, 2, 2)
T1(2, 0, 1)(3, 2, 2)(1, 2, 1)
T2(1, 1, 2)(2, 2, 4)(1, 1, 2)
T3(0, 1, 1)(4, 2, 3)(4, 1, 2)

Die Belegungen ergeben zusammen (4, 3, 4), sodass Available = (3, 2, 2) verbleibt.

Die Sicherheitsprüfung durchgehen

Setze Work auf die Verfügbarkeit. Wähle einen noch nicht abgeschlossenen Thread, dessen Restbedarf zum Arbeitsvektor passt, simuliere seinen Abschluss und addiere seine Belegung zum Arbeitsvektor.

  1. T1 passt zu (3, 2, 2). Die Rückgabe von (2, 0, 1) ergibt den Arbeitsvektor (5, 2, 3).
  2. T0 passt. Die Rückgabe von (1, 1, 0) ergibt (6, 3, 3).
  3. T2 passt. Die Rückgabe von (1, 1, 2) ergibt (7, 4, 5).
  4. T3 passt. Die Rückgabe von (0, 1, 1) ergibt (7, 5, 6).

Jeder Thread wird in dieser Simulation fertig, also ist der Zustand sicher. Der Arbeitsvektor steigt um die Belegung, nicht um das Maximum: Die zusätzlich für den Abschluss vorübergehend zugeteilten Ressourcen werden ebenfalls zurückgegeben, sodass der Nettozuwachs dem entspricht, was der Thread ursprünglich hielt.

Probeweise bewilligen und erneut prüfen

Eine Anfrage muss sowohl zum Restbedarf des Threads als auch zur Verfügbarkeit passen. Subtrahiere sie dann von der Verfügbarkeit, addiere sie zur Belegung, subtrahiere sie vom Restbedarf und führe die Sicherheitsprüfung für diesen vorläufigen Zustand aus.

Ausgehend von der ursprünglichen Tabelle hinterlässt eine Anfrage von T2 nach (1, 0, 1) die Verfügbarkeit (2, 2, 1). T1 kann weiterhin zuerst laufen, gefolgt von T0, T2 und T3, also ist der Zustand sicher.

Fordert stattdessen T3 (2, 1, 1) an, bleibt die Verfügbarkeit (1, 1, 1). Der verbleibende Restbedarf von T3 ist (2, 0, 1). Kein Thread passt: T0 benötigt 4 von A, T1 benötigt 2 von B, T2 benötigt 2 von C und T3 benötigt 2 von A. Verzögere diese Anfrage trotz der derzeit freien Ressourcen.

Jede spätere Anfrage braucht dieselbe Prüfung. Vermeidung benötigt angegebene maximale Bedarfe und ein bekanntes Ressourcenmodell und eignet sich daher eher für vorhersehbare spezialisierte Systeme als für allgemeine Anwendungslasten.

Wichtige Erkenntnisse

  • Need entspricht dem maximalen Anspruch minus der aktuellen Belegung.
  • Eine sichere Folge muss jeden Ressourcentyp einzeln berücksichtigen.
  • Addiere nach dem simulierten Abschluss Allocation zu Work.
  • Eine Anfrage muss zu Need und Available passen und einen sicheren Zustand hinterlassen.
  • Unsicher bedeutet, dass keine Abschlussfolge garantiert ist; es bedeutet nicht, dass bereits eine Verklemmung entstanden ist.

4.4 Erkenne eine Verklemmung und wähle ihre Behebung

Ein System kann Verklemmungen entstehen lassen, erkennen und beheben. Es nimmt dabei gelegentlichen Arbeitsverlust in Kauf und schränkt gewöhnliche Ressourcenanfragen dafür weniger ein. Erkennung ist nur nützlich, wenn eine praktikable Maßnahme zur Behebung existiert.

Zyklen blockierter Threads finden

Führe bei Ressourcen mit jeweils einer Instanz einen Wartegraphen und suche nach einem Zyklus, zum Beispiel mit Tiefensuche. Angenommen, die Kanten lauten T1 → T2, T2 → T3, T3 → T1 und T4 → T2. T1, T2 und T3 bilden den Zyklus. T4 ist dahinter blockiert; die eigene Ausführung von T4 kann keine Ressource freigeben, die diesen Zyklus auflöst.

Bei mehreren Instanzen reicht ein Zyklus nicht aus. Verwende die Verfügbarkeit, die Belegung und die aktuell ausstehende Anfrage jedes Threads. Beginne den Arbeitsvektor mit der Verfügbarkeit. Finde wiederholt eine passende Anfrage, simuliere den Abschluss und addiere die Belegung dieses Threads zum Arbeitsvektor. Anders als bei der Vermeidung nutzt diese Prüfung das, worauf Threads jetzt warten, nicht ihren möglichen zukünftigen Höchstbedarf.

Das übliche Verfahren zur Erkennung von Ressourcenverklemmungen stellt zunächst Threads ohne gehaltene Ressourcen zurück: Sie können keine gehaltenen Ressourcen zu einem Ressourcenzyklus beitragen. Unter den Threads, die Ressourcen halten, sind diejenigen, die bei der Prüfung nicht abgeschlossen werden, gemäß dem Modell verklemmt. Andere Threads können trotzdem am Fortschritt gehindert sein, weil sie von dieser Gruppe abhängen. Die Trennung des Zyklus von den dahinter blockierten abhängigen Threads hilft, ein geeignetes Opfer für die Behebung auszuwählen.

Beheben, ohne den gemeinsamen Zustand zu beschädigen

Ein System kann einen oder mehrere Prozesse beenden, Ressourcen durch Zurücksetzen auf einen Prüfpunkt zurückgewinnen oder neu starten. Wähle Opfer nach Behebungsaufwand, Priorität, bereits geleisteter Arbeit und gehaltenen Ressourcen. Prüfe nach dem Entfernen eines Opfers erneut: Mehrere unabhängige Zyklen können mehr als eine Maßnahme erfordern.

Einen beliebigen Thread zu beenden reicht nicht aus, wenn er einen Mutex gesperrt oder eine gemeinsame Struktur nur halb geändert zurücklässt. Die Behebung muss Ressourcen freigeben oder rekonstruieren und einen gültigen Zustand wiederherstellen. Transaktionssysteme können eine verklemmte Transaktion abbrechen, weil das Zurücksetzen zu ihrem Entwurf gehört. Eine gewöhnliche Anwendung benötigt stattdessen oft einen kontrollierten Neustart des Prozesses.

Zeitüberschreitungen sorgfältig deuten

pthread_mutex_timedlock() kann nach Ablauf einer Frist ETIMEDOUT zurückgeben. Ein Watchdog kann ein Gerät zurücksetzen, das keinen Fortschritt mehr meldet. Beide erkennen übermäßiges Warten; langes Warten kann aber auch durch langsame Ein-/Ausgabe oder Ablaufplanung entstehen. Eine Zeitüberschreitung ist ein Anlass zur Untersuchung oder Behebung, keine eindeutige Diagnose eines Zyklus.

StrategieWas sie benötigtWichtigster Aufwand
VorbeugungEine Entwurfsregel, die eine notwendige Bedingung ausschließt.Weniger Flexibilität oder Nebenläufigkeit.
VermeidungZukünftige maximale Bedarfe und eine Sicherheitsprüfung.Verzögerte Anfragen und Verwaltungsaufwand.
Erkennung und BehebungEine Erkennung und einen gültigen Weg zur Behebung.Prüfaufwand und möglicher Arbeitsverlust.
Seltene Verklemmungen ignorierenEinen hinnehmbaren Ausfall und Neustartaufwand.Gelegentliche Unterbrechung.

Linux und Windows finden und beheben im Allgemeinen keine Zyklen zwischen den eigenen Sperren einer Anwendung. Das Programm muss seine gewählte Strategie implementieren. Wird bei der Behebung wiederholt dieselbe Arbeit als Opfer ausgewählt, kann diese Arbeit verhungern, obwohl jede einzelne Verklemmung aufgelöst wird.

Wichtige Erkenntnisse

  • Erkennung findet eine bestehende Verklemmung; Vorbeugung verhindert ihre Entstehung.
  • Die Erkennung bei mehreren Instanzen nutzt aktuelle Anfragen, nicht den maximalen Restbedarf Need.
  • Unterscheide einen Ressourcenzyklus von Threads, die dahinter blockiert sind.
  • Die Behebung muss gültige Ressourcen und einen gültigen gemeinsamen Zustand wiederherstellen, nicht nur einen Thread stoppen.
  • Eine Zeitüberschreitung kann auch auf eine langsame Operation hinweisen.
5

Halte eine Ereignisschleife reaktionsfähig

Nebenläufigkeit kann dadurch entstehen, dass ein Thread viele Aufgaben bedient, während sich deren Wartezeiten auf Ein-/Ausgabe überlappen. Eine Ereignisschleife wählt eine bereite Aufgabe und führt eine kurze Behandlungsroutine aus; danach wartet sie auf das nächste Ereignis. Dadurch sind zwischen synchronen Behandlungsroutinen dieser Schleife keine Sperren nötig, aber jede blockierende Operation verzögert alle anderen. Bereitschaftsprüfungen, asynchrone Ein-/Ausgabe und gespeicherte Fortsetzungen halten die Schleife in Gang; mehrere Kerne und gemeinsamer Zustand erfordern weiterhin Sorgfalt.

5.1 Behandle bereite Deskriptoren ohne Blockieren

Eine Ereignisschleife wartet auf Ereignisse und führt kurze Behandlungsroutinen auf einem Thread aus. Das Programm wählt ausdrücklich, welches bereite Ereignis als Nächstes behandelt wird. Node.js und nginx nutzen ereignisbasierte Entwürfe, um viele Verbindungen zu bedienen, ohne jeder Verbindung einen eigenen Thread zuzuweisen.

while (1) {
    events = get_events();
    /* handle each ready event, then return to the loop */
}

Synchrone Behandlungsroutinen derselben Schleife laufen einzeln nacheinander. Die gewöhnlichen Befehle einer Behandlungsroutine werden nicht von einer anderen Routine derselben Schleife unterbrochen. Die Schleife ist weiterhin darauf angewiesen, dass die Routinen zügig zurückkehren.

select-Mengen aufbauen und prüfen

select() akzeptiert Mengen von Deskriptoren, die auf Lese-, Schreib- und Ausnahmebedingungen überwacht werden sollen, sowie eine Wartezeit. Sein Argument nfds ist um eins größer als der höchste überwachte Deskriptor. Die Überwachung der Deskriptoren 3, 5 und 9 benötigt daher nfds = 10, nicht 3.

FD_ZERO(&readset);
FD_SET(3, &readset);
FD_SET(5, &readset);
FD_SET(9, &readset);
int n = select(10, &readset, NULL, NULL, NULL);
/* If n > 0, test each watched descriptor with FD_ISSET. */

Ist das Timeout-Argument ein Nullzeiger, wartet der Aufruf auf Bereitschaft; bei einer Wartezeit von null prüft er und kehrt sofort zurück. select() überschreibt die Mengen mit den bereiten Deskriptoren, baue sie daher vor jedem Aufruf erneut auf. Ein positiver Rückgabewert zählt die gesetzten Bits in den zurückgegebenen Mengen; ein in zwei Mengen bereiter Deskriptor zählt doppelt. Null bedeutet Zeitüberschreitung; −1 bedeutet Fehler.

Bereitschaft ist keine Datenübertragung. Das Programm ruft weiterhin read() oder recv() auf. Das Ende der Eingabe kann als Lesebereitschaft erscheinen; gibt ein Leseaufruf 0 zurück, hat die Gegenstelle ihre Sendeseite geschlossen. Behandle dieses Ergebnis, statt dieselbe beendete Eingabe wiederholt zur Bearbeitung zu übergeben.

Behandlungsroutinen kurz und nicht blockierend halten

Setze Sockets auf O_NONBLOCK. Kann eine Operation nicht fortfahren, gibt sie −1 zurück und setzt errno auf EAGAIN oder EWOULDBLOCK, sodass die Behandlungsroutine zur Schleife zurückkehren kann. Die Bereitschaft kann sich vor der Operation ändern, daher schützt der nicht blockierende Modus auch vor unerwartetem Warten nach einer Bereitschaftsmeldung.

Ein blockierender Lesevorgang innerhalb einer Behandlungsroutine verzögert jedes Ereignis in der Warteschlange. Bei 100 wartenden Anfragen und einem blockierenden Festplattenlesevorgang von 10 ms pro Routine wartet die letzte Anfrage etwa 990 ms, bevor ihr eigener Lesevorgang beginnt; der Stapel braucht etwa 1.000 ms. Der Thread verbrachte den Großteil dieser Zeit mit Warten, während bereite Arbeit in der Warteschlange lag.

Eine Bereitschaftsschnittstelle wählen
SchnittstelleDarstellung der überwachten MengeWichtige Eigenschaft
select()Deskriptor-Bitmengen, die für jeden Aufruf neu aufgebaut werden.Deskriptorwerte müssen in FD_SETSIZE passen, üblicherweise 1024.
poll()Ein Array von Deskriptoren und angeforderten/zurückgegebenen Ereignissen.Keine entsprechende feste Grenze für Deskriptorwerte.
epoll / kqueueEine registrierte Interessensmenge, die der Kernel verwaltet.Effiziente Ereigniszustellung bei großen überwachten Mengen.

select() und poll() untersuchen bei jedem Aufruf die übergebene überwachte Menge, sodass ihr Aufwand mit deren Größe wächst. Linux bietet epoll; FreeBSD und macOS bieten kqueue. Die Überwachung nur weniger Deskriptoren macht einen hohen Deskriptorwert bei einem fd_set fester Größe nicht zulässig.

Wichtige Erkenntnisse

  • Eine Schleife führt synchrone Behandlungsroutinen einzeln nacheinander aus.
  • Bei select ist nfds der höchste überwachte Deskriptor plus eins.
  • Baue select-Mengen nach jedem Aufruf erneut auf.
  • Bereitschaft erfordert einen späteren Ein-/Ausgabeaufruf; gibt read 0 zurück, bedeutet das das Ende der Eingabe.
  • Nicht blockierende Sockets lassen Behandlungsroutinen zurückkehren, wenn eine Operation warten müsste.
  • FD_SETSIZE begrenzt Deskriptorwerte, nicht nur die Anzahl der überwachten Deskriptoren.

5.2 Speichere die Arbeit, die nach der Ein-/Ausgabe fortgesetzt wird

Netzwerkbereitschaft teilt einer Schleife mit, wann sie Ein-/Ausgabe versuchen kann. Bei gewöhnlichen Festplattendateien ist das anders: select() und poll() melden sie als bereit, selbst wenn der Zugriff auf ihre Daten noch auf den Speicher warten muss. Um weiterhin andere Ereignisse zu bedienen, reiche Arbeit asynchron ein und behandle ihr Ergebnis später.

Einreichen, anderswo weitermachen, Ergebnis abholen

Die asynchrone Ein-/Ausgabe von POSIX speichert eine Anfrage in struct aiocb. Wichtige Felder sind aio_fildes für den Dateideskriptor, aio_offset für die Dateiposition, aio_buf für das Ziel und aio_nbytes für die angeforderte Länge.

/* cb describes a read request */
aio_read(&cb);
/* Return to other work. Later: */
int status = aio_error(&cb);
/* EINPROGRESS: still running; 0: succeeded; otherwise an error. */
/* After completion, aio_return(&cb) collects its result. */

aio_read() stellt die Anfrage in die Warteschlange und kehrt zurück. Der Abschluss ist ein separates Ereignis. Wenn aio_error() den Abschluss anzeigt, liefert aio_return() das Ergebnis, etwa nach Erfolg die Anzahl gelesener Bytes. Eine Anfrage kann scheitern, und ein erfolgreicher Lesevorgang kann weniger Bytes als angefordert zurückgeben. Der Anfragedatensatz und sein Puffer müssen gültig bleiben, solange die Operation aussteht.

Das wiederholte Durchsuchen jeder Anfrage in einer engen Schleife verschwendet CPU-Zeit. aio_sigevent kann ein Abschlusssignal oder einen Callback konfigurieren. Linux io_uring nutzt Einreichungs- und Abschlusswarteschlangen; Windows-Ein-/Ausgabeabschlussports ermöglichen die abschlussbasierte Weiterleitung. Diese Schnittstellen lassen das Programm auf abgeschlossene Arbeit reagieren, ohne jede unerledigte Anfrage ständig erneut zu prüfen.

Zustand über die Lücke hinweg bewahren

Ein blockierter Thread behält die lokalen Variablen seiner Funktion auf seinem Stack. Eine Ereignisbehandlungsroutine, die Ein-/Ausgabe einreicht und zurückkehrt, muss speichern, was die spätere Behandlungsroutine benötigt. Diese ausdrückliche Verwaltung ist manuelle Stackverwaltung; der gespeicherte Zustand ist eine Fortsetzung.

Angenommen, ein Server liest Dateideskriptor 12 für einen Client an Socket 7. Beim Einreichen speichert er einen Datensatz, der diese Leseanfrage mit Socket 7 und ihrem Antwortzustand verknüpft. Wenn der Abschluss eintrifft, findet die Schleife den Datensatz, prüft das Leseergebnis und sendet die Antwort an den richtigen Client weiter. Getrennte Datensätze verhindern, dass bei mehreren ausstehenden Anfragen die Ziele verwechselt werden.

Die Fortsetzung sollte an die jeweilige Operation gebunden sein, nicht nur an eine Kennung, die später erneut verwendet werden könnte. Halte die Clientreferenz gültig, bis der Abschluss behandelt ist, und entferne den Datensatz, wenn die Aufgabe endet. Diese Einzelheiten zur Lebensdauer ersetzen Annahmen, die eine geradlinige blockierende Funktion auf ihrem Stack bewahren könnte.

Lesbare Syntax kann eine Suspendierung verbergen

Callbacks, Promises und async/await automatisieren einen Großteil dieser Zustandsspeicherung. In JavaScript suspendiert const data = await readFile(path); die Funktion, während ihre Ein-/Ausgabe aussteht. Andere Arbeit der Schleife kann laufen, und die Funktion wird später mit wiederhergestelltem lokalem Zustand fortgesetzt.

Asynchron bedeutet, dass der Aufrufer fortfahren kann, bevor die Operation abgeschlossen ist. Es bedeutet nicht, dass seine Behandlungsroutinen gleichzeitig auf mehreren CPU-Kernen laufen. Es bewirkt auch nicht, dass CPU-intensiver Code nach einem await automatisch den Prozessor abgibt: Dieser Code belegt den ausführenden Thread weiterhin, bis er zurückkehrt oder erneut suspendiert wird.

Wichtige Erkenntnisse

  • Die Bereitschaft einer gewöhnlichen Datei garantiert nicht, dass ein Festplattenlesevorgang ohne Warten abläuft.
  • Asynchrone Ein-/Ausgabe trennt Einreichung von Abschluss und Abholung des Ergebnisses.
  • Eine Fortsetzung speichert den Zustand, der nach der Rückkehr der ursprünglichen Behandlungsroutine benötigt wird.
  • Halte Anfragedatensätze und Puffer bis zum Abschluss gültig.
  • Async/await speichert den Zustand über eine Suspendierung hinweg; es erzeugt keine Ausführung auf mehreren Kernen.

5.3 Wähle Ereignisse, Threads oder einen hybriden Ansatz

Eine Ereignisschleife eignet sich gut für viele meist untätige Verbindungen mit kurzen Arbeitsschüben. Ein threadbasierter Entwurf hält den Kontrollfluss und den Stack einer Aufgabe zusammen. Die beste Wahl hängt von Wartezeit, CPU-Arbeit, Speicher und der Weiterentwicklung des Codes ab.

Den Aufwand der Entwürfe vergleichen
AspektEreignisbasierter EntwurfThreadbasierter Entwurf
Meist untätige VerbindungenKleiner Zustandsdatensatz pro Verbindung.Ein Thread verursacht Stack- und Schedulingaufwand.
Blockierende ArbeitAuslagern oder am Abschluss aufteilen.Der aufrufende Thread kann warten, während andere laufen.
Mehrere CPU-KerneMehrere Schleifen ausführen oder Worker nutzen.Ausführungsbereite Threads auf Kerne verteilen.
Gemeinsam genutzte veränderliche DatenEine synchrone Schleife vermeidet Überlappungen zwischen Behandlungsroutinen.Gemeinsame Zugriffe müssen synchronisiert werden.
Eine Funktion beginnt zu blockierenIhre Aufrufer müssen möglicherweise mit Fortsetzungen umstrukturiert werden.Geradliniger Kontrollfluss kann erhalten bleiben.

Ein Chatserver mit 50.000 meist untätigen Verbindungen kann von Ereignisdatensätzen statt eines eigenen Threadstacks pro Verbindung profitieren. Ein Bildverarbeitungsprogramm mit aufwendigen Berechnungen pro Anfrage braucht genügend Worker, um die verfügbaren Kerne zu nutzen; eine einzelne ausgelastete Schleife kann diese Berechnungen nicht parallel ausführen.

Verborgene Wartevorgänge sind weiterhin wichtig

Eine Behandlungsroutine kann ins Stocken geraten, ohne eine offensichtlich blockierende Schnittstelle aufzurufen. Der Zugriff auf eine ausgelagerte Seite kann einen Festplattenlesevorgang auslösen. Während dieses Seitenfehlers wartet der Thread der Schleife. mlock() kann benötigte Seiten im Speicher halten, wenn das System die angeforderte Menge fixierten Speichers zulässt. Das bloße Entfernen ausdrücklicher blockierender Aufrufe beseitigt nicht jede Ursache für Verzögerungen.

Codeänderungen können einen weiteren verborgenen Wartevorgang einführen. Beginnt eine zuvor sofort zurückkehrende Hilfsfunktion damit, Daten abzurufen, muss ein ereignisbasierter Aufrufer seine Arbeit an einer Fortsetzung aufteilen oder den Aufruf auslagern. Der Stack eines blockierenden Threads speichert bereits, wo fortgesetzt werden muss und welcher lokale Zustand benötigt wird.

Die Stärken kombinieren

Ein hybrider Entwurf belässt die Netzwerkbereitschaft bei der Ereignisschleife und sendet blockierende oder CPU-intensive Aufgaben an einen Worker-Pool. Abschlüsse gelangen zur Behandlung der Antwort an die Schleife zurück. Node.js nutzt den Threadpool von libuv für Dateisystemoperationen, sodass eine asynchrone Schnittstelle durch Hintergrundthreads implementiert werden kann.

Um mehrere Kerne zu nutzen, kann ein Server pro Kern eine Ereignisschleife in getrennten Threads oder Prozessen ausführen. Nutzen diese Schleifen veränderliche Daten gemeinsam, benötigen sie die üblichen Sperren oder ein anderes Synchronisierungsprotokoll. Der Vorteil, keine Sperren zu benötigen, gilt für synchrone Behandlungsroutinen einer Schleife, nicht für jeden Teil eines ereignisbasierten Servers.

Eine Aufgabe kann sich über await hinweg verzahnen

Angenommen, ein Stück ist noch auf Lager. Eine Behandlungsroutine prüft die Verfügbarkeit, wartet auf die Zahlung und verringert dann den Bestand. Während sie suspendiert ist, kann eine andere Behandlungsroutine denselben Bestand prüfen und ebenfalls eine Zahlung beginnen. Beide Prüfungen können erfolgreich sein, bevor eine der Routinen den Bestand verringert. Keine zwei synchronen Behandlungsroutinen laufen gleichzeitig, dennoch ist die gesamte Prüf- und Aktualisierungsoperation nicht atomar.

Reserviere den Bestand vor der Abgabe der Ausführung durch eine synchronisierte Zustandsänderung und sieh einen Freigabepfad für eine fehlgeschlagene Zahlung vor. Alternativ prüfe unter einem Protokoll erneut, das einen zweiten Verkauf verhindert. Ein Mutex, der während einer langen Wartezeit gehalten wird, kann andere Fortschrittsprobleme verursachen. Entwirf deshalb den Zustandsübergang und seine Wiederherstellung ausdrücklich. Auch bei Ereigniscode musst du überlegen, welche Tatsachen über jede Suspendierung hinweg weiterhin gelten.

Wichtige Erkenntnisse

  • Ereignisse eignen sich für viele untätige Verbindungen; CPU-intensive Arbeit braucht parallele Worker.
  • Eine Ereignisschleife nutzt einen Kern; gemeinsam genutzte Daten zwischen Schleifen müssen synchronisiert werden.
  • Seitenfehler und Hilfsfunktionen, die neuerdings blockieren, können eine Schleife anhalten.
  • Hybride Server lagern blockierende Arbeit aus und melden Abschlüsse an die Schleife zurück.
  • Bei einer Aufgabe, die sich über await erstreckt, kann eine Atomaritätsverletzung zwischen Prüfung und Handlung auftreten.

Semaphore und Verklemmungen: Beispiele

5 Interaktive Lektionen

1

Vom Abschlussflag zur funktionierenden Warteschlange

12 Schritte
Verfolge das Blockieren, Aufwachen, erneute Prüfen und Weiterleiten von Benachrichtigungen zwischen Erzeugern und Verbrauchern.
  1. 1
    EinführungWarte, ohne eine CPU zu verschwenden

    Ein Elternthread benötigt das Ergebnis seines Kindthreads, doch der Kindthread kann vor oder nach Beginn des Wartens fertig werden. Ein funktionierendes Protokoll muss beide Reihenfolgen behandeln.

    Verwende ein gemeinsames Flag done, um den Abschluss festzuhalten, einen Mutex zum Schutz des Flags und eine Bedingungsvariable, damit der Elternthread blockierend warten kann. Verfolge, wie der Mutex bei der Ausführung der Threads weitergegeben wird.

  2. 2
    BeispielDer Elternthread trifft zuerst ein

    Anfangs gilt done = 0. Der Elternthread erwirbt m, prüft das Flag und ruft wait(&c, &m) innerhalb einer while-Schleife auf.

    Der Warteaufruf gibt den Mutex atomar frei und beginnt zu warten. Der Kindthread kann nun m erwerben. Zwischen der Prüfung der Bedingung und dem Beginn des Wartens gibt es keine Lücke ohne Sperre, in der der Kindthread eine Benachrichtigung senden könnte, die der Elternthread verpasst.

  3. 3
    BeispielDer Kindthread hält den Abschluss fest

    Der Kindthread erwirbt m, speichert done = 1, signalisiert c und gibt die Sperre frei. Das Signal macht den Elternthread bereit; es führt ihn nicht sofort aus.

    Der Elternthread erwirbt m erneut, bevor der Warteaufruf zurückkehrt, prüft done erneut und fährt fort, weil der Abschluss des Kindthreads nun festgehalten ist. Beide Threads verwenden denselben Mutex für das gemeinsame Flag.

  4. 4
    HerausforderungKehre ihre Ankunftsreihenfolge um

    Der Kindthread wird fertig und signalisiert, bevor der Elternthread seinen Warteaufruf erreicht hat. Was verhindert, dass der Elternthread für immer blockiert bleibt?

    ---reveal---

    Der Kindthread hat bereits unter dem Mutex done = 1 gesetzt. Der Elternthread prüft dieses Flag, während er den Mutex hält, und überspringt das Warten. Das frühe Signal wird nicht gespeichert; das Zustandsflag bewahrt die Information bei der anderen Ankunftsreihenfolge.

  5. 5
    BeispielEin Wecksignal reserviert kein Element

    C1 wartet auf einen Puffer mit einem Platz. Ein Erzeuger fügt ein Element ein und signalisiert C1. Bevor C1 den Mutex erneut erwirbt, erwirbt C2 ihn und entnimmt das Element.

    Bei der Mesa-Semantik ist C1 nur bereit, um den Mutex zu konkurrieren. C1 muss den Puffer nach dem Erwerb des Mutex erneut prüfen. Die Rückkehr aus dem Warteaufruf ersetzt nicht die Prüfung, ob das Element noch vorhanden ist.

  6. 6
    HerausforderungWähle die richtige erneute Prüfung

    In diesem Ablauf verwendete C1 if (count == 0) wait(...). C1 kehrt nun zurück, nachdem C2 den Platz geleert hat. Welche Codeänderung verhindert eine ungültige Entnahme?

    ---reveal---

    Verwende while (count == 0) wait(...). Nachdem C1 den Mutex erneut erworben hat, sieht C1 den leeren Puffer und wartet erneut. Dieselbe Schleife behandelt auch spontanes Aufwachen und ist daher selbst ohne einen zweiten Verbraucher nötig.

  7. 7
    BeispielWecke jeden Thread mit erfüllbarer Anfrage auf

    Ein Allokator hat 0 freie Bytes. Ta wartet auf 100 Bytes und Tb auf 10. Die Freigabe von 50 ermöglicht nur Tb das Fortfahren, aber ein Signal könnte Ta aufwecken.

    Ein Broadcast erreicht beide. Jeder erwirbt den Mutex erneut und prüft die eigene angeforderte Menge. Tb kann 10 nehmen; Ta wartet erneut. Die Prädikatschleife gewährleistet Korrektheit, während die breite Benachrichtigung verhindert, dass ein Thread blockiert bleibt, der fortfahren kann.

  8. 8
    BeispielBewahre die Warteschlangenreihenfolge beim Rücksprung

    Die Kapazität beträgt 3. Füge 7, 8, 9 ein; entnimm 7; füge 10 ein. Das physische Array ist nun [10, 8, 9].

    Der Leseindex zeigt auf Platz 1, sodass die Entnahmereihenfolge 8, 9, 10 ist. Setze beide Indizes modulo Kapazität weiter und ändere count unter dem Mutex. Die Anzeige des Arrays von links nach rechts entspricht nicht zwingend der logischen Reihenfolge der Warteschlange.

  9. 9
    KernaussageGib jeder Rolle eine eigene Warteschlange

    Ein Erzeuger wartet auf empty, solange der Puffer voll ist, fügt ein Element ein und signalisiert dann fill. Ein Verbraucher wartet auf fill, solange der Puffer leer ist, entnimmt ein Element und signalisiert dann empty. Beide Bedingungsvariablen verwenden den Puffermutex.

    Die getrennten Warteschlangen sind wichtig: Eine Elementbenachrichtigung erreicht einen Verbraucher und eine Platzbenachrichtigung einen Erzeuger. Eine gemeinsame Signalwarteschlange könnte einen Thread auswählen, dessen Bedingung noch nicht erfüllt ist.

  10. 10
    HerausforderungFinde den blockierten Erzeuger

    Ein Puffer mit einem Platz verwendet eine gemeinsame Bedingungswarteschlange und while-Schleifen. P ist blockiert, weil der Puffer voll ist. C1 entnimmt das Element, signalisiert C2 und wartet dann, weil der Puffer leer ist. Auch C2 findet den Puffer leer vor und wartet. Warum bleibt der Platz ungenutzt?

    ---reveal---

    P hat die Platzbenachrichtigung nie erhalten. Alle ausführbare Arbeit ist zum Stillstand gekommen, obwohl ein Erzeuger fortfahren könnte. Getrennte Warteschlangen für Erzeuger und Verbraucher leiten das Signal von C1 an P.

  11. 11
    HerausforderungBeende die Arbeit, ohne Elemente zu verlieren

    Zwei Erzeuger erzeugen noch Anfragen. Ein Koordinator plant, jetzt für jeden Verbraucher eine Stoppmarke hinzuzufügen. Was muss er ändern und warum?

    ---reveal---

    Warte zuerst, bis alle Erzeuger fertig sind. Stelle erst dann die Stoppmarken in die Warteschlange. Andernfalls können Verbraucher die Marken entnehmen und sich beenden, während Erzeuger noch reguläre Anfragen einfügen müssen. Zum Einfügen und Entnehmen der Marken wird weiterhin die normale Warteschlangensynchronisierung verwendet.

  12. 12
    ZusammenfassungPrüfe einen bedingten Warteaufruf

    Bestimme für jeden Warteaufruf Zustand, Mutex und Warteschlange. Halte die Prädikatprüfung unter dem Mutex, beginne das Warten ohne Lücke ohne Sperre und prüfe nach jeder Rückkehr erneut.

    Untersuche dann den Signalpfad: Hat er den Zustand geändert, und kann die Benachrichtigung einen Thread erreichen, der nun fortfahren kann? Diese Fragen decken sowohl verlorene Wecksignale als auch Pufferstillstände durch das Aufwecken der falschen Rolle auf.

2

Verfolge Erlaubnisse durch reale Protokolle

15 Schritte
Wähle Zählerwerte, verfolge Übergaben und halte Ressourcenreservierungen vom wechselseitigen Ausschluss getrennt.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungBenenne die Bedeutung einer Erlaubnis
  2. 2
    BeispielVerfolge einen binären Semaphor
  3. 3
    HerausforderungKorrigiere einen falschen Anfangswert
  4. 4
    BeispielHalte einen frühen Abschluss fest
  5. 5
    BeispielBegrenze eine speicherintensive Phase
  6. 6
    HerausforderungTrenne Zulassung und Verwaltung
  7. 7
    VerbindungTriff dich mit dem anderen Thread, bevor du fortfährst
  8. 8
    ErklärungReserviere, aktualisiere, gib bekannt
  9. 9
    BeispielVerfolge einen blockierten Verbraucher
  10. 10
    HerausforderungErkenne die Sperrabhängigkeit
  11. 11
    ErklärungLass eine Lesergruppe eine Erlaubnis halten
  12. 12
    BeispielBeobachte den wartenden Schreiber
  13. 13
    HerausforderungBehandle eine nötige Aktualisierung
  14. 14
    HerausforderungBewahre beide Arten des Schutzes
  15. 15
    ZusammenfassungPrüfe jede erworbene Erlaubnis
3

Analysiere die verborgene Verzahnung

9 Schritte
Unterscheide eine Operation, die zusammenbleiben muss, von einer Operation, die zuerst geschehen muss.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungFrag dich, was der Code annimmt
  2. 2
    BeispielSetze eine Änderung in die Lücke
  3. 3
    BeispielMache die Entscheidung über Zahlen unteilbar
  4. 4
    HerausforderungFinde die fehlende Grenze des kritischen Abschnitts
  5. 5
    BeispielEin neuer Worker kann zuerst laufen
  6. 6
    ErklärungGib einen abgeschlossenen Zustand bekannt
  7. 7
    HerausforderungErsetze eine Schätzung des Zeitpunkts
  8. 8
    HerausforderungWähle zwischen den beiden Korrekturen
  9. 9
    ZusammenfassungTeste die Garantie, nicht die Verzögerung
4

Vom Sperrzyklus zur sicheren Anfrage

17 Schritte
Verfolge eine Verklemmung, schließe eine Bedingung aus, prüfe die Ressourcensicherheit und plane die Behebung.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungUnterscheide drei Strategien
  2. 2
    BeispielSchließe einen Zyklus aus zwei Threads
  3. 3
    ErklärungBenenne die notwendigen Bedingungen
  4. 4
    HerausforderungUnterscheide eine Kette von einem Zyklus
  5. 5
    BeispielOrdne beide Richtungen gleich
  6. 6
    TippGib teilweise erworbene Ressourcen vor einem erneuten Versuch frei
  7. 7
    HerausforderungPrüfe die verborgene Hilfsfunktion
  8. 8
    BeispielBerechne die verbleibenden Ansprüche
  9. 9
    BeispielFinde einen Thread, der zuerst fertig werden kann
  10. 10
    BeispielVervollständige die sichere Folge
  11. 11
    KernaussageEine Zuteilung freier Ressourcen kann trotzdem unsicher sein
  12. 12
    HerausforderungVergleiche Ressourcen nach Typ
  13. 13
    VerbindungErkenne das gegenwärtige Warten
  14. 14
    ErklärungStelle einen gültigen Zustand wieder her
  15. 15
    HerausforderungWähle ein geeignetes Ziel für die Behebung
  16. 16
    HerausforderungDeute ein unsicheres Ergebnis
  17. 17
    ZusammenfassungWähle die zur Frage passende Prüfung
5

Bediene Ereignisse, während Ein-/Ausgabe aussteht

13 Schritte
Implementiere eine Bereitschaftsschleife, speichere Fortsetzungen und finde die verbleibenden Nebenläufigkeitsrisiken.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungVerlege das Warten an die Grenze der Schleife
  2. 2
    BeispielBaue die überwachte Menge von select auf
  3. 3
    ErklärungFühre den tatsächlichen Lesevorgang aus
  4. 4
    HerausforderungPrüfe einen hohen Deskriptorwert
  5. 5
    ErklärungVerwechsle Dateibereitschaft nicht mit Abschluss
  6. 6
    BeispielSpeichere das Ziel der Antwort
  7. 7
    HerausforderungDeute eine ausstehende Operation
  8. 8
    VerbindungLass await lokalen Zustand bewahren
  9. 9
    BeispielNutze Worker für aufwendige Arbeit
  10. 10
    TippAchte auf implizites Warten
  11. 11
    HerausforderungFinde die Lücke über await hinweg
  12. 12
    HerausforderungBehalte die Grenze zwischen mehreren Kernen im Blick
  13. 13
    ZusammenfassungPrüfe jeden Suspendierungspunkt

Semaphore und Verklemmungen: Übungsaufgaben

50 Übungsfragen in 5 Sets

Set 1: Warten, Erlaubnisse und Abhängigkeiten10 Übungsfragen

Frage 1 von 10

Ein Dienst hat sieben gleichartige freie Gerätehandles. Welcher Semaphor-Anfangswert erlaubt es, alle verfügbaren Handles auszuleihen?
  1. 0
  2. 1
  3. 7
  4. 8
Antwort zeigenAntwort ausblenden
Antwort: C
Initialisiere den Semaphor mit 7, weil sofort sieben Einheiten verfügbar sind. Jeder Entleiher verbraucht eine Erlaubnis und gibt sie nach der Nutzung zurück. Ein Anfangswert von 1 würde alle Entleiher nacheinander bedienen, obwohl der Pool sieben bedienen kann; ein Anfangswert von 0 würde den ersten Entleiher blockieren.

Frage 2 von 10

Welche Schleife sollte einen POSIX-Warteaufruf an einer Bedingungsvariable umgeben, wenn ein Verbraucher eine nicht leere Warteschlange benötigt?
  1. while (count == 0)
  2. if (count == 0)
  3. while (count > 0)
  4. if (count > 0)
Antwort zeigenAntwort ausblenden
Antwort: A
Warte, solange die Warteschlange leer ist, und prüfe nach jeder Rückkehr erneut. Die Warteschlange kann sich geändert haben, bevor der aufgeweckte Thread den Mutex erneut erwirbt, und spontanes Aufwachen ist erlaubt. Ein if führt keine zweite Prüfung aus und kann den Verbraucher daher aus einer leeren Warteschlange entnehmen lassen.

Frage 3 von 10

Welcher Anfangswert eines Semaphors lässt vor einer V-Operation genau einen Thread in einen kritischen Abschnitt?
  1. 0
  2. 2
  3. −1
  4. 1
Antwort zeigenAntwort ausblenden
Antwort: D
Eine anfängliche Erlaubnis lässt den ersten Thread eintreten und zwingt einen zweiten, auf eine zurückgegebene Erlaubnis zu warten. Ein Wert von 0 blockiert dagegen den ersten Thread, sodass er den kritischen Abschnitt und seine spätere V-Operation nicht erreichen kann. Ein negativer Anfangswert ist für die Initialisierung eines POSIX-Semaphors nicht zulässig.

Frage 4 von 10

Ein Rechner hat 12 GB RAM. Andere Arbeit benötigt 3 GB, und jeder zugelassene Worker benötigt 750 MB. Wie viele Worker passen bei Verwendung dezimaler SI-Einheiten in den verbleibenden Speicher?
  1. 16
  2. 12
  3. 9
  4. 15
Antwort zeigenAntwort ausblenden
Antwort: B
Nach der Reservierung von 3 GB stehen den Workern 9 GB oder 9.000 MB zur Verfügung. 9.000 MB geteilt durch 750 MB ergibt 12 gleichzeitige Worker. Würden 16 zugelassen, verbrauchten die Worker die vollen 12 GB und ließen keinen Speicher für die angegebene andere Arbeit.

Frage 5 von 10

Zwei Threads reservieren jeweils vier Plätze. Es sind noch sechs Plätze frei, und beide Verfügbarkeitsprüfungen sind vor einer der Reservierungen abgeschlossen. Welche Abgrenzung des kritischen Abschnitts verhindert eine Überbuchung?
  1. Ein gesperrter Bereich für die Prüfung, dann einer für die Reservierung.
  2. Ein gesperrter Bereich für Prüfung und Reservierung zusammen.
  3. Eine Lesesperre um die gesamte Reservierung jedes Threads.
  4. Ein eigener privater Mutex um jede Reservierung.
Antwort zeigenAntwort ausblenden
Antwort: B
Die Platzprüfung muss bis zur davon abhängigen Reservierung gültig bleiben. Schütze daher beide in einem kritischen Abschnitt, den alle reservierenden Threads gemeinsam nutzen. Getrennte gesperrte Zugriffe lassen weiterhin zu, dass beide Prüfungen sechs freie Plätze sehen, bevor einer vier reserviert. Ein privater Mutex kann den konkurrierenden Thread nicht ausschließen.

Frage 6 von 10

Ein Berichtsthread benötigt eine Endsumme, die ein Worker unmittelbar vor seinem Ende schreibt. Welche Operation stellt den Abschluss sicher, bevor der Bericht die Summe liest?
  1. Vor dem Lesen kurz blockierend warten.
  2. Vor dem Lesen die CPU freiwillig abgeben.
  3. Nur während des Lesens eine Sperre halten.
  4. Vor dem Lesen mit Join auf den Worker warten.
Antwort zeigenAntwort ausblenden
Antwort: D
Join auf den Worker wartet, bis dieser beendet ist, sodass seine fertige Summe verfügbar ist, bevor der Bericht sie liest. Eine Wartezeit schätzt lediglich, wie lange die Arbeit dauert. Eine Sperre allein beim Lesen kann nicht verlangen, dass der Worker zuerst fertig geworden ist oder das Ergebnis geschrieben hat.

Frage 7 von 10

Thread H hält M und wartet auf N. Thread J hält N und wartet auf M. Beide Sperren sind exklusiv und können nicht zwangsweise entzogen werden. Was beschreibt diese beiden Threads?
  1. Eine verklemmte Gruppe.
  2. Eine verhungernde Gruppe.
  3. Ein Paar im Livelock.
  4. Ein ausführungsbereites Paar.
Antwort zeigenAntwort ausblenden
Antwort: A
Jeder Thread wartet auf eine Sperre, die nur sein blockierter Partner freigeben kann. So entsteht ein dauerhafter Zyklus. Dies ist eine Verklemmung, bei der beide Threads blockiert sind. Bei einem Livelock müssten sie weiterlaufen und reagieren; beim Verhungern würde andere Arbeit Fortschritte machen, während ein wartender Thread zurückgestellt wird.

Frage 8 von 10

Eine Übertragung sperrt Kontoobjekte mit eindeutigen Schlüsseln. Welche Erwerbsregel verhindert zyklisches Warten bei Übertragungen in entgegengesetzter Richtung?
  1. Das Quellkonto vor dem Zielkonto sperren.
  2. Das Zielkonto vor dem Quellkonto sperren.
  3. Beide Konten nach aufsteigendem Schlüssel sperren.
  4. Das Konto mit dem kleineren Kontostand zuerst sperren.
Antwort zeigenAntwort ausblenden
Antwort: C
Eine gemeinsame aufsteigende Schlüsselreihenfolge lässt beide Übertragungsrichtungen dieselbe erste Sperre anfordern und verhindert das entgegengesetzte Erwerbsmuster. Wird das Quellkonto zuerst gesperrt, kehrt sich die Reihenfolge mit der Übertragungsrichtung um. Eine Reihenfolge nach Kontostand kann sich mit den Kontoständen ändern und ist daher keine feste gemeinsame Schlüsselregel.

Frage 9 von 10

Eine Schleife überwacht mit select die Deskriptoren 2, 8 und 14. Welchen Wert sollte sie für nfds übergeben?
  1. 3
  2. 14
  3. 15
  4. 16
Antwort zeigenAntwort ausblenden
Antwort: C
Select prüft Deskriptornummern von 0 bis einschließlich nfds - 1, sodass der höchste überwachte Deskriptor 14 den Wert 15 erfordert. Das Argument beruht auf dem größten Deskriptorwert, nicht auf den drei Einträgen der überwachten Menge. Die Übergabe von 14 würde Deskriptor 14 aus dem geprüften Bereich ausschließen.

Frage 10 von 10

Behandlungsroutine A reicht einen Lesevorgang für Client-Socket 40 ein. Behandlungsroutine B reicht danach einen weiteren Lesevorgang für Socket 41 ein. Beide Abschlussroutinen verwenden ein globales Ziel, das nun 41 ist. Der Lesevorgang von A wird zuerst abgeschlossen. Wohin wird die Antwort von A geleitet, und welche Korrektur berücksichtigt beide Clients?
  1. An 40; das globale Ziel weiterverwenden.
  2. An 41; das Ziel pro Anfrage speichern.
  3. An 40; die select-Menge vor der Antwort neu aufbauen.
  4. An 41; nur ausstehende Anfragen zählen.
Antwort zeigenAntwort ausblenden
Antwort: B
Die Abschlussroutine von A liest den überschriebenen globalen Wert 41 und leitet die Antwort daher an den Client von B statt an Socket 40. Speichere jedes Ziel bei seiner eigenen Anfrage und schlage es beim Abschluss nach. Eine schnellere Abschlussbehandlung kann nicht dafür sorgen, dass ein globales Ziel zwei ausstehende Clients berücksichtigt.
Set 2: Verfolge das Protokoll anhand eines Ablaufs10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Ein Ringpuffer hat Kapazität 4 und ist anfangs leer, beide Indizes stehen bei 0. Er erhält 21, 22, 23, gibt zwei Elemente aus und erhält dann 24 und 25. Wie lautet nun seine logische Entnahmereihenfolge?
  1. 25, 23, 24
  2. 23, 24, 25
  3. 24, 25, 23
  4. 21, 24, 25
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Eine Laderoutine setzt ready unter einem Mutex und signalisiert, bevor ein Nutzer zu warten beginnt. Der Nutzer erwirbt später diesen Mutex und prüft ready, bevor er entscheidet, ob er wartet. Warum kann er fortfahren?
  1. Die Bedingungsvariable hat das Signal gespeichert.
  2. Der Erwerb des Mutex wiederholt die Benachrichtigung.
  3. Das Signal hat den Mutex an den späteren Nutzer übergeben.
  4. Der Bereitschaftszustand bleibt nach dem Signal bestehen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Ein Puffer mit Semaphoren hat Kapazität 6 und enthält anfangs zwei Elemente. Es laufen keine Operationen. Wie lauten die Anzahlen der Erlaubnisse für empty und full?
  1. Empty 4; full 2.
  2. Empty 6; full 2.
  3. Empty 2; full 4.
  4. Empty 4; full 0.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Drei Leser halten die grundlegende leserbevorzugende Sperre, und ein Schreiber wartet. Zwei Leser verlassen den Abschnitt. Was verhindert, dass der Schreiber jetzt eintritt?
  1. Der Schreiber muss drei getrennte Schreiberlaubnisse einsammeln.
  2. Der zuletzt gegangene Leser hat writelock bereits freigegeben.
  3. Der verbleibende Leser hält noch den Zugriff der Gruppe.
  4. Die Zählersperre gehört weiterhin dem wartenden Schreiber.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein gemeinsam genutzter Kontostand wird atomar geladen und atomar verringert. Zwei Threads prüfen jeweils, ob mindestens 45 Einheiten vorhanden sind, und ziehen dann 45 ab; der Kontostand beginnt bei 70. Welcher Fehler bleibt möglich, wenn beide Prüfungen zuerst abgeschlossen werden?
  1. Die zweite Subtraktion lehnt die Abbuchung ab.
  2. Das atomare Laden reserviert jeweils 45 Einheiten.
  3. Beide Abbuchungen können erfolgen und −20 hinterlassen.
  4. Nur der erste Aufrufer kann 70 lesen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Available ist (1, 1). A hat Allocation (1, 0) und Max (2, 2); B hat Allocation (0, 1) und Max (1, 2). Was ergibt die Sicherheitsprüfung des Bankieralgorithmus?
  1. Sicher; B kann vor A fertig werden.
  2. Sicher; A kann vor B fertig werden.
  3. Unsicher; kein verbleibender Anspruch passt anfangs.
  4. Unsicher; das volle Maximum von A übersteigt Available.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Ein zeitlich begrenzter Warteaufruf auf einen Mutex gibt ETIMEDOUT zurück. Was steht damit fest?
  1. Der Besitzer ist Teil eines Sperrzyklus.
  2. Der Mutex wurde bereits freigegeben.
  3. Der geschützte Zustand ist beschädigt.
  4. Das Warten hat seine Frist überschritten.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ein Server hält 30.000 meist untätige Sockets und führt pro Nachricht wenig Berechnung aus. Warum kann eine Ereignisschleife pro Verbindung weniger Speicher benötigen als ein eigener Thread pro Socket?
  1. Untätige Verbindungen benötigen keinen Zustand.
  2. Verbindungen können kleine Zustandsdatensätze nutzen.
  3. Jede Ereignisroutine erhält einen eigenen CPU-Kern.
  4. Alle Sockets teilen sich einen wiederverwendbaren Netzwerkdeskriptor.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein wartender Thread liest done ohne den Mutex als falsch. Der Worker erwirbt dann den Mutex, setzt done auf wahr, signalisiert, während niemand wartet, und gibt den Mutex frei. Danach erwirbt der wartende Thread den Mutex und ruft wait ohne erneute Prüfung auf. Welcher Abhängigkeitsfehler ist aufgetreten?
  1. Ein spontanes Aufwachen vor dem Signal des Workers.
  2. Eine sichere Mesa-Prüfung nach dem Erwerb des Mutex.
  3. Ein gespeichertes Signal für den nächsten Aufrufer.
  4. Ein verlorenes Wecksignal nach einer ungeschützten Prüfung.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

In einem Wartegraphen für Ressourcen mit jeweils einer Instanz gelten U → V, V → W, W → V, und X hat keine ausgehende Kante. Welche Threads bilden den Abhängigkeitszyklus?
  1. V und W.
  2. U, V und W.
  3. U und X.
  4. V, W und X.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 3: Wende die Regeln bei veränderten Bedingungen an10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Ein gemeinsam genutzter Allokator hat keinen freien Speicher. Ein wartender Thread benötigt 180 Bytes, ein anderer 40. Eine Freigabe fügt 90 Bytes hinzu und signalisiert nur dem auf 180 Bytes wartenden Thread; weitere Änderungen folgen nicht. Wie sollte die Benachrichtigungsstrategie geändert werden, damit ein Thread fortfahren kann, dessen Anfrage erfüllbar ist?
  1. Nach der Freigabe einen Broadcast ausführen.
  2. Erst bei 180 verfügbaren Bytes signalisieren.
  3. Bei der Aktualisierung den Mutex weglassen.
  4. Beide while-Schleifen durch if ersetzen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Ein Puffer mit Bedingungsvariablen und Kapazität zwei enthält ein Element. Zwei Erzeuger prüfen jeweils unter dem Mutex, ob Platz ist, geben ihn frei und erwerben ihn später nur für put erneut. Beide Prüfungen enden vor einem der put-Aufrufe. Welchen Zustand können die beiden Aufrufe erzeugen?
  1. Zähler 2; die zweite Prüfung reserviert den Platz.
  2. Zähler 1; einer der sequenziellen Aufrufe wird verworfen.
  3. Zähler 3; die Lücke lässt beide Prüfungen ein Element zulassen.
  4. Zähler 0; der zurückgesprungene Index leert den Puffer.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

An einem Semaphor für den Abschluss wartet kein Thread, als sein Worker eine V-Operation ausführt. Was hinterlässt diese Operation für eine spätere erfolgreiche P-Operation?
  1. Einen gespeicherten Mutex-Besitzer.
  2. Eine verlorene Benachrichtigung.
  3. Einen blockierten wartenden Thread.
  4. Eine gespeicherte Erlaubnis.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Eine Lesesperre schützt einen Cache. Ein Leser bemerkt einen fehlenden Eintrag, gibt den Lesezugriff frei und wartet auf Schreibzugriff. Ein anderer Schreiber ergänzt den Eintrag, bevor der Leser eintritt. Was muss der ursprüngliche Leser nach dem Erwerb des Schreibzugriffs tun?
  1. Aufgrund der früheren Beobachtung sofort einfügen.
  2. Erneut prüfen, ob der Eintrag noch fehlt.
  3. Unter Schreibzugriff erneut Lesezugriff erwerben.
  4. Den Schreibzugriff ohne Prüfung des Caches freigeben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Initialisierung und erste Verwendung erwerben jeweils denselben Mutex, dennoch sieht die erste Verwendung manchmal einen nicht initialisierten Datensatz. Welche Garantie fehlt?
  1. Exklusiver Zugriff während jeder Operation.
  2. Abgeschlossene Initialisierung vor der Verwendung.
  3. Eine andere private Sperre für jede Operation.
  4. Eine feste Verzögerung nach jeder Mutex-Freigabe.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Aufgaben erwerben ihre vollständigen Ressourcenmengen, während sie einen globalen Erwerbsmutex halten, und geben diesen vor der Arbeit frei. Jeder Pfad folgt dieser Regel, und die Ressourcenfreigabe benötigt den Erwerbsmutex nie. Warum können konkurrierende teilweise Erwerbe keinen Zyklus bilden?
  1. Es kann nur eine unvollständige Erwerbsfolge geben.
  2. Kein Thread kann beim Warten eine Ressource halten.
  3. Alle Ressourcen werden bei Arbeitsbeginn entziehbar.
  4. Die CPU kann beim Erwerb keinen Thread verdrängen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Es gibt 9 gleichartige Ressourcen. A hält 3 bei Max 7; B hält 2 bei Max 4. A fordert 3 weitere an. Sollte der Bankieralgorithmus diese Anfrage bewilligen?
  1. Nein; der resultierende Zustand hat keine sichere Folge.
  2. Ja; B kann vor A fertig werden.
  3. Nein; die Anfrage übersteigt den Restanspruch von A.
  4. Ja; A kann vor B fertig werden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ein lesebereiter Stream-Socket gibt bei einem erfolgreichen Leseaufruf 0 zurück. Welchen Eingabezustand sollte die Behandlungsroutine verarbeiten?
  1. Ein vorübergehendes Ergebnis, das Blockieren anzeigt.
  2. Ein empfangenes Byte mit dem Wert null.
  3. Das Ende der Eingabe von der Gegenstelle.
  4. Einen ausstehenden Festplattenleseabschluss.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Eine Behandlungsroutine stellt einen asynchronen Festplattenlesevorgang in die Warteschlange, gibt seinen Zielpuffer frei und kehrt zurück. Die Anfrage steht noch aus. Was muss sich ändern, bevor der Abschlusspfad das Ergebnis sicher nutzen kann?
  1. Nur den Dateideskriptorwert bis zur Rückkehr behalten.
  2. Sofort lesen, weil die Einreichung erfolgreich war.
  3. Die Pufferfreigabe vor die Einreichung verschieben.
  4. Den Puffer bis zum Ein-/Ausgabeabschluss gültig halten.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine Schleife prüft ein gemeinsames Kontingent, wartet auf externe Genehmigung und verringert dann das Kontingent. Zwei Aufgaben können die Prüfung bestehen, bevor eine von ihnen fortgesetzt wird. Welche Änderung verhindert, dass beide die letzte Einheit beanspruchen?
  1. Das Laden des Kontingents atomar machen, die Entscheidung aber aufteilen.
  2. Nur die endgültige Verringerung nach der Genehmigung sperren.
  3. Vor dem Warten atomar eine Kontingenteinheit reservieren.
  4. Nach der Genehmigung anhand der früheren Prüfung reservieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 4: Analysiere unvollständige Korrekturen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Ein auf eine Bedingung wartender Thread hält für sein Prädikat den Mutex M und zusätzlich die Sperre L. Der Warteaufruf gibt M frei. Der einzige Thread, der das Prädikat erfüllen kann, muss zuerst L erwerben. Was verhindert den Fortschritt, und welche Abhängigkeit muss sich ändern?
  1. Der signalisierende Thread muss M vor der Zustandsänderung zweimal erwerben.
  2. Der wartende Thread muss M während des gesamten Wartens gesperrt halten.
  3. Der wartende Thread muss das Prädikat verwerfen und einmal warten.
  4. Der wartende Thread darf L während dieses Wartens nicht halten.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Vier Verbraucher verarbeiten eine FIFO-Warteschlange. Auf alle Erzeuger wurde nun mit Join gewartet, reguläre Elemente können noch in der Warteschlange liegen, und eine reservierte Stoppmarke beendet einen Verbraucher. Wie viele Marken sollte der Koordinator in die Warteschlange stellen?
  1. 1
  2. 4
  3. 3
  4. 5
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Ein Pool beginnt mit drei Erlaubnissen. Ein Entleiher führt erfolgreich eine P-Operation aus, gibt später seine Ressource zurück und führt versehentlich zweimal eine V-Operation aus. Wie viele Entleiher können, nachdem alle Arbeit ruht, P-Operationen passieren, bevor jemand erneut eine V-Operation ausführt?
  1. 4
  2. 2
  3. 3
  4. 5
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Ein Puffer mit Semaphoren und Kapazität zwei ist leer. P1 und P2 führen jeweils erfolgreich eine P-Operation auf empty aus. Dann fügt P1 unter dem Mutex ein Element ein und gibt den Mutex frei, hat aber noch keine V-Operation auf full ausgeführt. P2 hat noch nichts eingefügt. Ein Verbraucher wartet nun auf full. Was geschieht?
  1. Er entnimmt das Element von P1, bevor P2 einfügt.
  2. Er blockiert, bis P2 sein Einfügen abschließt.
  3. Er blockiert, bis eine Elementerlaubnis bereitgestellt wird.
  4. Er entnimmt das Element von P1, sobald der Mutex frei ist.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein Programmierer möchte die Erhöhung eines gemeinsamen Zählers ohne Mutex als eine unteilbare Aktualisierung ausführen. Welche atomare Operation implementiert diese Aktualisierung direkt?
  1. Nur atomares Laden.
  2. Nur atomares Speichern.
  3. Eine Signalisierung.
  4. Fetch-and-Add.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Fünf speisende Philosophen nutzen einen Semaphor mit Anfangswert 4. Jeder führt eine P-Operation darauf aus, nimmt eine linke Gabel auf, führt eine V-Operation auf dem Semaphor aus und versucht dann, die rechte Gabel zu nehmen. Können alle fünf blockiert werden und dabei jeweils eine Gabel halten?
  1. Nein; der Anfangswert begrenzt Gabelhalter dauerhaft auf vier.
  2. Ja; frühe Rückgaben lassen alle fünf den Gabelzyklus erreichen.
  3. Nein; jede V-Operation gibt die linke Gabel atomar zurück.
  4. Ja; aber nur, wenn der Semaphor-Anfangswert über vier steigt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Available ist (1, 1). P hält (1, 0) bei Max (3, 1); Q hält (1, 1) bei Max (2, 2). P fordert (0, 1) an. Sollte der Bankieralgorithmus diese Anfrage bewilligen?
  1. Ja; P kann mit den verbleibenden Ressourcen fertig werden.
  2. Nein; der vorläufige Zustand hat keine sichere Folge.
  3. Ja; Q kann mit den verbleibenden Ressourcen fertig werden.
  4. Nein; P fordert mehr als seinen verbleibenden Anspruch.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ein Erkennungsverfahren für mehrere Instanzen sieht Available (0, 0). P hält (1, 0) und fordert (0, 1) an; Q hält (0, 1) und fordert (1, 0) an. R hält nichts und fordert (1, 0) an. Welche Halter erhalten die Ressourcenverklemmung aufrecht?
  1. P, Q und R.
  2. P und R.
  3. P und Q.
  4. Q und R.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Eine select-Schleife überwacht anfangs die Stream-Sockets 6 und 10. Nur 6 wird als bereit zurückgegeben. Die Schleife verwendet diese zurückgegebene Menge erneut, ohne sie neu aufzubauen, während 10 Daten empfängt und Leseaufrufe auf 6 nun 0 zurückgeben. Welche zwei Fehler können folgen?
  1. Socket 10 wird weiter überwacht; Socket 6 liefert ein Nutzdatenbyte null.
  2. Socket 10 wird schreibbereit; Socket 6 wartet auf einen Festplattenabschluss.
  3. Socket 10 entfällt; Socket 6 kann am Eingabeende erneut bearbeitet werden.
  4. Socket 10 ersetzt 6 in der Menge; Socket 6 wird von select geschlossen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Vierzig Anfragen stehen an einer Ereignisschleife in der Warteschlange. Jede Behandlungsroutine führt vor ihrer Rückkehr einen blockierenden Festplattenlesevorgang von 6 ms aus; die übrige Arbeit ist vernachlässigbar. Wie lange wartet die letzte Anfrage, bevor ihr Lesevorgang beginnt?
  1. 234 ms
  2. 240 ms
  3. 6 ms
  4. 39 ms
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 5: Übertrage die Überlegungen auf neue Entwürfe10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Ein Java-Thread ruft notifyAll auf einem Objekt außerhalb jedes synchronized-Bereichs auf und besitzt dessen Monitor auch sonst nicht. Was geschieht?
  1. Der Aufruf speichert eine Benachrichtigung für später Wartende.
  2. Der Aufruf löst IllegalMonitorStateException aus.
  3. Der Aufruf übergibt die Sperren des Aufrufers an Wartende.
  4. Der Aufruf wartet, bis der Monitor frei ist.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Ein Semaphor wird mit einem Mutex, einer Bedingungsvariable und einem nicht negativen Wert implementiert. Die V-Operation erhöht den Wert und signalisiert unter dem Mutex. Die P-Operation prüft den Wert mit if, wartet bei null und verringert ihn dann. Wie kann dies dennoch scheitern?
  1. Der Mutex lässt zwei Threads den Wert gleichzeitig ändern.
  2. Der Semaphor kann keine V-Operation ohne Wartende speichern.
  3. Ein zurückkehrender wartender Thread kann den Wert null verringern.
  4. Eine V-Operation verringert den Wert immer, wenn niemand wartet.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Ein Verbraucher hält einen Puffermutex, während er auf den Belegungssemaphor wartet. Der Puffer ist leer. Ein Korrekturvorschlag sieht vor, dass der Erzeuger vor dem Einfügen eine V-Operation auf dem Belegungssemaphor ausführt, damit er den Verbraucher ohne den Mutex aufwecken kann. Warum ist diese Korrektur unsicher?
  1. Sie erlaubt die Entnahme vor dem Einfügen durch den Erzeuger.
  2. Sie erlaubt das Einfügen vor der Reservierung von Platz.
  3. Sie gibt eine Platzerlaubnis vor Abschluss der Entnahme zurück.
  4. Sie gibt dieselbe Erlaubnis für ein fertiges Element zweimal aus.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Im grundlegenden Entwurf mit Leserzähler kann der erste Leser die Zählersperre halten, während er auf writelock wartet. Warum kann der aktive Schreiber dennoch fertig werden, anders als ein Erzeuger, der durch den Puffermutex eines Verbrauchers blockiert ist?
  1. Der wartende Leser gibt die Zählersperre in sem_wait frei.
  2. Der Schreiber hält die Sperre des Leserzählers bereits selbst.
  3. Die V-Operation des Schreibers wartet, bis der Leserzähler null erreicht.
  4. Der Schreiber kann ohne Zählersperre eine V-Operation auf writelock ausführen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein Worker liest einen gemeinsamen Zeiger, bevor der erzeugende Thread dessen Zuweisung abgeschlossen hat. Um welche Art von Abhängigkeitsfehler handelt es sich?
  1. Eine Reihenfolgeverletzung.
  2. Eine Ressourcenverklemmung.
  3. Verhungern von Schreibern.
  4. Ein Livelock beim Wiederholen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Zwei Aktualisierungsschleifen erwerben jeweils eine Sperre, scheitern mit Trylock an der anderen, geben ihre Sperre frei und versuchen es unbegrenzt gemeinsam erneut. Ein Korrekturvorschlag behält die erste Sperre, während normal auf die zweite gewartet wird. Was riskiert diese Änderung?
  1. Der Livelock bleibt bestehen, weil beide Threads weiter versuchen.
  2. Ein Abschluss ist garantiert, während der andere verhungern kann.
  3. Der Livelock beim Wiederholen wird durch eine mögliche Verklemmung ersetzt.
  4. Beide Erwerbe werden unter einer gemeinsamen ersten Sperre serialisiert.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Available ist (1, 1). P hält (1, 0) bei Max (2, 1); Q hält (0, 2) bei Max (1, 3); R hält (1, 0) bei Max (3, 2). In welcher sicheren Folge können alle drei fertig werden?
  1. R, P, Q.
  2. P, R, Q.
  3. Q, R, P.
  4. P, Q, R.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Available ist (0, 0). A hält (1, 0), fordert aktuell (0, 0) an und hat einen verbleibenden maximalen Need von (0, 2). B hält (0, 2) und fordert aktuell (1, 0) an. Findet die Erkennung anhand aktueller Anfragen eine Verklemmung?
  1. Ja; kein maximaler Need passt zum anfänglichen Available.
  2. Nein; A gibt (1, 0) frei, sodass die Anfrage von B erfüllt werden kann.
  3. Ja; die aktuelle Anfrage von B kann nicht sofort erfüllt werden.
  4. Nein; beide Anfragen passen bereits zum anfänglichen Available.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein Entwickler ersetzt das wiederholte Durchsuchen ausstehender Ein-/Ausgabeanfragen durch eine Abschlusswarteschlange. Welche Arbeit kann die Schleife nun direkt zur Ausführung übergeben?
  1. Endgültige Antworten für jede neu eingereichte Anfrage.
  2. Folgearbeit für als abgeschlossen gemeldete Operationen.
  3. Lesevorgänge für jede von select als bereit gemeldete Datei.
  4. Parallele Ausführung aller Behandlungsroutinen auf einem Schleifenthread.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Ein Ringpuffer mit Kapazität zwei ist anfangs leer. Füge 31 und 32 ein, entnimm einmal und füge 33 ein. Beide Indizes sind nun 1. Wie lauten der Belegungszähler und der Rückgabewert der nächsten Entnahme?
  1. Zähler 1; nächster Wert 32.
  2. Zähler 2; nächster Wert 33.
  3. Zähler 0; Entnahme muss warten.
  4. Zähler 2; nächster Wert 32.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe

Schlüsselbegriffe

90 Schlüsselbegriffe, jeweils mit Definition

A

Abschlusswarteschlange
Eine Abschlusswarteschlange liefert Datensätze für abgeschlossene asynchrone Operationen, sodass ein Programm Ergebnisse abholen und Folgearbeit zur Ausführung übergeben kann.
Die Schleife liest einen Abschlussdatensatz, statt jeden ausstehenden Lesevorgang wiederholt zu prüfen.
Arbeitsvektor
Ein Arbeitsvektor ist die simulierte Verfügbarkeit der Sicherheitsprüfung. Er wird um die Allocation eines Threads erhöht, sobald dieser als abgeschlossen markiert wird.
Der Abschluss eines Threads, der (2, 1) hält, ändert Work (1, 3) zu (3, 4).
Asynchrone Ein-/Ausgabe
Asynchrone Ein-/Ausgabe trennt die Einreichung einer Operation von der Abholung ihres abgeschlossenen Ergebnisses, sodass der Aufrufer inzwischen andere Arbeit bedienen kann.
Die Behandlungsroutine reicht einen Dateilesevorgang ein und sendet die Antwort, nachdem dessen Abschluss eintrifft.
Atomares Freigeben und Warten
Atomares Freigeben und Warten macht die Freigabe des Mutex und den Beginn des Wartens an einer Bedingungsvariable zu einer unteilbaren Übergabe an konkurrierende Threads.
Der Kindthread kann nicht in einer Lücke signalisieren, nachdem der Elternthread die Sperre freigegeben hat, aber bevor er zu warten beginnt.
Atomaritätsverletzung
Eine Atomaritätsverletzung erlaubt einem anderen Thread, Zugriffe zu unterbrechen, die sich wie eine einzige unteilbare Operation verhalten müssen.
Zwei getrennte gesperrte Bereiche für eine Kontostandprüfung und eine Abbuchung lassen weiterhin eine unsichere Lücke.

B

Bankieralgorithmus
Der Bankieralgorithmus bewilligt eine Ressourcenanfrage nur, wenn sie zu den angegebenen verbleibenden Ansprüchen passt und eine sichere Abschlussfolge erhält.
Ein Allokator bewilligt eine Anfrage probeweise, prüft den resultierenden Zustand und verzögert eine unsichere Zuteilung.
Bedingungsvariable
Eine Bedingungsvariable stellt eine Warteschlange bereit, in der Threads blockierend warten, bis eine Änderung des gemeinsamen Zustands es ihnen ermöglicht, ihre Bedingung erneut zu prüfen.
Ein Verbraucher wartet an einer Bedingungsvariable, wenn die Anfragenwarteschlange keine Arbeit enthält.
Belegen und Anfordern
Belegen und Anfordern liegt vor, wenn ein Thread bereits gehaltene Ressourcen behält, während er blockierend auf zusätzlich benötigte Ressourcen wartet.
Eine Übertragung behält eine Kontosperre, während sie auf den Erwerb der anderen Kontosperre wartet.
Belegungssemaphor
Ein Belegungssemaphor zählt bereitgestellte Pufferelemente, die Verbraucher reservieren können, bevor sie sie aus dem gemeinsamen Array entnehmen.
Ein anfangs leerer Puffer gibt seinem Belegungssemaphor null Erlaubnisse.
Bereitschaftsflag
Ein Bereitschaftsflag hält eine abgeschlossene Initialisierung unter Synchronisierung fest und lässt Nutzer warten, bis der erforderliche Zustand vorhanden ist.
Der erzeugende Thread setzt ready erst nach der Zuweisung des Datensatzes, den der Worker verwenden wird.
Beschränkter Puffer
Ein beschränkter Puffer ist eine gemeinsame Warteschlange fester Kapazität, die Erzeuger bei vollem und Verbraucher bei leerem Puffer warten lässt.
Ein Puffer mit Kapazität drei kann kein viertes Element aufnehmen, bevor ein Verbraucher eines entnimmt.
Binärer Semaphor
Ein als Sperre verwendeter binärer Semaphor beginnt mit einer Erlaubnis und verbindet deren Erwerb vor einem kritischen Abschnitt mit ihrer Freigabe danach.
Ein zweiter eintretender Thread wird blockiert, bis der erste die Erlaubnis für den kritischen Abschnitt zurückgibt.
Broadcast
Ein Broadcast weckt alle aktuell an einer Bedingungsvariable wartenden Threads auf; jeder muss den Mutex erneut erwerben und sein eigenes Prädikat prüfen.
Ein Allokator führt nach einer Freigabe einen Broadcast aus, weil wartende Threads unterschiedliche Speichermengen anfordern.

C

Compare-and-Swap
Compare-and-Swap ersetzt den Wert einer Speicherstelle atomar nur dann, wenn ihr aktueller Wert dem vom Aufrufer angegebenen erwarteten Wert entspricht.
Ein Thread zur Zähleraktualisierung versucht es erneut, wenn ein anderer Thread den alten Wert ändert, bevor sein CAS erfolgreich ist.

D

Datenendmarke
Eine Datenendmarke ist ein reserviertes Warteschlangenelement, das einem Verbraucher anzeigt, dass er nach der Verarbeitung aller vorherigen regulären Elemente stoppen soll.
Ein Koordinator wartet mit Join auf jeden Erzeuger, bevor er pro Verbraucher eine Stoppmarke einfügt.
Deskriptormenge
Eine Deskriptormenge stellt zur Überwachung ausgewählte Dateideskriptornummern dar, wobei Bits die Zugehörigkeit oder zurückgemeldete Bereitschaft anzeigen.
Der Server baut seine Lesemenge erneut auf, weil select() sie durch die bereite Teilmenge ersetzt.
Drosselung
Drosselung begrenzt den gleichzeitigen Eintritt in eine aufwendige Phase, damit der gemeinsame Bedarf innerhalb eines gewählten Ressourcenbudgets bleibt.
Zwölf gleichzeitige Worker mit jeweils 500 MB nutzen während ihrer speicherintensiven Phase 6.000 MB.

E

Ein-/Ausgabebereitschaft
Ein-/Ausgabebereitschaft meldet, dass eine Ein-/Ausgabeoperation für einen Deskriptor versucht werden kann; sie überträgt die Daten nicht in das Programm.
Ein lesebereiter Socket kann beim Lesen Nutzdaten zurückgeben oder das Ende der Eingabe melden.
epoll
epoll ist eine Linux-Ereignisschnittstelle, die eine Interessensmenge im Kernel hält und bereite Ereignisse für registrierte Deskriptoren meldet.
Ein Server kann viele Verbindungen überwachen, ohne jedes Mal die vollständige überwachte Liste erneut zu übergeben.
Ereignisschleife
Eine Ereignisschleife wartet auf Ereignisse und führt kurze Behandlungsroutinen aus, sodass ein Thread viele Aufgaben koordinieren kann, deren Wartezeiten sich überlappen.
Ein Server behandelt Daten eines bereiten Sockets und kehrt dann zurück, um eine andere Verbindung zu bedienen.
Erlaubnis
Eine Erlaubnis steht für einen verfügbaren Zugang oder eine Ressourceneinheit, die eine erfolgreiche P-Operation verbraucht und eine V-Operation bereitstellt.
Ein Semaphor mit drei ungenutzten Erlaubnissen kann drei Threads zulassen, ohne sie zu blockieren.
Erzeuger-Verbraucher-Problem
Das Erzeuger-Verbraucher-Problem koordiniert Threads, die Elemente in einen gemeinsamen Puffer begrenzter Kapazität einfügen und daraus entnehmen.
Ein Netzwerkempfänger erzeugt Anfragen, die Worker-Threads aus einer beschränkten Warteschlange entnehmen.

F

Fairer Zugang
Fairer Zugang gewährt Zugriff nach einer Strategie, die verhindert, dass eine fortlaufend eintreffende Gruppe einen wartenden Thread unbegrenzt überholt.
Eine faire Lese-Schreib-Strategie verhindert, dass spätere Leser einen früheren Schreiber endlos überholen.
Fetch-and-Add
Fetch-and-Add ist eine atomare Read-Modify-Write-Operation, die einen Betrag addiert und den alten Wert zurückgibt, ohne eine dazwischenliegende Aktualisierung zuzulassen.
Ein atomares Fetch-and-Add verhindert, dass gleichzeitige Zählererhöhungen einander überschreiben.
Fortsetzung
Eine Fortsetzung speichert den Zustand, der zur Fortsetzung einer Aufgabe benötigt wird, nachdem eine Behandlungsroutine während des Wartens auf eine asynchrone Operation zurückgekehrt ist.
Die Leseanfrage speichert, welcher Client-Socket ihre Daten erhalten soll.
Freiplatzsemaphor
Ein Freiplatzsemaphor zählt Erlaubnisse zur Reservierung aktuell verfügbarer Pufferplätze, bevor Erzeuger ihre fertigen Elemente einfügen.
Ein leerer Puffer mit Kapazität fünf initialisiert seinen Freiplatzsemaphor mit fünf.

H

Hoare-Semantik
Bei der Hoare-Semantik werden die Sperre und die Ausführung direkt an einen benachrichtigten wartenden Thread übertragen, sodass die signalisierte Bedingung bei der Übergabe erhalten bleibt.
Der benachrichtigte Thread wird fortgesetzt, bevor ein anderer eintretender Thread das neu verfügbare Element entnehmen kann.
Hybride Nebenläufigkeit
Hybride Nebenläufigkeit kombiniert eine Ereignisschleife mit Workern, die blockierende oder CPU-intensive Operationen außerhalb des Schleifenthreads ausführen.
Eine Netzwerkschleife sendet Bildverarbeitung an Worker und behandelt später deren Abschlüsse.

I

Implizites Blockieren
Implizites Blockieren verzögert einen Thread durch eine unmittelbar erscheinende Operation, etwa den Zugriff auf eine Seite, die von der Festplatte gelesen werden muss.
Eine Behandlungsroutine gerät bei einem Seitenfehler ins Stocken, obwohl sie keinen ausdrücklichen blockierenden Leseaufruf ausführt.
io_uring
io_uring ist eine Linux-Schnittstelle mit Einreichungs- und Abschlusswarteschlangen für asynchrone Operationen, die eine abschlussgesteuerte Ein-/Ausgabebehandlung unterstützt.
Ein Server reicht Anfragen ein und führt später anhand ihrer Abschlussdatensätze Fortsetzungen aus.

J

Join
Ein Join wartet auf das Ende eines bestimmten Threads, bevor der wartende Thread mit Arbeit fortfährt, die dessen Abschluss benötigt.
Der Berichtsthread wartet mit Join auf den summierenden Worker, bevor er dessen Endsumme liest.

K

Kontrollblock für asynchrone Ein-/Ausgabe
Ein Kontrollblock für asynchrone Ein-/Ausgabe beschreibt eine POSIX-AIO-Anfrage, einschließlich Deskriptor, Dateioffset, Puffer und angeforderter Byteanzahl.
Der aiocb und sein Zielpuffer bleiben gültig, während der eingereihte Lesevorgang noch aussteht.
kqueue
kqueue ist eine Ereignisbenachrichtigungsschnittstelle unter FreeBSD und macOS, die registrierte Interessen verwaltet und gemeldete Ereignisse zurückgibt.
Ein Server registriert Socketinteressen bei kqueue, statt vollständige select-Deskriptormengen neu aufzubauen.

L

Lese-Schreib-Sperre
Eine Lese-Schreib-Sperre erlaubt mehreren Lesern gemeinsamen Zugriff oder einem Schreiber exklusiven Zugriff auf geschützte Daten.
Mehrere Cache-Abfragen können sich überlappen, aber das Aktualisieren des Caches erfordert Schreibzugriff.
Leserbevorzugung
Leserbevorzugung lässt eintreffende Leser einer aktiven Lesergruppe beitreten, selbst wenn bereits ein Schreiber auf Zugriff wartet.
Leser, deren Zugriffe sich überlappen, können den Leserzähler über null halten, während ein Schreiber wartet.
Livelock
Bei einem Livelock laufen Threads weiter und reagieren aufeinander, während ihre wiederholten Handlungen keine nützliche Arbeit abschließen.
Zwei Trylock-Schleifen geben wiederholt gemeinsam frei und versuchen es erneut, ohne dass eine beide Sperren erhält.

M

Manuelle Stackverwaltung
Manuelle Stackverwaltung bewahrt lokalen Aufgabenzustand ausdrücklich über die Rückkehr von Behandlungsroutinen hinweg, statt sich auf einen suspendierten Aufrufstack zu verlassen.
Ein Antwortdatensatz hält die Anfrageposition und das Ziel fest, bis der Lesevorgang abgeschlossen ist.
Mehrkernparallelität
Mehrkernparallelität führt Berechnungen gleichzeitig auf mehreren CPU-Kernen aus und benötigt dafür mehr ausführende Worker als einen Schleifenthread.
Mehrere Bildverarbeitungs-Worker können gleichzeitig rechnen, während eine Ereignisschleife Sockets behandelt.
Mesa-Semantik
Bei der Mesa-Semantik wird ein benachrichtigter wartender Thread bereit, um seine Sperre zu konkurrieren. Nach dem erneuten Erwerb muss er daher die Bedingung erneut prüfen.
Ein zweiter Verbraucher kann das einzige Element entnehmen, bevor der benachrichtigte erste Verbraucher den Mutex zurückerhält.
Monitor
Ein Monitor bündelt gemeinsamen Zustand mit Operationen, die jeweils einen Thread zulassen, sowie mit bedingungsbasiertem Warten.
Ein synchronized-Block in Java hält den Objektmonitor, der für Aufrufe von wait() benötigt wird.

N

Nicht blockierende Ein-/Ausgabe
Nicht blockierende Ein-/Ausgabe gibt die Kontrolle zurück, wenn eine Operation nicht sofort fortfahren kann, sodass der aufrufende Thread andere Arbeit behandeln kann.
Ein Socket-Leseaufruf gibt EAGAIN zurück und die Behandlungsroutine kehrt zur Ereignisschleife zurück.
Nichtentziehbarkeit
Nichtentziehbarkeit bedeutet, dass eine gehaltene Ressource ihrem Halter nicht zwangsweise entzogen werden kann und entsprechend ihrem Protokoll freigegeben werden muss.
Ein auf einen gewöhnlichen Mutex wartender Thread kann die Sperre ihrem aktuellen Besitzer nicht einfach wegnehmen.

O

Opfer bei der Verklemmungsbehebung
Ein Opfer bei der Verklemmungsbehebung ist ausgewählte Arbeit, die ein System abbricht oder zurücksetzt, um Ressourcen freizugeben und eine erkannte Verklemmung aufzulösen.
Ein Prozess mit wenig abgeschlossener Arbeit kann sich kostengünstiger neu starten lassen als ein fast fertiger Prozess.

P

P-Operation
Eine P-Operation erwirbt atomar eine Erlaubnis und blockiert, wenn keine verfügbar ist, bis eine andere Operation eine bereitstellt.
Ein Entleiher führt vor der Entnahme eines Handles eine P-Operation aus, damit die Zugangsgrenze des Pools nicht überschritten wird.
poll
poll() überwacht ein Array von Deskriptoreinträgen auf angeforderte Ein-/Ausgabeereignisse, ohne die feste Deskriptorwertbegrenzung von fd_set.
Ein hoch nummerierter Socket kann mit poll überwacht werden, selbst wenn sein Wert nicht in eine select-Menge passt.
Prädikat
Ein Prädikat ist eine wahre oder falsche Bedingung für den gemeinsamen Zustand, die ein Thread prüft, bevor er entscheidet, ob er fortfahren kann.
Das Prädikat done != 0 lässt einen Elternthread prüfen, ob sein Kindthread fertig ist.
Prüfen und anschließendes Handeln
Eine Operation aus Prüfen und anschließendem Handeln prüft gemeinsamen Zustand und ändert oder verwendet ihn danach auf eine Weise, die von der fortdauernden Gültigkeit der Prüfung abhängt.
Die Prüfung und Reservierung des Bestands müssen zusammenbleiben, wenn andere Threads denselben Artikel verkaufen können.
Puffermutex
Ein Puffermutex serialisiert Änderungen am Pufferarray und an den Indizes, unabhängig von Semaphoren, die verfügbare Ressourcen zählen.
Zwei Erzeuger mit gültigen Platzerlaubnissen aktualisieren den Schreibindex des Puffers, fill_ptr, weiterhin unter demselben Mutex.

R

Reihenfolgeverletzung
Eine Reihenfolgeverletzung tritt auf, wenn eine Operation vor einer erforderlichen früheren Operation ausgeführt wird, weil ihre Abhängigkeit nicht durchgesetzt wird.
Ein Worker liest eine Konfiguration, bevor die Laderoutine die Werte vollständig bekannt gegeben hat.
Rendezvous
Ein Rendezvous ist ein Treffpunkt, den keiner von zwei Threads passieren darf, bevor beide ihre Ankunft gemeldet haben.
Jeder Thread meldet seine Ankunft durch eine V-Operation, bevor er auf die Ankunftserlaubnis des anderen Threads wartet.
Ressourcenreservierung
Eine Ressourcenreservierung weist einer zugelassenen Operation eine Ressourceneinheit zu, noch bevor diese Operation gemeinsame Verwaltungsdaten aktualisiert.
Die erfolgreiche P-Operation eines Erzeugers auf dem Freiplatzsemaphor reserviert einen Platz, während er auf den Puffermutex wartet.
Restbedarfsvektor
Ein Restbedarfsvektor hält den verbleibenden maximalen Anspruch eines Threads für jeden Ressourcentyp fest und wird als Max minus aktuelle Allocation berechnet.
Ein Thread mit Max (4, 3) und Allocation (1, 2) hat Need (3, 1).
Ringpuffer
Ein Ringpuffer speichert eine Warteschlange in einem Array, dessen Lese- und Schreibindizes vom letzten Platz auf den ersten zurückspringen.
Nach dem Schreiben auf Platz 2 eines Arrays mit drei Plätzen springt die nächste Schreibposition auf Platz 0 zurück.
Rücksetzen
Rücksetzen stellt einen früheren gültigen Prüfpunkt wieder her, sodass Arbeit Ressourcen freigeben oder zurückgewinnen kann, ohne teilweise Änderungen zu bewahren.
Ein Transaktionssystem bricht eine verklemmte Transaktion ab und versucht sie mithilfe seines Rücksetzmechanismus erneut.

S

Schreiberbevorzugung
Schreiberbevorzugung blockiert neue Leser, wenn ein Schreiber wartet. Das verringert die Zurückstellung von Schreibern, kann aber Leser verhungern lassen.
Eine eintreffende Abfrage wartet hinter dem Cache-Schreiber, statt sich der aktuellen Lesergruppe anzuschließen.
select
select() überwacht Deskriptormengen auf Ein-/Ausgabebereitschaft, ersetzt sie durch bereite Teilmengen und beschränkt die Prüfung auf Deskriptornummern unter nfds.
Die Überwachung der Deskriptoren 5 und 12 erfordert nfds 13 und den erneuten Aufbau der überwachten Mengen nach jedem Aufruf.
Semaphor
Ein Semaphor ist ein ganzzahlbasiertes Synchronisierungsobjekt mit atomaren P- und V-Operationen zum Erwerb und zur Bereitstellung von Erlaubnissen.
Eine V-Operation kann vor einer P-Operation erfolgen, weil der Semaphor die ungenutzte Erlaubnis speichert.
Semaphorbesitz
Ein Semaphor hat keinen Besitzer, sodass jeder Thread eine Erlaubnis durch eine V-Operation bereitstellen kann, selbst wenn ein anderer Thread die zugehörige P-Operation ausgeführt hat.
Ein Kindthread führt eine V-Operation auf einem Abschlusssemaphor aus, auf den sein Elternthread wartet.
Sichere Folge
Eine sichere Folge ordnet alle Threads so, dass jeder verbleibende maximale Anspruch zu den Ressourcen passt, die nach Abschluss früherer Threads verfügbar sind.
Jeder Schritt einer vorgeschlagenen Folge muss für jeden Ressourcentyp passen, nicht nur für die Gesamtanzahl.
Sicherer Zustand
Ein sicherer Zustand hat mindestens eine Abschlussreihenfolge, die den verbleibenden maximalen Bedarf jedes Threads mit verfügbaren und freigegebenen Ressourcen erfüllen kann.
Ein Thread kann zuerst fertig werden und genügend Ressourcen freigeben, damit ein zweiter Thread danach abschließen kann.
Signalisierung
Eine Signalisierung benachrichtigt mindestens einen aktuell an einer Bedingungsvariable wartenden Thread, sofern Threads warten, ohne eine Benachrichtigung für später eintreffende Threads zu speichern.
Ein Kindthread signalisiert nach dem Setzen seines geschützten Abschlussflags, damit ein wartender Elternthread es erneut prüfen kann.
Sperraufwertung
Eine Sperraufwertung wechselt von Lesezugriff zu exklusivem Schreibzugriff; im grundlegenden Entwurf muss der Lesezugriff zuerst freigegeben werden.
Nach dem erneuten Erwerb von Schreibzugriff prüft ein Thread zur Cache-Aktualisierung, ob der Eintrag noch geändert werden muss.
Sperrreihenfolge
Eine Sperrreihenfolge legt eine gemeinsame Erwerbsreihenfolge für Sperren fest und verlangt, dass jeder relevante Codepfad sie einhält.
Beide Richtungen einer Übertragung sperren das Konto mit der niedrigeren Nummer vor dem mit der höheren.
Spontanes Aufwachen
Spontanes Aufwachen ist die Rückkehr aus einem Warteaufruf an einer Bedingungsvariable ohne zugehörige Benachrichtigung. Wie bei jedem Aufwachen muss das Prädikat erneut geprüft werden.
Ein Verbraucher kehrt in der Schleife zum Warten zurück, wenn er aufwacht, den Puffer aber weiterhin leer vorfindet.
Suspendierung
Eine Suspendierung pausiert eine Funktion, bis eine erwartete Operation abgeschlossen ist, und erlaubt der Ereignisschleife gleichzeitig, andere bereite Arbeit auszuführen.
Eine asynchrone Funktion wird nach Abschluss ihres Lesevorgangs mit erhaltenen lokalen Variablen fortgesetzt.

T

Trylock
Ein Trylock versucht, einen Mutex zu erwerben, und kehrt sofort zurück, wenn dieser nicht verfügbar ist, sodass der Aufrufer freigeben und es erneut versuchen kann.
Ein fehlgeschlagener zweiter Trylock veranlasst die Übertragung, ihre erste Kontosperre freizugeben.

Ü

Überdeckende Bedingung
Eine überdeckende Bedingung nutzt eine breite Benachrichtigung, um jeden wartenden Thread zu erreichen, dessen spezifischere Bedingung nun erfüllt sein könnte.
Gibt ein Allokator 50 Bytes frei, während Threads auf 10 und 100 Bytes warten, weckt er beide auf, damit jeder erneut prüfen kann.

U

Unsicherer Zustand
Ein unsicherer Zustand hat keine garantierte Abschlussfolge für den maximalen Bedarf, obwohl das tatsächliche zukünftige Verhalten eine Verklemmung noch vermeiden kann.
Ein Thread könnte Ressourcen frühzeitig freigeben, statt seinen gesamten verbleibenden Anspruch anzufordern.

V

V-Operation
Eine V-Operation stellt atomar eine Erlaubnis bereit und lässt damit einen blockiert wartenden Thread fortfahren oder speichert die Verfügbarkeit für eine spätere P-Operation.
Ein Worker führt eine V-Operation auf seinem Abschlusssemaphor aus, selbst wenn sein Elternthread noch nicht zu warten begonnen hat.
Verfügbarkeitsvektor
Ein Verfügbarkeitsvektor zählt die aktuell nicht zugeteilten Einheiten jedes Ressourcentyps und bildet den Anfangswert von Work für eine Sicherheitsprüfung.
Subtrahiere alle gehaltenen Einheiten von der Gesamtmenge jedes Ressourcentyps, bevor du die Abschlusssimulation beginnst.
Verhungern
Verhungern stellt einen Thread unbegrenzt zurück, während andere weiter nützliche Arbeit leisten, obwohl kein blockierter Abhängigkeitszyklus sie alle festsetzt.
Ein Schreiber wartet weiter, während ein ununterbrochener Strom von Lesern mit überlappenden Zugriffen die Ressource nutzt.
Verhungern von Schreibern
Verhungern von Schreibern ist das unbegrenzte Zurückstellen eines wartenden Schreibers, während Leser weiterhin eintreten und Fortschritte machen.
Eine Sperre, die nur Leser zählt, gibt einem Schreiber keine Gelegenheit, wenn sich neue Leserzugriffe ständig überlappen.
Verklemmung
Eine Verklemmung setzt eine Gruppe blockierter Threads fest, weil jeder eine Handlung oder Ressource benötigt, die nur ein anderes festgesetztes Mitglied bereitstellen kann.
T1 hält A und wartet auf B, während T2 B hält und auf A wartet.
Verklemmungserkennung
Die Verklemmungserkennung durchsucht aktuelle Warteabhängigkeiten oder Ressourcenanfragen nach einer bestehenden Gruppe, die keinen Fortschritt machen kann.
Eine Zyklussuche findet drei Mutex-Halter, die einander blockieren.
Verklemmungsvermeidung
Verklemmungsvermeidung prüft vorgeschlagene Ressourcenzuteilungen anhand vorab bekannter Bedarfsinformationen und verzögert Zuteilungen, die das System unsicher machen würden.
Eine freie Ressource kann zurückgehalten werden, wenn ihre Zuteilung jede sichere Abschlussfolge beseitigen würde.
Verklemmungsvorbeugung
Verklemmungsvorbeugung gestaltet den Ressourcenerwerb so, dass mindestens eine notwendige Bedingung für Verklemmungen nie auftreten kann.
Eine gemeinsame Kontosperrreihenfolge verhindert, dass Übertragungen in entgegengesetzter Richtung zyklisches Warten erzeugen.
Verlorenes Update
Ein verlorenes Update tritt auf, wenn sich überlappende Read-Modify-Write-Operationen gegenseitig überschreiben, sodass eine beabsichtigte Änderung verloren geht.
Zwei Threads laden den Zählerstand 8 und speichern jeweils 9, obwohl zwei Erhöhungen 10 ergeben sollten.
Verlorenes Wecksignal
Ein verlorenes Wecksignal entsteht, wenn eine benötigte Benachrichtigung erfolgt, bevor ein Thread zu warten beginnt, sodass er ohne eine spätere Benachrichtigung blockiert bleibt.
Prüft ein Elternthread ein Abschlussflag außerhalb des Mutex, kann der Kindthread zwischen der Prüfung und dem Warten fertig werden.
Verzahnung
Eine Verzahnung ist eine Reihenfolge von Schritten verschiedener Threads, die die eigene Reihenfolge jedes Threads bewahrt und dabei deren Zugriffe mischt.
Wird das Zurücksetzen eines Zeigers zwischen die Prüfung und die Verwendung durch einen anderen Thread gesetzt, zeigt sich der unsichere Ablauf.

W

Wartegraph
Ein Wartegraph stellt Threads als Knoten und Warteabhängigkeiten als gerichtete Kanten vom wartenden Thread zu einem Ressourcenhalter dar.
Ein Pfeil von T1 zu T2 bedeutet, dass T1 eine Ressource benötigt, die T2 aktuell hält.
Watchdog-Timer
Ein Watchdog-Timer löst eine Wiederherstellung aus, wenn Software innerhalb einer Frist keinen Fortschritt meldet, ohne die Ursache eindeutig zu diagnostizieren.
Eine eingebettete Steuerung wird zurückgesetzt, nachdem ihre Software die erwarteten Fortschrittsmeldungen nicht mehr sendet.
Wechselseitiger Ausschluss
Wechselseitiger Ausschluss erlaubt jeweils nur einem Thread, eine Ressource zu halten oder eine geschützte Operation auf gemeinsamem Zustand auszuführen.
Ein exklusiver Mutex kann keinem zweiten Thread gewährt werden, während sein Besitzer ihn noch hält.
Worker-Pool
Ein Worker-Pool ist eine begrenzte Gruppe wiederverwendbarer Threads oder Prozesse, die eingereichte Aufgaben ausführen und deren Ergebnisse zurückgeben.
libuv übergibt Dateisystemoperationen an seinen Threadpool, während die Schleife Ereignisse bedient.

Z

Zählender Semaphor
Ein zählender Semaphor beginnt mit der Anzahl verfügbarer gleichartiger Einheiten und begrenzt, wie viele Nutzer sie gleichzeitig erwerben können.
Ein Verbindungspool mit vier Erlaubnissen lässt einen fünften Entleiher auf eine zurückgegebene Verbindung warten.
Zählermodell mit Vorzeichen
Das Zählermodell mit Vorzeichen stellt blockiert auf einen Semaphor wartende Threads durch negative Werte dar und erhält dabei dasselbe für Threads sichtbare Verhalten der P- und V-Operationen.
Ein Ablaufwert von −2 steht für zwei blockiert wartende Threads, nicht für eine negative Anzahl von Erlaubnissen unter Linux.
Zeitüberschreitung
Eine Zeitüberschreitung beendet ein Warten nach Ablauf einer Frist und zeigt übermäßige Verzögerung an, ohne eine dauerhafte Verklemmung von langsamem Fortschritt zu unterscheiden.
Ein zeitlich begrenzter Mutex-Warteaufruf kann sowohl wegen eines überlasteten Systems als auch wegen eines Sperrzyklus ablaufen.
Zufälliges Backoff
Zufälliges Backoff fügt vor einem erneuten Versuch eine zufällig gewählte Verzögerung ein und verringert so wiederholte Kollisionen zwischen sonst gleichzeitigen Konkurrenten.
Übertragungen warten vor einem erneuten Versuch unterschiedlich lange, sodass sie seltener erneut kollidieren.
Zulassungskontrolle
Die Zulassungskontrolle entscheidet, ob weitere Arbeit in einen ressourcenbegrenzten Bereich eintreten darf, und verzögert sie, wenn die erlaubte Kapazität belegt ist.
Ein Worker wartet auf eine Verarbeitungserlaubnis, bevor er seinen großen temporären Puffer reserviert.
Zustandsvariable
Eine Zustandsvariable hält eine Tatsache über gemeinsame Arbeit fest, sodass ein Thread sie auch nach einer früheren Benachrichtigung noch prüfen kann.
Ein Abschlussflag bleibt gesetzt, wenn der Kindthread fertig wird, bevor der Elternthread zu warten beginnt.
Zyklisches Warten
Zyklisches Warten ist eine geschlossene Kette von Threads, in der jeder auf eine Ressource wartet, die der nächste Thread der Kette hält.
Drei Threads mit Warteabhängigkeiten von A zu B, B zu C und C zu A schließen den Abhängigkeitszyklus.

Über diese Einheit

Von StudeliFehler melden

Erstelle deine eigenen Lernmaterialien

Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.

Jetzt starten