PD Stefan Bosse
Universität Bremen, FB Mathematik & Informatik
SS 2020
Version 2020-07-01 sbosse@uni-bremen.de |
Definition 1.
Nebenläufigkeit mit Konkurrenz. Operationen in verschiedenen Prozessen eines Programms konkurrieren z. B. um gemeinsame Ressourcen → Wettbewerb.
Konkurrenz bedeutet optionale Parallelität und kann auch sequenziell ausgeführt werden!
Definition 2.
Parallelität. Parallele Ausführung auf Verarbeitungs- und Systemebene mit zeitlicher Überschneidung von Ereignissen und Operationen ohne zwangsläufig mit Wettbewerb.
Synchronisation ist Kommunikation und dient der Bewahrung von Invarianten der Datenverarbeitung (Datenkonsistenz)!
Synchronisationsobjekte sind ebenfalls geteilte Ressourcen!!!!!
Definition 3.
Kritischer Bereich. Sequenzielle Ausführung einer Anweisungssequenz K={I1,I2,..} in einem parallelen Programm darf nur von einem einzigen Prozess zur gleichen Zeit ausgeführt werden. Ein kritischer Bereich K ist daher eine geschützte Ressource.
Definition 4.
Mutualer Ausschluss. Es muss einen Algorithmus geben der sicher stellt dass immer und maximal nur ein Prozess pi aus einer Menge von Prozessen P={p1,p2,..} den kritischen Bereich K ausführen darf (die Ressource zugeteilt bekommt).
Definition 5.
Wettbewerb. Konkurrieren mehrere Prozesse um die Ressource K, so gewinnt maximal einer, die anderen verlieren den Wettbewerb.
Ein solches Wettbewerbsproblem ist gekennzeichnet durch die Eigenschaften Sicherheit und Lebendigkeit.
Sicherheit bedeutet: Bedingung Anzahl der Prozesse im kritischen Bereich K muss |{ p | I(p) ∈ K}| ≤ 1 immer erfüllt sein.
Wenn bis zu einem beliebigen Zeitpunkt t eine Reihe von Prozessen versucht haben den kritischen Bereich zu erlangen, ihn aber nicht erhalten haben, zu wird es in der Zukunft eine Zeit t' > t geben, wo sie erfolgreich sind (d.h. die Anfrage-Operation terminiert).
D.h. eine Anfrage der Ressource terminiert mit einem gewonnen Wettbewerb garantiert irgendwann.
Der Service ist die Ressource, der kritische Bereich. Der Klient ist der Prozess, der die Ressource anfragt.
Aus Sicht des Service (Ressource) ist die Deadlock Freiheit wichtigstes Kriterium. Der konkurrierende Wettbewerb führt immer dazu, dass irgendein Prozess die Ressource nutzen kann → die Ressource gewinnt immer.
Aus Sicht des Klienten (Prozesses) ist die Starvation Freiheit wichtigstes Kriterium. Wenn immer ein Prozess die Ressource anfragt, wird er sie (eventuell) zugeteilt bekommen → der Prozess gewinnt immer.
1. Bounded Bypass f(N)=X ⇒
2. Starvation Freiheit ≡ Endlicher Bypass f(N) ≠ ∞ ⇒
3. Deadlock Freiheit
Ein LOCK Objekt ist ein geteiltes Objekt dass sich in zwei verschiedenen Zuständen SLOCK={FREE,LOCKED} befinden kann und eine Ressource schützt:
Ein LOCK Objekt stellt Prozessen zwei Operationen zur Verfügung:
Ein Prozess muss die Operation ACQUIRE und RELEASE immer paarweise verwenden!
OBJECT L: LOCK
VAR V
PAR
PROCESS P1 IS PROCESS P2 IS
WHILE (True) WHILE (True)
non-critical section non-critical section
L.ACQUIRE() L.ACQUIRE()
critical section(V) critical section(V)
L.RELEASE() L.RELEASE()
non-critical section non-critical section
DONE DONE
Das LOCK Objekt löst das mutuale Ausschlussproblem (mutual exclusion), und wird daher auch als Mutex bezeichnet.
Mehr als zwei Prozesse können mit einem LOCK synchronisiert werden.
Definition 6.
Atomares Register. Der Zugriff auf ein atomares Register R erfolgt mittels zweier Operationen OP={READ,WRITE}: R.READ(), welches den aktuellen Wert von R zurück liefert, und R.WRITE(v), welche einen neuen Wert in das Register schreibt. Dabei ist ein Konflikt durch konkurrierende Schreib- und Lesezugriffe von einer Menge von Prozessen durch mutualen Ausschluss aufgelöst!
VAR GLOBAL SHARED: R (VAR LOCAL NOTSHARED: x)
P1: VAR x P2: VAR x P3: VAR x
|x := ε(R)| |R := ε(x)| |R := ε(x)|
Ein atomares geteiltes Register erfüllt dabei folgende Eigenschaften:
Zugriffe (op=READ/WRITE) erscheinen als wären sie zu einem einzigen Zeitpunkt τ(op) ausgeführt worden.
Der Zeitpunkt der Ausführung der Operation liegt garantiert innerhalb des Intervalls:
τ(opS) ≤ τ(op) ≤ τ(opE), mit
Für zwei verschiedene Operationen op1 und op2 gilt:
τ(op1) ≠ τ(op2)
Das bedeutet: ein atomares Register verhält sich so als ob die Ausführung aller Operationen sequenziell ausgeführt worden wäre.
Atomarität erlaubt die einfache Komposition von geteilten (atomaren) Objekten zu neuen geteilten und atomaren Objekten ohne zusätzlichen (Synchronisations-) Aufwand.
D.h. wenn eine Menge von für sich atomaren Registern {R1,R2,..} mit den Operationen {R1.READ,R1.WRITE}, {R2.READ,R3.WRITE}, .. zu einem neuen Objekt (R1,R2,…) zusammengefasst (komponiert) werden, so ist dieses ebenfalls atomar.
Eine Mutex garantiert mittels eines LOCK Objekts den mutualen Ausschluss durch Erfüllung einer Invariante, d.h. einer logischen Bedingung die niemals verletzt werden darf.
Angenommen eine Menge von Prozessen {P1,P2,..} wollen durch Mutualen Ausschluss sicher stellen dass immer nur ein Prozess sich im kritischen Bereich CS befindet (pi=True). Dann gilt für die Invariante der Mutex:
Die Blockierung eines oder mehrere Prozesse beruht prinzipiell auf einer atomaren Schleife die auf das Eintreten einer booleschen Bedingung wartet, z.B. in der Form
|await
(cond) then
(action)|
bool in1 = false, in2 = false;
## MUTEX: not(inl and in2) -- global invariant
process CSl {
while (true) {
|await (!in2) then in1 = true|; /* entry */
critical section
|in1 = false|;
noncritical section
/* exit */
}
}
process CS2 {
while (true) {
|await (!in1) then in2 = true|; /* entry */
critical section
|in2 = false|;
noncritical section
/* exit */
}
}
bool lock = false;
process CS1 {
while (true) {
|await (!lock) then lock = true|; /* entry */
critical section
lock = false;
noncritical section;
/* exit */
}
}
process CS2 {
while (true) {
|await (!lock) then lock = true|; /* entry */
critical section
lock = false;
noncritical section
/* exit */
}
}
bool TS (bool lock) {
|bool initial = lock;
lock = true;
return initial|
}
Die bedingten atomaren Aktionen werdend ann durch Schleifen ersetzt, die erst dann enden, wenn die Sperre falsch ist, und TS gibt daher false zurück.
Da alle Prozesse dieselben Protokolle ausführen, funktioniert die gezeigte Lösung für eine beliebige Anzahl von Prozessen.
Wenn eine Sperrvariable als ein Spin Lock verwendet wird, heisst das, dass die Prozesse während des Wartens auf das Löschen der Sperre eine Schleife durchlaufen (rotieren).
Gegenseitiger Ausschluss ist sichergestellt:
bool lock = false;
process CS[i = 1 to n] {
while (true) {
while (TS(lock)) skip;
critical section
lock = false;
noncritical section
}
}
Es wird keine atomare Schleife mehr benötigt!
Aber auch hier ist der Eintritt (Lebendigkeit einzelner Prozesse) nicht garantiert. Warum?
Weiterhin zeigt obiger Algorithmus bei Multiprozessorsystemen eine schlechte Performanz → Speicherzugriffe.
boo1 lock = false;
process CS[i = 1 to n] {
while (true) {
while (lock) skip;
while (TS(lock)) { while (lock) skip};
critical section
lock = false;
noncritical section
}
}
Faires Scheduling durch “Fahrstuhlalgorithmus” und Iteration über mehrere (n-1) Ebenen [4]:
int in[l:n] = ([n] 0), last[1:n] = ([n] 0);
process CS[i = 1 to n] {
while (true) {
for [j = 1 to n] { /* entry protocol */
/* remember process i is in stage j and is last */
last[j] = i; in[i] = j;
for [k = 1 to n with i != k] {
/* wait if process k is in higher numbered stage and
process i was the last to enter stage j */
while (in[k] >= in[i] and last[j] == i) skip;
}
}
critical section
in[i] = 0;
noncritical section
}
}
Algorithmus für die Operationen {ACQUIRE,RELEASE} des LOCK Objekts, der mutualen Ausschluss garantiert und Wettbewerb um eine geschützte Ressource ermöglicht.
Der Peterson Algorithmus besteht aus zwei Komponenten, die unterschiedliche Eigenschaften des LOCK erfüllen.
Erfüllung des mutualen Ausschlusses
OPERATION ACQUIREA(i) IS OPERATION RELEASEA(i) IS
AFTER_YOU := i RETURN
WAIT UNTIL AFTER_YOU /= i END OPERATION
RETURN
END OPERATION
Dieser Teil garantiert den mutualen Ausschluss und löst das Wettbewerbsproblem, d. h. maximal ein Prozess x von zwei Prozessen erlangt die Ressource und die Operation ACQUIRE(x) terminiert, der Operation ACQUIRE(y) des anderen Prozesses y terminiert nicht.
Nachteil: Rendezvous ist erforderlich. Beide Prozesse müssen die Ressource anfordern, d.h. ein einzelner Prozess verliert seinen eigenen Wettbewerb immer.
Erfüllung des Lebendigkeitskriteriums
OPERATION ACQUIREB(i) IS OPERATION RELEASEB(i) IS
FLAG[i] := UP FLAG[i] := DOWN
WAIT UNTIL FLAG[j] = DOWN END OPERATION
RETURN
END OPERATION
Nachteil: Deadlock wenn beide Prozesse gleichzeitig ACQUIRE Operation durchführen. (beide setzen gleichzeitig ihr FLAG auf UP).
Zusammenführung beider Teilalgorithmen 1. und 2. führt zum Peterson Algorithmus für die Implementierung der Operationen eines LOCK Objekts (Zwei Prozesse i und j).
OBJECT LOCK IS
REG FLAG[2],AFTER_YOU
OPERATION ACQUIRE(i) IS OPERATION RELEASE(i) IS
FLAG[i] := UP FLAG[i] := DOWN
AFTER_YOU := i RETURN
WAIT UNTIL FLAG[j] = DOWN OR END OPERATION
AFTER_YOU != i
RETURN
END OPERATION
FLAG zeigt das Interesse an einer Ressource an mit Wertemenge {DOWN, UP}.
AFTER_YOU besitzt die Wertemenge {1,2} und bezeichnet die Prozessnummer.
FLAG und AFTER_YOU sind atomare Register (Single Write Multiple Read Verhalten).
Dieser Algorithmus erfüllt
OBJECT LOCK IS
REG FLAG_LEVEL[N],AFTER_YOU[N]
OPERATION ACQUIRE(i) IS OPERATION RELEASE(i) IS
FOR l = 1 TO (N-1) DO
FLAG_LEVEL[i] := l FLAG_LEVEL[i] := 0
AFTER_YOU[l] := i RETURN
WAIT (∀ k != i:
FLAG_LEVEL[k] < l) OR
AFTER_YOU[l] != i
RETURN
END OPERATION END OPERATION
Ein Semaphore Objekt S ist ein LOCK basiertes Synchronisationsobjekt und ein geteilter Zähler mit Warteschlange S.counter, das folgende Eigenschaften erfüllt:
Die Bedingung S.counter ≥ 0 ist immer erfüllt.
Der Semaphorenzähler wird mit einem nicht-negativen Wert s0 initialisiert.
Es gibt zwei wesentliche Operationen {S.DOWN, S.UP}
um den konkurrierenden und synchronisierenden Zugriff auf eine Semaphore zu ermöglichen.
Wenn #(S.DOWN) und #(S.UP) die Anzahl der Operationsaufrufe ist, die terminiert sind, so gilt die Invariante:
S.DOWN erniedrigt den Zähler S.counter atomar um den Wert 1 sofern S.counter > 0.
Wenn S.counter=1 und mehrere Prozesse versuchen gleichzeitig die DOWN Operation anzuwenden, so wird einer den Wettbewerb gewinnen (Mutualer Ausschluss = LOCK). Wenn S.counter = 0 dann werden Prozesse blockiert und in einer Warteschlange S.Q eingereiht.
S.UP erhöht den Zähler S.counter atomar um den Wert 1. Diese Operation kann immer ausgeführt werden, und blockiert nicht die Ausführung des aufrufenden Prozesses.
Wenn S.counter = 0 ist und es blockierte Prozesse gibt, wird einer aus S.Q ausgewählt und freigegeben (d. h. der Zähler ändert sich effektiv nicht).
Eine Semaphore mit s0=1 ist eine binäre Semaphore vergleichbar mit einer Mutex! Eine Semaphore ist stark wenn die blockierten (wartenden) Prozesse in der Reihenfolge wieder freigegeben werden in der sie blockiert wurden (First In First Out / FIFO Ordnung).
|
|
|
|
|
OBJECT ARRAY Forks [N]: SEMA(1)
PROCESS ARRAY Philosopher [N]
FOR try = 1 TO 10 DO
THINKING ...
Forks[#id].down()
Forks[#id+1 % N].down()
EATING ...
Forks[#id+1 % N].up()
Forks[#id].up()
DONE
END
sema[left]:down();
if sema[right]:level() == 1 then sema[right]:down()
else sema[left]:up(); sleep(random time) end
Mutex und Semaphore sind low-level Synchronisationsobjekte
Monitore erlauben die Definition von konkurrierenden Objekten auf der Abstraktionsebene der imperativen Programmiersprachen → high-level.
Ein Monitor ist eine Generalisierung eines Objekts, welches Daten und Operationen kapselt. Zugriff auf die interne Repräsentation erfolgt durch die Operationen mutual exklusiv.
Ein Prozess muss das Schloss des Monitors öffnen. Nachdem der Prozess den Monitor “betritt” (nutzt), schließt das Schloss automatisch wieder. Wenn der Prozess den Monitor verlässt wird das Schloss wieder automatisch geöffnet.
Bedingungsvariablen C sind Objekte die interne Synchronisation ermöglichen sollen.
Bedingungsvariablen können nur innerhalb des Monitors durch folgende Operationen verwendet werden:
Bewahrung des mutualen Ausschlusses in Fall ii.) erforderlich:
OBJECT CONDITION
(M:MONITOR) IS
OBJECT q: QUEUE OPERATION signal IS
p: PROCESS IF q != {} THEN
OPERATION wait IS p := Head(q), q := Tail(q)
p := MYSELF() UNBLOCK(p)
q := q + {p} END IF
|BLOCK(p) & M.l.unlock()| END OPERATION
END OPERATION
MONITOR SEM IS
VAR s:INT
OBJECT c: CONDITION
OPERATION down IS OPERATION up IS
IF s=0 THEN c.wait() s := s + 1
s := s - 1 c.signal();
RETURN RETURN
END OPERATION END OPERATION
Ein signalisierter (blockierter) Prozess der auf eine Bedingung C wartet wird sofort aktiviert (d. h. die Bedingung ist erfüllt).
Würde nach Aktivierung des wartenden Prozesses (cond=TRUE) auch der signalisierende Prozess im Monitor aktiv sein könnte er die Bedingung, die der Bedingungsvariablen zu Grunde liegt, wieder ungültig (cond=FALSE) machen und müsste ebenfalls wieder blockiert werden.
Es gilt daher für den Monitor die Erfüllung eines Prädikats P (boolesche Bedingung), das die Datenkonsistenz des Objektes sicher stellen muss.
Der signalisierende Prozess wartet nach der Signalisierung und der mutuale Ausschluss ist sichergestellt und die Wahrung P=TRUE. Der wartende Prozess führt aus:
IF ¬P THEN C.wait() END IF
Der signalisierende Prozess läuft weiter! Das könnte zur Verletzung des Prädikats P führen, daher muss der signalisierte Prozess nochmals die Gültigkeit P=TRUE prüfen so dass für ihn auszuführen ist:
WHILE ¬P DO C.wait () END WHILE
Mit signal-and-continue Monitoren und Bedingungsvariablen lassen sich einfache temporale ereignisbasierte Synchronisationsobjekte implementieren.
Mehrere Prozess p1,p2,…,pN-1 können auf ein Event warten (OP E.await). Eine geordnete Warteschlange ist nicht erforderlich.
Dieses Event wird durch einen weiteren Prozess pN ausgelöst (OP E.wakeup), so dass alle blockierten inklusive des signalisierenden Prozesse in der Ausführung fortfahren (auch zeitgleich).
Ein Prozess p wird blockiert bis das Ereignis (event) eintritt
Die Operation aktiviert alle auf das Ereignis wartenden Prozesse {pi}.
MONITOR EVENT IS
OBJECT c: CONDITION
OPERATION await IS OPERATION wakeup
c.wait() c.signal();
RETURN RETURN
END OPERATION END OPERATION
local ev = Event();
local mem = Buffer(16000);
ProcessArray(function () {
local sum=0;
waitForData:await();
for i=1,data.size do sum = sum + data:read(i) end
print(sum);
},N,{waitForData = ev, data=mem, N=N})
for i=1,mem.size do mem.write(m.random(1,256),i) end
ev:wakeup();
Der aufrufende Prozess p wird blockiert wenn #blocked(B) < N, ansonsten fährt er in der Ausführung weiter und aktiviert alle blockierten Prozesse.
Setzt die Größe der Barrierengruppe (Anzahl der Prozesse N)
MONITOR BARRIER (N) IS
VAR counter: {0,1,..,N} := 0
OBJECT q: condition
OPERATION await IS
counter := counter + 1
IF counter < N THEN q.await()
ELSE counter := 0
q.signal() END IF
RETURN
END OPERATION
Der aufrufende Prozess p wird blockiert bis der Timer ein wakeup ausführt.
Setzt das Timerintervall und ein Flag ob der Timer einmalig oder periodisch arbeitet
Startet den Timer
Anders als bei den vorherigen Synchronisationsobjekten dienen Kanäle und Warteschlangen der synchronisierten Nachrichtenübertragung zwischen Prozessen: Daten + Synchronisation
Kanäle werden auch häufig in der verteilten Programmierung eingesetzt.
Kanäle sind daher Semaphoren (Produzenten-Konsumenten System!) mit Daten
Ein Kanal kann multidimensional sein (tupelbasiert)
Schreiben eines Datenwertes oder Datensatzes. Bedingung: Wenn der Kanal voll ist wird der schreibende Prozess solange blockiert und in eine Prozesswarteschlange eingereiht bis ein lesender Prozess teilnimmt.
Testet den Kanal ob er leer ist (keine Daten enthält)