Synchronisation mit Lock-Objekten, Read-Write-Locks und Semaphoren

Parallele Programmierung (3IB)

Prof. Dr.-Ing. Sandro Leuchter
Hochschule Mannheim, Fakultät für Informatik
Wintersemester 2024/2025

 

Dieses Werk ist lizenziert unter einer Creative Commons „Namensnennung – Nicht-kommerziell – Weitergabe unter gleichen Bedingungen 4.0 International“ Lizenz.

Überblick

=> Grundlage: Hettel und Tran (2016, Kap. 8)

  • gegenseitiger Ausschluss mit “Locks”
    • Lock-Interface
    • ReentrantLock-Implementierung von Lock
    • ReadWriteLock
    • StampedLock
  • Semaphore

Nachteile von “synchronized”

  • Durch synchronized blockierter Thread kann nicht mit interrupt() unterbrochen werden. interrupt() wirkt sich erst nach der Blockierung aus.
  • kein Timeout
  • keine Regel für Reihenfolge der Zuteilung an mehrere wartende Tasks
  • nur Blockstruktur (Eintritt und Austritt in unterschiedlichen Methoden nicht möglich)

ReentrantLock

Lock-Implementierungen und ihre Beziehungen

Lock-Interface

public interface Lock {
    public void lock();
    public void lockInterruptibly()
        throws InterruptedException;

    public boolean tryLock();
    public boolean tryLock(long time, TimeUnit unit)
        throws InterruptedException;

    public void unlock();
    public Condition newCondition();
}
  • ein wegen lockInterruptably() blockierter Thread kann durch interrupt() unterbrochen werden (\(\to\) InterruptedException)
  • tryLock() liefert false, falls `Lock `-Instanz schon belegt ist

Nachteile von synchronized

  • Durch synchronized blockierter Thread kann nicht mit interrupt() unterbrochen werden. interrupt() wirkt sich erst nach der Blockierung aus.
  • kein Timeout
  • keine Regel für Reihenfolge der Zuteilung an mehrere wartende Tasks
  • nur Blockstruktur (Eintritt und Austritt in unterschiedlichen Methoden nicht möglich)

Vorteile von Lock-Implementierungen

  • Unterbrechbarkeit
  • Timeout
  • Fairness
  • “Non-Blockstruktur”

Benutzungsmuster von Lock-Interface und -Implementierungen



Lock lock = new ReentrantLock();

lock.lock();
try {
    //... kritischer Abschnitt
} finally {
    lock.unlock();
}
  • Durch tryfinally wird sichergestellt, dass der Lock auf jeden Fall am Ende gelöst wird.
  • lock() und unlock() können von unterschiedlichen Methoden aus aufgerufen werden.

Benutzungsmuster von Lock-Interface und -Implementierungen

boolean fairness;
//...
Lock lock = new ReentrantLock(fairness);

lock.lock();
try {
    //... kritischer Abschnitt
} finally {
    lock.unlock();
}
  • fairness-Parameter gibt an, ob als nächstes der am längsten wartende Thread entblockiert wird oder nicht.

Benutzungsmuster von Lock-Interface und -Implementierungen

Lock lock = new ReentrantLock();

if(lock.tryLock()) {
    try {
        //... kritischer Abschnitt
    } finally {
        lock.unlock();
    }
} else {
    //... etwas anderes tun...
}
  • mit tryLock() prüfen, ob Lock belegt und schließen oder entwas anderes tun

Laboraufgabe “Unterbrechbarkeit von Threads, die blockiert sind”

  • Projekt: pp.06.01-SynchInterrupt
  • Bearbeitungszeit: 25 Minuten
  • Musterlösung: 15 Minuten
  • Kompatibilität: mindestens Java SE 10

ReadWriteLock

Lock-Implementierungen und ihre Beziehungen

ReadWriteLock

  • warum sollten nicht mehrere Threads gleichzeitig lesend auf eine Variable zugreifen dürfen, wenn ein gleichzeitiges Schreiben ausgeschlossen ist?
  • Eine Lock-Variante, die ReadLock und WriteLock besitzt
    • ReadLock: mehrere dürfen in den kritischen Abschnitt, wenn der dazugehörende WriteLock nicht geschlossen ist (“nicht-exklusiver” Lock).
    • WriteLock: nur einer darf in den kritischen Abschnitt und auch nur, wenn der ReadLock nicht gerade benutzt wird (“exklusiver” Lock).
  • Die Vergabe der Locks erfolgt in der Reihenfolge, wie sie angefordert wurden.

ReadWriteLock

public interface ReadWriteLock {
    public Lock readLock();
    public Lock writeLock();
}


