Begriffe
Nebenläufigkeit
Nebenläufigkeit bezieht sich auf die logische Gleichzeitigkeit von Prozessen
- müssen nicht gleichzeitig laufen. sondern können auch abwechselnd ausgeführt werden
- mehrere Prozesse machen Fortschritte
Parallelität
Parallelität ist die physische Gleichzeitigkeit von Prozessen
- parallele Ausführung auf verschiedenen Kernen
- Data parallelism: Daten werden aufgeteilt und dieselbe Operation auf verschiedenen Prozessoren / Kernen darauf ausgeführt
- Task parallelism: Threads mit unterschiedlichen Aufgaben werden auf verschiedenen Prozessoren / Kernen ausgeführt

Amdahl’s Law
Es sei der serielle Anteil eines Programms und die Anzahl der Prozessor-Kerne. Dann ist der erreichbare speedup beschränkt durch
- Für gegen unendlich geht der Speedup gegen
- Der serielle Anteil hat damit einen entscheidenden Effekt auf den möglichen Speedup
Multiprocessing
- symmetrisch: jeder Prozesse kann jede Aufgabe übernehmen
- asymmetrisch: jeder Prozesse erhält eine spezifische Aufgabe
Synchronisation
Race Condition
Zwei Prozesse versuchen auf dieselben Variablen / Daten zuzugreifen, wobei die korrekte Ausführung vom Scheduling der beiden Prozesse abhängt
Critical Section Problem
Ein kritischer Abschnitt ist ein Abschnitt von Code eines Prozesses, der geteilte Variablen, Dateien, usw. ändern könnte
- es darf sich stets nur ein Prozess zur Zeit in seinem kritischen Abschnitt befinden
- Problem: Entwicklung eines Protokolls, das dies gewährleistet
Eine Lösung des Critical-Section-Problems muss die folgenden drei Kriterien erfüllen:
- Mutual exclusion: Wenn ein Prozess sich in seiner Critical Section befindet, darf kein anderer Prozess dies ebenfalls tun
- Progress: Wenn sich gerade kein Prozess in seiner Critical Section befindet, dann muss nach einer gewissen Zeit in neuer Prozess dafür ausgewählt werden (cannot be postponed indefinitely)
- Bounded waiting: Wenn ein Prozess in seine Critical Section gehen möchte, muss es ein Limit dafür geben geben, wie oft andere Prozesse in ihre Critical Section übergehen dürfen, bevor es tut
Kein festes Limit, stochastische Gewissheit genügt
Lösungsansätze
- Interrupts während Critical Section deaktivieren: Kein Zeitlimit für Critical Section, nicht anwendbar auf 2 Kerne
- Software-basiert:
int turn-Variable gibt an, wer als nächstes seinen kritischen Abschnitt betreten darf- Mutual exclusion ist erfüllt, aber Progress und Bounded waiting nicht, wenn der jeweils andere Prozess gar nicht in seinen kritischen Abschnitt eintreten möchte
- Peterson’s Solution:
int turn-Variable zusammen mitboolean flag[2]-Array, das angibt, ob ein Prozess bereit ist, seinen kritischen Abschnitt zu betreten- Hält alle drei Bedingungen ein (für 2 Threads)
- scheitert auf modernen Architekturen allerdings am Umordnen von Instruktion
- Bakery Algorithm: Threads bekommen Ticket-Nummern in nicht-absteigender Reihenfolge zugeweisen, Thread mit der niedrigsten Ticket-Nummer oder bei Gleichheit Thread ID darf in kritischen Abschnitt eintreten
Memory Barries
Ansatz gegen Instruction Reordering bzw. fehlende Cache-Kohärenz
- Memory Models: Garantien, die ein Betriebssystem bezüglich des Speichers an Programme macht
- Strongly ordered: Veränderung auf einem Prozessor ist sofort auf allen anderen sichtbar
- Weakly ordered: Veränderung auf einem Prozessor kann erst verzögert auf anderen Prozessoren sichtbar werden
- Memory Barrier: Instruktion, die die Weitergabe aller Änderungen an die anderen Prozessoren erzwingt
- z.B.
memory_barrier(); - alle loads und stores vor der Barriere werden vor allen nach der Barriere ausgeführt schützt auch gegen Instruction Reordering
- z.B.
Hardware Instructions
Garantieren atomare Ausführung von kurzen Speicher-Instruktionen
- Test and Set:
boolean test_and_set(boolean *target)setzt einen Boolean auf wahr und gibt den ursprünglichen Wert zurück- Verwendung etwa um lock zu prüfen (
while) und direkt zu setzen - löst Mutual Exclusion, aber nicht Bounded Waiting und verwendet Spinlocks
- Verwendung etwa um lock zu prüfen (
- Compare and Swap:
int compare_and_swap(int *value, int expected, int new_value)setztvaluegenau dann aufnew_value, wenn*value == expectedund gibt den ursprünglichen Wert zurück- gleiche Verwendung und Probleme wie Test and Set, aber Bounded Waiting lässt sich lösen
- werden meist als Basis für komplexere Synchronisations-Tools verwendet
Atomic Variables
Eine atomare Variable stellt atomare Veränderungen von simplen Datentypen wie Integers und Booleans bereit
- z.B. atomare Variable
sequencemit atomarem Zugriffincrement(sequence) - implementiert etwa mit
compare_and_swap()
Mutex
Für Programmierer leicht verwendbare Lösung des Critical Section Problems
- eine Mutex ist eine Boolean-Variable, die angibt ob ein lock verfügbar ist oder nicht
- atomare Instruktionen:
aqcuire()undrelease() - benötigt busy waiting und wird daher Spinlock genannt
Spinlocks
Bei einem Spinlock fragt ein Prozess eine Mutex wiederholt an, bis diese verfügbar ist
- vermeidet Kontextwechsel
- jedoch wird in der Zeit nichts Sinnvolles ausgeführt (busy waiting)
- sinnvoll insbesondere auf Multiprozessorsystemen, auf denen keine hohe Wartezeit erwartet / erlaubt wird
Semaphore
Eine Semaphore ist eine Integer-Variable, auf welche (außer zur Initialisierung) nur über folgende Funktionen zugegriffen werden kann:
wait()/P(): Prozesse, die starten möchten, rufenwait()auf der Semaphore auf- Wert der Semaphore wird dekrementiert
signal()/V(): Wenn ein Prozess Ressourcen wieder freigibt, ruft ersignal()auf- Wert der Semaphore wird inkrementiert
Wenn die Semaphore auf fällt, sind alle Ressourcen belegt und Prozesse müssen warten, bis ein anderer fertig wird. Man unterscheidet
- counting semaphore: beliebig groß
- binary semaphore: , äquivalent zu mutex locks
Semaphoren implementieren eine waiting queue und vermeiden so busy waiting
- Prozesse blockieren, wenn Ressource nicht verfügbar ist CPU-Zeit kann sinnvoll genutzt werden
blocklegt einen Prozesse in die waiting queue undwakelegt ihn wieder in die ready queue
Monitor
Eine high-level Abstraktion, die einen effektiven Mechanismus zur Prozess-Synchronisierung zur Verfügung stellt
- Es kann stets nur ein Prozess innerhalb des Monitors aktiv sein
- Über Condition Variables kann auf das Eintreten von Events gewartet werden
- z.B.
condition xmitx.wait()undx.signal(), wobei letzteres eine wartenden Prozess startet (sofern einer vorhanden ist) - Erweiterung mit Prioritäten über
x.wait(p)möglich
- z.B.
- Implementation vollständig mittels Semaphoren möglich
monitor monitor-name
{
// shared variable declarations
procedure P1 (...) { ... }
procedure P2 (...) { ... }
procedure Pn (...) { ... }
initialization code (...) { ... }
}
Single Resource Allocation
Dient der Zuteilung einer Ressource unter konkurrierenden Prozesse, wobei jeweils eine maximale Nutzungsdauer der Ressource angegeben wird
- Interface z.B.
R.acquire(t)undR.release() - Prozess mit der kürzesten Nutzungsdauer wird bevorzugt
- implementierbar mit prioritäten-basiertem Monitor
Liveness
Bedingungen, die ein System erfüllen muss, um progress zu garantieren. Verstöße sind etwa:
- Starvation: Ein Prozess wird niemals aus der Semaphore-Warteschlange entfernt, in der er wartet
- Priority Inversion: Prozess mit niedriger Priorität hält lock, auf den Prozess mit höherer Priorität wartet
- Priority-inheritance protocol: Priorität des Prozesses, der das lock hält, wird angehoben, um das Inversion-Problem zu lösen
- Deadlocks
Deadlocks
Als Deadlock bezeichnet man eine Situation während der Ausführung mehrerer Threads, in der diese allesamt auf die Freigabe von locks (z.B. Mutex oder Semaphore) warten, wobei diese jeweils von den anderen Threads gehalten werden.
- keiner der Threads wird seine gehaltenen locks freigeben, bevor dies nicht ein anderer Thread tut, sodass alle Threads endlos aufeinander warten

Windows
- verwendet auf Uniprozessor-Systemen Interrupt Masks (teilweise disabled)
- verwendet auf Multiprozessor-Systemen Spinlocks
- verwendet Dispatcher-Objekte, die sich etwa wie eine Mutex oder Semaphore verhalten können
Dispatcher-Objekte
Dispatcher-Objekte stellen im Windows-Betriebssystem ein allgemeines Tool zur Thread-Synchronisierung zur Verfügung
- Unterstützung für Mutexes, Semaphoren, Events (ähnlich zu Condition Variablen), Timer, etc.
- Verwendung mittels
WaitForSingleObject()bzw.WaitForMultipleObjects
Kurz: Alles, worauf man warten kann
Ein Dispatcher-Objekt kann sich in zwei Zuständen befinden:
- signalised: Objekt steht derzeit zur Verfügung und kann von einem anfragenden Prozess aqcuired werden
- non-signalised: Thread blockiert bei dem Versuch, dieses Objekt zu erlangen. Wird zu einer Warteschlange hinzugefügt (FiFo)
Bedeutungen von signalised
- Prozess: ist beendet
- Datei: I/O-Vorgang (z.B. lesen oder schreiben) abgeschlossen
- Event: ist eingetreten
- Timer: gewisse Zeit verstrichen
- Mutex / Semaphore: ist verfügbar
- und weitere
Linux
- deaktiviert Interrupts auf Uniprozessor-Systemen
- verwendet auf Multiprozessor-Systemen Spinlocks
- nutzt Semaphoren, Atomic Integers und Reader-writer locks, wenn länger Code-Abschnitte Datenzugriff benötigen
- Typ
atomic_tmit Operationenatomic_set(&i, 5),atomic_add(5, &i),atomic_sub(5, &i),atomic_inc(&i)undvalue = atomic_read(&i)
- Typ
- implementiert POSIX-Synchronisations-Primitive (darunter auch Condition Variablen)
Alternative Ansätze
- Transactional Memory: Speicher Transaktion
atomic { /* modify shared data */ }wird atomar ausgeführt - OpenMP: Code innerhalb von
#pragma omp critical { ... }wird als kritischer Abschnitt behandelt und atomar ausgeführt - Functional Programming Languages: verbieten Zustand, indem Variablen immutable (unveränderlich) sind. Die beschriebenen Synchronisations-Probleme entstehen daher gar nicht erst
Klassische Probleme
Bounded Buffer
- Buffers, jeder kann ein Item halten
- Semaphore
mutex, die auf initialisiert ist - Semaphore
full, die auf initialisiert ist - Semaphore
empty, die auf initialisiert ist
while (true) {
/* produce an item in next_produced */
wait(empty);
wait(mutex);
/* add next produced to the buffer */
signal(mutex);
signal(full);
}while (true) {
wait(full);
wait(mutex);
/* remove an item from buffer to next_consumed */
signal(mutex);
signal(empty);
/* consume the item in next consumed */
}Readers-Writers
- Ein Datensatz wird unter nebenläufigen Prozessen geteilt, die sich wie folgt unterteilen:
- Readers: Lesen den Datensatz nur, verändern ihn also nicht
- Writers: Können sowohl lesen als auch schreiben
- Problem: Mehreren Prozesse sollen zugleich lesen dürfen, doch ein schreibender Prozess muss alleinigen Zugriff haben
- Lösung über
rw_mutex-Semaphore (), die von schreibendem Prozess bzw. erstem lesendem Prozess übernommen wird- darüber hinaus
mutex-Semaphore () für Critical Sections undint read_count(), worüber die Anzahl der lesenden Zugriffe dokumentiert wird
- darüber hinaus
- Bei validen Lösungen kann es passieren, dass ein Writer niemals Zugriff erhält Second reader-writer problem, bei dem sobald der Writer bereit ist, kein neuer Reader Zugriff erhalten darf
- bei beiden Problemen kann es bei validen Lösungen zu Starvation kommen, einige Betriebssysteme lösen das Problem mit Reader-writer locks
Dining Philosophers
- Philosophen sitzen an einem runden Tisch mit einer Schüssel Reis in der Mitte
- manchmal denken Philosophen nach, manchmal essen sie, dabei interagieren sie aber nie mit ihren Nachbarn
- wenn sie essen wollen, müssen sie das Stäbchen rechts und links von ihnen nacheinander aufnehmen
- Modellierung: Datensatz (Reis-Schüssel) und Semaphoren
chopstick[5], jeweils auf initialisiert - in naivem Algorithmus sind Deadlocks möglich, mit Monitoren lässt sich das verhindern, aber Starvation ist weiterhin möglich
- Monitor verwendet komplexere
pickup(int i)undputdown(int i)Methoden, die jeweils einentest(int i)ausführen und andernfalls auf einsignal()warten
- Monitor verwendet komplexere