Ein umfassender Leitfaden für globale Entwickler zur Parallelitätskontrolle. Erkunden Sie Lock-basierte Synchronisation, Mutexe, Semaphore, Deadlocks und Best Practices.
Beherrschung von Parallelität: Ein tiefer Einblick in die Lock-basierte Synchronisation
Stellen Sie sich eine geschäftige Profiküche vor. Mehrere Köche arbeiten gleichzeitig und benötigen alle Zugang zu einer gemeinsamen Vorratskammer mit Zutaten. Wenn zwei Köche im selben Moment versuchen, das letzte Glas eines seltenen Gewürzes zu ergattern, wer bekommt es? Was ist, wenn ein Koch eine Rezeptkarte aktualisiert, während ein anderer sie liest, was zu einer halb geschriebenen, unsinnigen Anweisung führt? Dieses Küchenchaos ist eine perfekte Analogie für die zentrale Herausforderung in der modernen Softwareentwicklung: Parallelität.
In der heutigen Welt der Multi-Core-Prozessoren, verteilten Systeme und hochreaktionsschnellen Anwendungen ist Parallelität – die Fähigkeit verschiedener Teile eines Programms, in beliebiger Reihenfolge oder in Teilreihenfolge auszuführen, ohne das Endergebnis zu beeinträchtigen – kein Luxus, sondern eine Notwendigkeit. Sie ist der Motor hinter schnellen Webservern, reibungslosen Benutzeroberflächen und leistungsstarken Datenverarbeitungspipelines. Diese Leistung bringt jedoch erhebliche Komplexität mit sich. Wenn mehrere Threads oder Prozesse gleichzeitig auf gemeinsame Ressourcen zugreifen, können sie sich gegenseitig stören, was zu beschädigten Daten, unvorhersehbarem Verhalten und kritischen Systemausfällen führen kann. Hier kommt die Parallelitätskontrolle ins Spiel.
Dieser umfassende Leitfaden untersucht die grundlegendste und am weitesten verbreitete Technik zur Bewältigung dieses kontrollierten Chaos: Lock-basierte Synchronisation. Wir werden entmystifizieren, was Locks sind, ihre verschiedenen Formen untersuchen, ihre gefährlichen Fallstricke durchgehen und eine Reihe globaler Best Practices für das Schreiben von robustem, sicherem und effizientem parallelem Code festlegen.
Was ist Parallelitätskontrolle?
Im Kern ist die Parallelitätskontrolle eine Disziplin innerhalb der Informatik, die sich der Verwaltung gleichzeitiger Operationen auf gemeinsam genutzten Daten widmet. Ihr Hauptziel ist es, sicherzustellen, dass parallele Operationen korrekt ausgeführt werden, ohne sich gegenseitig zu stören, wodurch die Datenintegrität und -konsistenz erhalten bleiben. Stellen Sie es sich als den Küchenchef vor, der Regeln für den Zugriff der Köche auf die Vorratskammer festlegt, um Verschüttungen, Verwechslungen und verschwendete Zutaten zu vermeiden.
In der Welt der Datenbanken ist die Parallelitätskontrolle unerlässlich, um die ACID-Eigenschaften (Atomarität, Konsistenz, Isolation, Dauerhaftigkeit) aufrechtzuerhalten, insbesondere Isolation. Die Isolation stellt sicher, dass die gleichzeitige Ausführung von Transaktionen zu einem Systemzustand führt, der erreicht würde, wenn Transaktionen seriell, eine nach der anderen, ausgeführt würden.
Es gibt zwei Hauptphilosophien zur Implementierung der Parallelitätskontrolle:
- Optimistische Parallelitätskontrolle: Dieser Ansatz geht davon aus, dass Konflikte selten sind. Sie ermöglicht Operationen ohne vorherige Überprüfungen. Vor dem Festschreiben einer Änderung überprüft das System, ob eine andere Operation die Daten in der Zwischenzeit geändert hat. Wenn ein Konflikt erkannt wird, wird die Operation in der Regel zurückgesetzt und erneut versucht. Es ist eine Strategie des "Bitte um Vergebung, nicht um Erlaubnis".
- Pessimistische Parallelitätskontrolle: Dieser Ansatz geht davon aus, dass Konflikte wahrscheinlich sind. Sie zwingt eine Operation, ein Lock für eine Ressource zu erwerben, bevor sie darauf zugreifen kann, wodurch andere Operationen nicht stören können. Es ist eine Strategie des "Bitte um Erlaubnis, nicht um Vergebung".
Dieser Artikel konzentriert sich ausschlieĂźlich auf den pessimistischen Ansatz, der die Grundlage der Lock-basierten Synchronisation bildet.
Das Kernproblem: Race Conditions
Bevor wir die Lösung schätzen können, müssen wir das Problem vollständig verstehen. Der häufigste und tückischste Fehler in der parallelen Programmierung ist die Race Condition. Eine Race Condition tritt auf, wenn das Verhalten eines Systems von der unvorhersehbaren Reihenfolge oder dem Timing unkontrollierbarer Ereignisse abhängt, wie z. B. der Planung von Threads durch das Betriebssystem.
Betrachten wir das klassische Beispiel: ein gemeinsames Bankkonto. Angenommen, ein Konto hat einen Saldo von 1000 US-Dollar und zwei parallele Threads versuchen, jeweils 100 US-Dollar einzuzahlen.
Hier ist eine vereinfachte Abfolge von Operationen fĂĽr eine Einzahlung:
- Lesen Sie den aktuellen Saldo aus dem Speicher.
- Addieren Sie den Einzahlungsbetrag zu diesem Wert.
- Schreiben Sie den neuen Wert zurĂĽck in den Speicher.
Eine korrekte, serielle AusfĂĽhrung wĂĽrde zu einem Endsaldo von 1200 US-Dollar fĂĽhren. Aber was passiert in einem parallelen Szenario?
Eine mögliche Verschachtelung von Operationen:
- Thread A: Liest den Saldo (1000 US-Dollar).
- Kontextwechsel: Das Betriebssystem pausiert Thread A und fĂĽhrt Thread B aus.
- Thread B: Liest den Saldo (immer noch 1000 US-Dollar).
- Thread B: Berechnet seinen neuen Saldo (1000 US-Dollar + 100 US-Dollar = 1100 US-Dollar).
- Thread B: Schreibt den neuen Saldo (1100 US-Dollar) zurĂĽck in den Speicher.
- Kontextwechsel: Das Betriebssystem setzt Thread A fort.
- Thread A: Berechnet seinen neuen Saldo basierend auf dem zuvor gelesenen Wert (1000 US-Dollar + 100 US-Dollar = 1100 US-Dollar).
- Thread A: Schreibt den neuen Saldo (1100 US-Dollar) zurĂĽck in den Speicher.
Der Endsaldo beträgt 1100 US-Dollar, nicht die erwarteten 1200 US-Dollar. Eine Einzahlung von 100 US-Dollar ist aufgrund der Race Condition in Luft aufgelöst. Der Codeblock, in dem auf die gemeinsam genutzte Ressource (das Kontoguthaben) zugegriffen wird, wird als kritischer Abschnitt bezeichnet. Um Race Conditions zu verhindern, müssen wir sicherstellen, dass nur ein Thread jeweils innerhalb des kritischen Abschnitts ausgeführt werden kann. Dieses Prinzip wird als gegenseitiger Ausschluss bezeichnet.
EinfĂĽhrung in die Lock-basierte Synchronisation
Die Lock-basierte Synchronisation ist der primäre Mechanismus zur Durchsetzung des gegenseitigen Ausschlusses. Ein Lock (auch als Mutex bezeichnet) ist eine Synchronisationsprimitive, die als Schutz für einen kritischen Abschnitt dient.
Die Analogie eines Schlüssels zu einer Einzeltoilette ist sehr treffend. Die Toilette ist der kritische Abschnitt und der Schlüssel ist das Lock. Viele Personen (Threads) warten möglicherweise draußen, aber nur die Person mit dem Schlüssel kann eintreten. Wenn sie fertig sind, verlassen sie den Raum und geben den Schlüssel zurück, sodass die nächste Person in der Schlange ihn nehmen und eintreten kann.
Locks unterstĂĽtzen zwei grundlegende Operationen:
- Acquire (oder Lock): Ein Thread ruft diese Operation auf, bevor er in einen kritischen Abschnitt eintritt. Wenn das Lock verfügbar ist, ruft der Thread es ab und fährt fort. Wenn das Lock bereits von einem anderen Thread gehalten wird, blockiert (oder "schläft") der aufrufende Thread, bis das Lock freigegeben wird.
- Release (oder Unlock): Ein Thread ruft diese Operation auf, nachdem er die AusfĂĽhrung des kritischen Abschnitts abgeschlossen hat. Dadurch wird das Lock fĂĽr andere wartende Threads zum Abrufen verfĂĽgbar.
Indem wir unsere Bankkonto-Logik mit einem Lock umschließen, können wir ihre Korrektheit garantieren:
acquire_lock(account_lock);
// --- Start des kritischen Abschnitts ---
balance = read_balance();
new_balance = balance + amount;
write_balance(new_balance);
// --- Ende des kritischen Abschnitts ---
release_lock(account_lock);
Wenn Thread A das Lock zuerst abruft, wird Thread B gezwungen, zu warten, bis Thread A alle drei Schritte abgeschlossen und das Lock freigegeben hat. Die Operationen werden nicht mehr verschachtelt und die Race Condition wird beseitigt.
Lock-Typen: Der Werkzeugkasten des Programmierers
Während das grundlegende Konzept eines Locks einfach ist, erfordern verschiedene Szenarien verschiedene Arten von Sperrmechanismen. Das Verständnis des Toolkits verfügbarer Locks ist entscheidend für den Aufbau effizienter und korrekter paralleler Systeme.
Mutex (Mutual Exclusion) Locks
Ein Mutex ist der einfachste und gebräuchlichste Lock-Typ. Es ist ein binäres Lock, was bedeutet, dass es nur zwei Zustände hat: gesperrt oder entsperrt. Es wurde entwickelt, um einen strengen gegenseitigen Ausschluss zu erzwingen und sicherzustellen, dass jeweils nur ein Thread das Lock besitzen kann.
- Eigentümerschaft: Ein wichtiges Merkmal der meisten Mutex-Implementierungen ist die Eigentümerschaft. Der Thread, der den Mutex abruft, ist der einzige Thread, der ihn freigeben darf. Dies verhindert, dass ein Thread unbeabsichtigt (oder böswillig) einen kritischen Abschnitt entsperrt, der von einem anderen verwendet wird.
- Anwendungsfall: Mutexe sind die Standardwahl zum Schutz kurzer, einfacher kritischer Abschnitte, wie z. B. das Aktualisieren einer gemeinsam genutzten Variablen oder das Ändern einer Datenstruktur.
Semaphore
Ein Semaphor ist eine allgemeinere Synchronisationsprimitive, die vom niederländischen Informatiker Edsger W. Dijkstra erfunden wurde. Im Gegensatz zu einem Mutex verwaltet ein Semaphor einen Zähler mit einem nicht negativen ganzzahligen Wert.
Es unterstĂĽtzt zwei atomare Operationen:
- wait() (oder P-Operation): Dekrementiert den Zähler des Semaphors. Wenn der Zähler negativ wird, blockiert der Thread, bis der Zähler größer oder gleich Null ist.
- signal() (oder V-Operation): Inkrementiert den Zähler des Semaphors. Wenn Threads auf den Semaphor blockiert sind, wird einer von ihnen freigegeben.
Es gibt zwei Haupttypen von Semaphoren:
- Binärer Semaphor: Der Zähler wird auf 1 initialisiert. Er kann nur 0 oder 1 sein, wodurch er funktional einem Mutex entspricht.
- Zählender Semaphor: Der Zähler kann auf eine beliebige ganze Zahl N > 1 initialisiert werden. Dadurch können bis zu N Threads gleichzeitig auf eine Ressource zugreifen. Er wird verwendet, um den Zugriff auf einen endlichen Pool von Ressourcen zu steuern.
Beispiel: Stellen Sie sich eine Webanwendung mit einem Verbindungspool vor, der maximal 10 gleichzeitige Datenbankverbindungen verarbeiten kann. Ein zählender Semaphor, der auf 10 initialisiert ist, kann dies perfekt verwalten. Jeder Thread muss ein `wait()` für den Semaphor ausführen, bevor er eine Verbindung herstellt. Der 11. Thread blockiert, bis einer der ersten 10 Threads seine Datenbankarbeit beendet und ein `signal()` für den Semaphor ausführt, wodurch die Verbindung zum Pool zurückgegeben wird.
Read-Write Locks (Shared/Exclusive Locks)
Ein häufiges Muster in parallelen Systemen ist, dass Daten viel häufiger gelesen als geschrieben werden. Die Verwendung eines einfachen Mutex in diesem Szenario ist ineffizient, da er verhindert, dass mehrere Threads gleichzeitig die Daten lesen, obwohl das Lesen eine sichere, nicht modifizierende Operation ist.
Ein Read-Write Lock behebt dies, indem er zwei Sperrmodi bereitstellt:
- Shared (Read) Lock: Mehrere Threads können gleichzeitig ein Read Lock abrufen, solange kein Thread ein Write Lock hält. Dies ermöglicht ein hochgradig paralleles Lesen.
- Exclusive (Write) Lock: Jeweils nur ein Thread kann ein Write Lock abrufen. Wenn ein Thread ein Write Lock hält, werden alle anderen Threads (sowohl Leser als auch Schreiber) blockiert.
Die Analogie ist ein Dokument in einer gemeinsamen Bibliothek. Viele Personen können gleichzeitig Kopien des Dokuments lesen (Shared Read Lock). Wenn jedoch jemand das Dokument bearbeiten möchte, muss er es exklusiv auschecken, und niemand sonst kann es lesen oder bearbeiten, bis er fertig ist (Exclusive Write Lock).
Recursive Locks (Reentrant Locks)
Was passiert, wenn ein Thread, der bereits einen Mutex hält, versucht, ihn erneut abzurufen? Mit einem Standard-Mutex würde dies zu einem sofortigen Deadlock führen – der Thread würde für immer darauf warten, dass er das Lock selbst freigibt. Ein Recursive Lock (oder Reentrant Lock) wurde entwickelt, um dieses Problem zu lösen.
Ein Recursive Lock ermöglicht es demselben Thread, dasselbe Lock mehrmals abzurufen. Es verwaltet einen internen Eigentumszähler. Das Lock wird erst vollständig freigegeben, wenn der besitzende Thread `release()` genauso oft aufgerufen hat, wie er `acquire()` aufgerufen hat. Dies ist besonders nützlich in rekursiven Funktionen, die während ihrer Ausführung eine gemeinsam genutzte Ressource schützen müssen.
Die Gefahren des Sperrens: Häufige Fallstricke
Obwohl Locks leistungsstark sind, sind sie ein zweischneidiges Schwert. Die unsachgemäße Verwendung von Locks kann zu Fehlern führen, die weitaus schwieriger zu diagnostizieren und zu beheben sind als einfache Race Conditions. Dazu gehören Deadlocks, Livelocks und Leistungsengpässe.
Deadlock
Ein Deadlock ist das gefĂĽrchtetste Szenario in der parallelen Programmierung. Er tritt auf, wenn zwei oder mehr Threads auf unbestimmte Zeit blockiert sind und jeweils auf eine Ressource warten, die von einem anderen Thread in derselben Menge gehalten wird.
Betrachten Sie ein einfaches Szenario mit zwei Threads (Thread 1, Thread 2) und zwei Locks (Lock A, Lock B):
- Thread 1 ruft Lock A ab.
- Thread 2 ruft Lock B ab.
- Thread 1 versucht nun, Lock B abzurufen, aber es wird von Thread 2 gehalten, sodass Thread 1 blockiert.
- Thread 2 versucht nun, Lock A abzurufen, aber es wird von Thread 1 gehalten, sodass Thread 2 blockiert.
Beide Threads befinden sich nun in einem permanenten Wartezustand. Die Anwendung kommt zum Erliegen. Diese Situation ergibt sich aus dem Vorhandensein von vier notwendigen Bedingungen (den Coffman-Bedingungen):
- Gegenseitiger Ausschluss: Ressourcen (Locks) können nicht gemeinsam genutzt werden.
- Halten und Warten: Ein Thread hält mindestens eine Ressource, während er auf eine andere wartet.
- Keine Präemption: Eine Ressource kann einem Thread, der sie hält, nicht gewaltsam entzogen werden.
- Zirkuläres Warten: Es existiert eine Kette von zwei oder mehr Threads, wobei jeder Thread auf eine Ressource wartet, die vom nächsten Thread in der Kette gehalten wird.
Die Verhinderung von Deadlocks beinhaltet das Aufbrechen mindestens einer dieser Bedingungen. Die gebräuchlichste Strategie besteht darin, die zirkuläre Wartebedingung aufzubrechen, indem eine strenge globale Reihenfolge für den Lock-Abruf erzwungen wird.
Livelock
Ein Livelock ist ein subtilerer Cousin des Deadlocks. In einem Livelock werden Threads nicht blockiert – sie werden aktiv ausgeführt – aber sie machen keine Fortschritte. Sie stecken in einer Schleife, in der sie auf die Zustandsänderungen des jeweils anderen reagieren, ohne nützliche Arbeit zu leisten.
Die klassische Analogie sind zwei Personen, die versuchen, sich in einem schmalen Flur zu passieren. Beide versuchen, höflich zu sein und nach links zu treten, aber sie blockieren sich gegenseitig. Dann treten beide nach rechts und blockieren sich wieder gegenseitig. Sie bewegen sich aktiv, kommen aber im Flur nicht voran. In der Software kann dies mit schlecht konzipierten Deadlock-Wiederherstellungsmechanismen geschehen, bei denen Threads wiederholt ausweichen und es erneut versuchen, nur um wieder in Konflikt zu geraten.
Starvation
Starvation tritt auf, wenn einem Thread der Zugriff auf eine notwendige Ressource dauerhaft verweigert wird, obwohl die Ressource verfügbar wird. Dies kann in Systemen mit Planungsalgorithmen geschehen, die nicht "fair" sind. Wenn beispielsweise ein Sperrmechanismus immer Zugriff auf Threads mit hoher Priorität gewährt, erhält ein Thread mit niedriger Priorität möglicherweise nie die Möglichkeit, ausgeführt zu werden, wenn es einen konstanten Strom von Anwärtern mit hoher Priorität gibt.
Performance Overhead
Locks sind nicht kostenlos. Sie verursachen auf verschiedene Weise Performance Overhead:
- Acquisition/Release Cost: Das Abrufen und Freigeben eines Locks beinhaltet atomare Operationen und Memory Fences, die rechenintensiver sind als normale Anweisungen.
- Contention: Wenn mehrere Threads häufig um dasselbe Lock konkurrieren, verbringt das System viel Zeit mit dem Kontextwechsel und der Planung von Threads, anstatt produktive Arbeit zu leisten. Eine hohe Contention serialisiert die Ausführung effektiv und macht den Zweck der Parallelität zunichte.
Best Practices fĂĽr die Lock-basierte Synchronisation
Das Schreiben von korrektem und effizientem parallelem Code mit Locks erfordert Disziplin und die Einhaltung einer Reihe von Best Practices. Diese Prinzipien sind universell anwendbar, unabhängig von der Programmiersprache oder Plattform.
1. Halten Sie kritische Abschnitte klein
Ein Lock sollte für die kürzestmögliche Dauer gehalten werden. Ihr kritischer Abschnitt sollte nur den Code enthalten, der unbedingt vor gleichzeitigem Zugriff geschützt werden muss. Alle nicht kritischen Operationen (wie E/A, komplexe Berechnungen, die nicht den gemeinsam genutzten Zustand betreffen) sollten außerhalb des gesperrten Bereichs durchgeführt werden. Je länger Sie ein Lock halten, desto größer ist die Wahrscheinlichkeit von Contention und desto mehr blockieren Sie andere Threads.
2. Wählen Sie die richtige Lock-Granularität
Die Lock-Granularität bezieht sich auf die Datenmenge, die durch ein einzelnes Lock geschützt wird.
- Grobkörniges Sperren: Verwenden eines einzelnen Locks, um eine große Datenstruktur oder ein ganzes Subsystem zu schützen. Dies ist einfacher zu implementieren und zu begründen, kann aber zu hoher Contention führen, da nicht verwandte Operationen an verschiedenen Teilen der Daten alle durch dasselbe Lock serialisiert werden.
- Feinkörniges Sperren: Verwenden mehrerer Locks, um verschiedene, unabhängige Teile einer Datenstruktur zu schützen. Anstelle eines Locks für eine gesamte Hash-Tabelle könnten Sie beispielsweise ein separates Lock für jeden Bucket haben. Dies ist komplexer, kann aber die Leistung erheblich verbessern, indem mehr echte Parallelität ermöglicht wird.
Die Wahl zwischen ihnen ist ein Kompromiss zwischen Einfachheit und Leistung. Beginnen Sie mit gröberen Locks und wechseln Sie nur zu feinkörnigeren Locks, wenn die Leistungsprofilierung zeigt, dass Lock-Contention ein Engpass ist.
3. Geben Sie Ihre Locks immer frei
Das Versäumnis, ein Lock freizugeben, ist ein katastrophaler Fehler, der Ihr System wahrscheinlich zum Erliegen bringt. Eine häufige Ursache für diesen Fehler ist, wenn eine Ausnahme oder eine frühzeitige Rückgabe innerhalb eines kritischen Abschnitts auftritt. Um dies zu verhindern, verwenden Sie immer Sprachkonstrukte, die die Bereinigung garantieren, wie z. B. try...finally-Blöcke in Java oder C# oder RAII-Muster (Resource Acquisition Is Initialization) mit bereichsbezogenen Locks in C++.
Beispiel (Pseudocode mit try-finally):
my_lock.acquire();
try {
// Kritischer Abschnittscode, der eine Ausnahme auslösen könnte
} finally {
my_lock.release(); // Dies wird garantiert ausgefĂĽhrt
}
4. Befolgen Sie eine strenge Lock-Reihenfolge
Um Deadlocks zu verhindern, besteht die effektivste Strategie darin, die zirkuläre Wartebedingung aufzubrechen. Legen Sie eine strenge, globale und willkürliche Reihenfolge für das Abrufen mehrerer Locks fest. Wenn ein Thread jemals sowohl Lock A als auch Lock B halten muss, muss er immer zuerst Lock A abrufen, bevor er Lock B abruft. Diese einfache Regel macht zirkuläre Wartezeiten unmöglich.
5. Erwägen Sie Alternativen zum Sperren
Obwohl Locks grundlegend sind, sind sie nicht die einzige Lösung für die Parallelitätskontrolle. Für Hochleistungssysteme lohnt es sich, fortschrittliche Techniken zu untersuchen:
- Lock-Free Datenstrukturen: Dies sind hochentwickelte Datenstrukturen, die mit Low-Level-Atomaren Hardwareanweisungen (wie Compare-And-Swap) entworfen wurden, die den gleichzeitigen Zugriff ohne Verwendung von Locks ermöglichen. Sie sind sehr schwer korrekt zu implementieren, können aber unter hoher Contention eine überlegene Leistung bieten.
- Unveränderliche Daten: Wenn Daten nach ihrer Erstellung nie geändert werden, können sie frei zwischen Threads ausgetauscht werden, ohne dass eine Synchronisation erforderlich ist. Dies ist ein Kernprinzip der funktionalen Programmierung und eine zunehmend beliebte Methode, um parallele Designs zu vereinfachen.
- Software Transactional Memory (STM): Eine Abstraktion höherer Ebene, mit der Entwickler atomare Transaktionen im Speicher definieren können, ähnlich wie in einer Datenbank. Das STM-System behandelt die komplexen Synchronisationsdetails im Hintergrund.
Fazit
Die Lock-basierte Synchronisation ist ein Eckpfeiler der parallelen Programmierung. Sie bietet eine leistungsstarke und direkte Möglichkeit, gemeinsam genutzte Ressourcen zu schützen und Datenbeschädigung zu verhindern. Vom einfachen Mutex bis zum differenzierteren Read-Write Lock sind diese Primitiven wesentliche Werkzeuge für jeden Entwickler, der Multi-Threaded-Anwendungen erstellt.
Diese Macht erfordert jedoch Verantwortung. Ein tiefes Verständnis der potenziellen Fallstricke – Deadlocks, Livelocks und Leistungsverschlechterung – ist nicht optional. Durch die Einhaltung von Best Practices wie der Minimierung der Größe kritischer Abschnitte, der Auswahl einer geeigneten Lock-Granularität und der Durchsetzung einer strengen Lock-Reihenfolge können Sie die Leistung der Parallelität nutzen und gleichzeitig ihre Gefahren vermeiden.
Die Beherrschung der Parallelität ist eine Reise. Sie erfordert sorgfältiges Design, rigorose Tests und eine Denkweise, die sich immer der komplexen Interaktionen bewusst ist, die auftreten können, wenn Threads parallel ausgeführt werden. Durch die Beherrschung der Kunst des Sperrens machen Sie einen wichtigen Schritt hin zum Aufbau von Software, die nicht nur schnell und reaktionsschnell, sondern auch robust, zuverlässig und korrekt ist.