ReadWriteLock lock = new ReentrantReadWriteLock(fairness);
Lock rLock = lock.readLock();
Lock wLock = lock.writeLock();
  • rLock und wLock werden benutzt wie die bisherigen Lock-Implementierungen
  • Sie verhalten sich aber anders (s.o.) und sind “miteinander verschränkt”.

Beispiel: Verschränkung Read/Write Lock

  • Zu jeder Instanz einer Implementierung von ReadWriteLock wird je ein Read-Lock und ein Write-Lock erzeugt.
  • Ihr Typ implementiert das Lock-Interface.
  • Nutzer eines Read- oder Write-Locks müssen eine Referenz auf das ReadriteLock-Objekt bekommen. Das ist im Sequenzdiagramm nur durch new(lock) angedeutet. Natürlich erwartet der Konstruktor von Thread kein Lock-Objekt.
  • Read-Lock und Write-Lock des selben ReadWriteLock sind miteinander “verschränkt” und wirken auf den sie erzeugenden ReadWriteLock zurück. Im Sequenzdiagramm ist das nur durch “namenlose Nachrichten” angedeutet.

Beispiel: Verschränkung Read/Write Lock

  • Im folgenden Sequenzdiagramm sind die Rückwirkungen von Aufrufen an rl und wl auf lock nicht dargestellt.
  • t1 schließt zuerst den Read-Lock
    \(\to\) lock: gelb
  • t2 darf am Read-Lock teilnehmen und wird nicht blockiert.
  • t3 möchte den Write-Lock und wird blockiert.
  • t3 wird erst entblockiert, wenn sowohl t1 als auch t2 den Read-Lock wieder freigegeben haben. Der Write-Lock ist nun von t3 gesperrt \(\to\) lock: blau
  • t1 möchte den Read-Lock und wird blockiert. Erst wenn t3 den Write-Lock freigibt, bekommt t1 den Read-Lock
    \(\to\) lock: gelb
  • t1 gibt am Ende den Read-Lock wieder frei.
  • Read-Lock sperrt Lesen “non-exklusiv”

Beispiel: Verschränkung Read/Write Lock

  • Im folgenden Sequenzdiagramm sind die Rückwirkungen von Aufrufen an rl und wl auf lock nicht dargestellt.
  • t1 schließt zuerst den Read-Lock
    \(\to\) lock: gelb
  • t3 möchte den Write-Lock und wird blockiert.
  • t2 darf nun wg. offener Lock-Anforderung von t3 nicht mehr am Read-Lock teilnehmen und wird blockiert.
  • t3 wird wg. der Reihenfolge der Anforderung als nächstes entblockiert, wenn t1 freigibt. Der Write-Lock ist nun von t3 gesperrt \(\to\) lock: blau
  • t2 wird erst entblockiert, wenn t3 den Lock freigibt. \(\to\) lock: gelb
  • t2 gibt am Ende den Read-Lock wieder frei.
  • Reihenfolge der lock() ist bestimmend

StampedLock

Lock-Implementierungen und ihre Beziehungen

  • Falls die zu schützenden kritischen Abschnitte kurz sind, ist der erforderliche Verwaltungsaufwand für einen Lock relativ hoch. synchronized kann von der Laufzeit her schneller sein.
  • StampedLock nützlich bei großem Leseanteil

StampedLock Modi

Ein StampedLock besteht aus Stamp und Modus

  • Modus Writing (“exklusiver” Lock):
    writeLock() blockiert für exklusiven Schreibzugriff; liefert eine long ID (“stamp”), die für unlockWrite(long) benutzt werden kann.
    keine readLocks und tryOptimisticRead möglich.
  • Modus Reading (“nicht-exklusiver” Lock):
    readLock() wartet auf nicht exklusiven-Zugriff; liefert eine long ID (“stamp”), die für unlockRead(long) benutzt werden kann.
  • Modus Optimistic Reading (kein Lock, aber post-hoc Kollisionserkennung möglich):
    tryOptimisticRead() liefert nur dann eine long ID (“stamp”), falls der Lock gerade nicht im Modus “Writing” ist. validate(long) liefert true, falls der Lock nicht zum Schreiben gesperrt wurde, seit die ID vergeben wurde.

ReadLock beim StampedLock

var lock = new StampedLock();

var stamp = lock.readLock();
try {
    //... kritischer Abschnitt
} finally {
    lock.unlockRead(stamp);
}

WriteLock beim “StampedLock”

var lock = new StampedLock();

var stamp = lock.writeLock();
try {
    //... kritischer Abschnitt
} finally {
    lock.unlockWrite(stamp);
}

Beispiel für die Modi “exklusiver”/“nicht-exklusiver” Lock

  • t1 bekommt zuerst den “nicht-exklusiven” Lock (“Read-Lock”) \(\to\) lock ist gelb
  • t3 möchte den “exklusiven” Lock (“Write-Lock”) und wird blockiert, bis der nicht-exklusive Lock endet \(\to\) t1 ist rot
  • t2 kommt zu t1 in den “nicht-exklusiven” Lock
  • t1 verlässt den “nicht-exklusiven” Lock
  • t3 bleibt vorerst weiterhin blockiert, solange bis der letzte Thread den “nicht-exklusiven” Lock verlässt
  • t2 tut dies, daraufhin bekommt
  • t3 den “exklusiven” Lock \(\to\) lock ist blau
  • t1 möchte wieder einen “nicht-exklusiven” Lock, wird aber wg. t3 blockiert \(\to\) t1 ist rot
  • t3 gibt den “exklusiven” Lock zurück, erst dann wird
  • t1 entblockiert und erhält den “nicht-exklusiven” Lock \(\to\) lock ist gelb

Optimistisches Lesen (Read-Lock) mit StampedLock

  • keine Änderungen im kritischen Abschnitt, aber “nicht-exklusiv” lesend wie Read-Lock
  • keine “technische” Durchsetzung des Locks, stattdessen:
    • Beginn des kritischen Abschnitts beim StampedLock-Objekt “markieren” (anmelden)
    • versuchen, den kritischen Abschnitt abzuarbeiten
    • am Ende beim StampedLock-Objekt nachfragen, ob es seit der Anmeldung Write-Locks gegeben hat (Read-Locks sind unkritisch)
    • falls ja: reagieren mit Roll-Back der Transaktion und erneutem Versuch
    • falls nein: kritischer Abschnitt erfolgreich
  • da kein Blockieren: sehr geringer Overhead
  • falls das Anwendungsprofil es zulässt (geringe Wahrscheinlichkeit für Verletzung des “nicht-exklusiven” Locks), die perfomanteste Lösung für konkurrierenden Zugriff:
    • bei Zugriff auf einzelne Variablen: Atomics-Wrapper
    • bei komplexeren kritischen Abschnitten: optimistische Nutzung von StampedLock

Optimistisches Lesen (Read-Lock) mit StampedLock

var lock = new StampedLock();

var stamp = lock.tryOptimisticRead();
//... kritischer Abschnitt ("nicht exklusiv")
if (!lock.validate(stamp)) {
    // nicht erfolgreich => das bisherige ist möglicherweise
    // inkonsistent und muss zurückgerollt werden; eine mögliche
    // Strategie: nochmal pessimistisch "non-excl." gelockt probieren:
    stamp = lock.readLock();
    try {
        //... kritischer Abschnitt (ReadLock)
    } finally {
        lock.unlockRead(stamp);
    }
}

andere Muster denkbar (z.B. wie bei Atomics: weiter optimistisch versuchen, bis erfolgreich \(\to\) s. nächstes Beispiel)

Beispiel für optimistisches Lesen (“nicht-exklusiv”)

  • t1 versucht optimistisch lock zu nutzen
  • t3 schließt lock exklusiv (t1 bekommt davon erstmal nichts mit und t3 wird auch nicht blockiert) \(\to\) lock blau
  • t1 ist fertig, stellt aber Misserfolg fest und versucht es erneut opt. (t1 wird nicht blockiert, obwohl t3 exkl. Lock hat)
  • t3 gibt lock wieder frei \(\to\) lock nicht mehr blau
  • t2 fordert erfolgreich nicht-exkl. Lock an. lock ist frei, t2 blockiert nicht. lock nun nicht-exkl. vergeben \(\to\) lock gelb
  • t1 ist fertig, stellt aber Misserfolg fest und versucht es erneut opt. (t1 wird nicht blockiert)
  • t2 ist fertig \(\to\) lock ist nicht mehr gelb
  • t1 ist fertig und stellt nun fest, dass das opt. Lesen endlich erfolgreich war

Laboraufgabe “Vergleich von ReentrantLock, ReentrantReadWriteLock, StampedLock und synchronized

  • Projekt: pp.06.02-LockTiming
  • Bearbeitungszeit: 30 Minuten
  • Musterlösung: 20 Minuten
  • Kompatibilität: mindestens Java SE 10

Semaphore

Koordination der konkurrierenden Nutzung von Ressourcen über Semaphore

  • Semaphore ermöglichen die Begrenzung der Nutzung einer Ressource auf eine bestimmte Anzahl Nutzer.
  • Kapazität eines Semaphores (permitCount): Anzahl an noch erlaubten Nutzern.
  • Operationen: release() und acquire() (blockiert, falls permitCount == 0)

Hettel und Tran (2016, 120)

Referenzen

Hettel, Jörg und Manh Tien Tran. 2016. Nebenläufige Programmierung mit Java. Konzepte und Programmiermodelle für Multicore-Systeme. Heildelberg: dpunkt.verlag.