Verteilte Systeme
Prof. Dr. Alexandra Mikityuk
HTW Berlin
Bisher (VL6 HTTP, VL7 MQTT) ging es darum, wie Systeme miteinander reden. Heute schauen wir, was in einem Knoten passiert, wenn viele Dinge gleichzeitig laufen.
Kommunikation ĂŒber Netzwerk â HTTP-Request/Response, MQTT-Publish/Subscribe. Mehrere Rechner tauschen Nachrichten aus.
NebenlĂ€ufigkeit innerhalb eines Rechners â mehrere Prozesse/Threads laufen gleichzeitig und teilen sich Ressourcen.
Zwei Threads erhöhen dieselbe Variable counter je 1000-mal. Am Ende sollte 2000 drinstehen.
Aber wenn man es ausfĂŒhrt, kommt oft weniger raus.
đ Wie kann 1000 + 1000 â 2000 sein?
Wir lösen das in Teil C auf. Merkt euch die Frage.
Ein Prozess ist wie ein eigenes, abgeschlossenes BĂŒro â eigene Möbel, eigener Aktenschrank, und die Illusion, allein in der Firma zu sein.
Ein Prozess ist ein in AusfĂŒhrung befindliches Programm. Das Betriebssystem gibt jedem Prozess eigenen Speicher und eigene CPU-Zeit und simuliert exklusiven Zugriff auf die Maschine.
Das Betriebssystem verwaltet alle Prozesse in einer Prozesstabelle. Jeder Eintrag ist ein Prozessleitblock (Process Control Block, PCB).
Ein Prozess durchlÀuft im Laufe seines Lebens drei HauptzustÀnde:
Die CPU ist diesem Prozess zugeteilt â seine Befehle laufen gerade.
AusfĂŒhrbar, aber wartet auf die CPU â ein anderer Prozess ist gerade dran.
Wartet auf ein Ereignis â z.B. Tastatur, Festplatte oder Netzwerk.
fork()POSIX-Systeme erzeugen neue Prozesse mit dem Systemaufruf fork().
Der neue Prozess (child) ist eine Kopie des Eltern-Prozesses (parent) und lÀuft nebenlÀufig. Danach lÀdt das Kind oft neuen Code (mit exec).
So entsteht ein Baum: jeder Prozess hat einen Eltern-Prozess, kann selbst Kinder erzeugen â eine ganze Familie von Prozessen.
fork() alle weiteren ab.
Zwei Köche, eine Herdplatte â sie wechseln sich ab (nebenlĂ€ufig). Zwei Köche, zwei Herdplatten â wirklich gleichzeitig (parallel).
Eine Aufgabe nach der anderen â erst fertig, dann die nĂ€chste.
Zeitlich ĂŒberlappend â auf 1 CPU durch schnelles Umschalten zwischen Aufgaben.
Echt gleichzeitig â braucht mehrere CPU-Kerne.
Wenn der Prozess das BĂŒro ist, sind Threads die Mitarbeiter im selben BĂŒro â sie teilen sich denselben Schreibtisch und Aktenschrank (= gemeinsamer Speicher).
Ein Thread ist eine AusfĂŒhrungseinheit innerhalb eines Prozesses. Er lĂ€uft nebenlĂ€ufig zu anderen Threads, teilt sich Speicher und Dateien des Prozesses, hat aber eigenen ProgrammzĂ€hler, Stack und Register.
| Prozess | Thread | |
|---|---|---|
| Speicher | getrennt (eigener Adressraum) | gemeinsam (im selben Prozess) |
| Erzeugung | teuer (eigener Adressraum) | leicht (nur Stack + Register) |
| Kommunikation | ĂŒber IPC (umstĂ€ndlich) | ĂŒber gemeinsamen Speicher (direkt) |
| Absturz | isoliert â betrifft nur sich | reiĂt den ganzen Prozess mit |
| Kontextwechsel | teuer | gĂŒnstig |
Header #include <pthread.h> · kompilieren mit gcc prog.c -lpthread
pthread_create | Thread erzeugen |
pthread_join | auf Thread warten |
pthread_exit | Thread beenden |
pthread_mutex_lock | Sperre nehmen |
pthread_mutex_unlock | Sperre freigeben |
#include <pthread.h>
#include <stdio.h>
void* sag_hallo(void* arg) {
printf("Hallo vom Thread!\n");
return NULL;
}
int main(void) {
pthread_t t;
pthread_create(&t, NULL, sag_hallo, NULL); // Thread starten
pthread_join(t, NULL); // auf Thread warten
return 0;
}
Wie verteilt man Arbeit auf mehrere Threads? Drei klassische Muster:
Ein Master verteilt Arbeitspakete an freie Worker. Klassisch fĂŒr âviele gleichartige Aufgaben".
Eine feste Anzahl Threads holt sich Jobs aus einer gemeinsamen Queue. Spart stÀndiges Neu-Erzeugen.
Threads bearbeiten am FlieĂband nacheinander Teilaufgaben â z.B. Intrusion-Detection: Pakete gegen verschiedene Angriffssignaturen.
counter++ ist NICHT atomarEs sieht aus wie ein einziger Schritt. In Wirklichkeit sind es drei:
counter aus dem Speicher in ein Register laden.
Im Register +1 rechnen.
Das Ergebnis zurĂŒck in den Speicher schreiben.
int counter = 0; // gemeinsame Variable
...
counter++; // NICHT atomar: laden, +1, schreiben
Beide Threads wollen counter (Startwert 5) um 1 erhöhen â erwartet wĂ€ren 7.
| Zeit | Thread A | Thread B | counter |
|---|---|---|---|
| t1 | liest counter (5) | â | 5 |
| t2 | â | liest counter (5) | 5 |
| t3 | rechnet 5 + 1 = 6 | â | 5 |
| t4 | schreibt 6 | â | 6 |
| t5 | â | rechnet 5 + 1 = 6 | 6 |
| t6 | â | schreibt 6 | 6 |
HĂ€ngt das Ergebnis nebenlĂ€ufiger Prozesse von der zeitlichen Reihenfolge ihrer AusfĂŒhrung ab, spricht man von einer Race Condition (kritischer Wettlauf).
Prozess A liest den freien Platz in=7, wird unterbrochen. Prozess B liest auch in=7, legt seinen Job auf Platz 7, setzt in=8. A macht weiter und legt seinen Job ebenfalls auf Platz 7 â Bs Job ist ĂŒberschrieben und verloren.
Der Programmteil, in dem mehrere Prozesse verĂ€ndernd auf gemeinsam genutzte Ressourcen zugreifen, heiĂt kritischer Abschnitt (kritische Region).
In eine Telefonzelle passt nur einer rein. Solange jemand drin telefoniert, mĂŒssen alle anderen drauĂen warten â genau das wollen wir fĂŒr den kritischen Abschnitt erreichen.
Nie zwei gleichzeitig im kritischen Abschnitt.
Keine Annahmen ĂŒber Geschwindigkeit oder Anzahl der CPUs.
Kein Prozess auĂerhalb des kritischen Abschnitts darf andere blockieren.
Kein Prozess wartet ewig (Starvation-frei).
Wir kennen jetzt die 4 Regeln. Was wĂ€re die naheliegende erste Idee, um zwei Threads auseinanderzuhalten? Zwei Versuche â beide kippen.
flagIdee: eine gemeinsame Variable. âIst sie frei? Dann belege ich sie."
while (flag); // warten bis frei (flag==0)
flag = 1; // belegen
... // kritischer Abschnitt
flag = 0; // freigeben
Warum es scheitert: âabfragen" und âsetzen" sind zwei Schritte (nicht atomar). Beide Threads sehen flag==0, beide laufen durch das while, beide setzen flag=1 â und sind gleichzeitig drin. Die Race Condition ist nur von counter nach flag gewandert.
turnIdee: eine Variable turn sagt, âwer dran ist". Thread 0 wartet, solange turnâ 0, dann Thread 1 â immer schön abwechselnd.
Ausschluss klappt â aber es bricht andere Regeln:
while) verbrennt CPU-Zeit.Ein Parkhaus mit N PlĂ€tzen und einer Schranke â sind alle voll, mĂŒssen weitere Autos warten, bis einer rausfĂ€hrt. Der ZĂ€hler S = âfreie PlĂ€tze".
Edsger W. Dijkstra (1930â2002), niederlĂ€ndischer Informatiker und Pionier. Erfand 1965 den Semaphor, den berĂŒhmten Dijkstra-Algorithmus (kĂŒrzeste Wege im Graphen) und prĂ€gte die strukturierte Programmierung. Turing-Award 1972 â der âNobelpreis der Informatik".
Eine ZĂ€hler-Variable S mit zwei atomaren Operationen DOWN (Platz nehmen) und UP (Platz freigeben). Anfangswert N erlaubt N gleichzeitige Nutzer. Ist kein Platz frei, wird der Prozess blockiert (schlafen gelegt) â statt aktiv zu warten.
DOWN(S) { S = S - 1; if (S < 0) blockieren; } // Platz belegen, sonst warten
UP(S) { S = S + 1; if (Warteschlange) einen wecken; } // Platz frei â nĂ€chsten wecken
Der Thread hĂ€ngt in einer Schleife wie while (flag); und fragt ununterbrochen âschon frei? schon frei?". Er bleibt die ganze Zeit wach und verbrennt CPU-Zeit, ohne etwas zu schaffen. (Das machten die naiven Versuche auf der vorigen Slide.)
Der Thread wird schlafen gelegt, gibt die CPU frei und braucht 0 % Rechenzeit. Wird ein Platz frei, weckt ihn UP wieder. Effizient statt verschwenderisch.
DOWN und UP laufen komplett durch, ohne dass ein anderer Thread dazwischengrĂ€tscht. Sonst hĂ€tten wir bei S = S - 1 genau dieselbe Race Condition wie bei counter++ â der âBeschĂŒtzer" wĂ€re selbst kaputt.
Setzt man den Anfangswert auf 1, darf nur einer zugleich rein â das ist ein Mutex (mutual exclusion).
Ein SchlĂŒssel fĂŒr einen Raum â wer ihn hat, darf rein; alle anderen warten, bis der SchlĂŒssel zurĂŒckkommt.
DOWN / lock (SchlĂŒssel nehmen), danach UP / unlock (SchlĂŒssel zurĂŒckgeben).
#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
void* zaehle(void* arg) {
for (int i = 0; i < 1000; i++) {
pthread_mutex_lock(&lock); // DOWN â SchlĂŒssel nehmen
counter++; // kritischer Abschnitt
pthread_mutex_unlock(&lock); // UP â SchlĂŒssel zurĂŒck
}
return NULL;
}
pthread_mutex_init | anlegen |
pthread_mutex_lock | sperren |
pthread_mutex_trylock | versuchen |
pthread_mutex_unlock | freigeben |
pthread_mutex_destroy | aufrÀumen |
Es gibt zwei Sperren, A und B. Jeder Thread schnappt sich zuerst eine â und will dann die andere.
| Zeit | Thread 1 | Thread 2 | Lage |
|---|---|---|---|
| t1 | nimmt Sperre A â | nimmt Sperre B â | jeder hat eine |
| t2 | will B â wartet (T2 hat B) | will A â wartet (T1 hat A) | beide warten |
| t3 | hÀlt A weiter fest beim Warten | hÀlt B weiter fest beim Warten | niemand gibt ab |
| t4 | gibt A erst frei, wenn es B hat | gibt B erst frei, wenn es A hat | fĂŒr immer â |
Zwei Leute, dazwischen zwei StĂ€bchen. Jeder greift eines und sagt: âIch esse, sobald ich beide habe â und gebe meines vorher nicht her." Beide halten, beide warten, beide verhungern.
âAber der andere hat die Sperre doch â also löst es sich?" Nein: ein wartender Thread gibt seine eigene Sperre nicht her. Halten und Warten passieren gleichzeitig. Freigeben kĂ€me erst nach dem kritischen Abschnitt â den keiner erreicht.
trylock mit Timeout â ein Thread gibt auf und lĂ€sst los.
Wir haben jetzt die Werkzeuge (Mutex, Semaphor) und kennen die Gefahr (Deadlock). Zeit fĂŒr das berĂŒhmteste Ăbungs-Beispiel, das alles zusammenbringt â und das uns zu verteilten Systemen fĂŒhrt.
Ein Produzent schreibt in einen Puffer mit N PlĂ€tzen, ein Konsument liest. Sie mĂŒssen sich abstimmen: nicht in einen vollen Puffer schreiben, nicht aus einem leeren lesen. Das Standardmuster kombiniert einen Mutex (Zugriffsschutz) mit Bedingungen (warten auf âvoll" / âleer").
Mutex & Semaphor brauchen gemeinsamen Speicher â den gibt es in einem verteilten System (mehrere Rechner) oft nicht!
Threads teilen Speicher â Synchronisation ĂŒber Mutex / Semaphor.
Kein gemeinsamer Speicher â Synchronisation ĂŒber Nachrichtenaustausch (Message Passing: send() / receive()).
Was ist die Ursache, dass counter++ aus zwei Threads ein falsches Ergebnis liefert?
counter++ ist nicht atomar â die Threads drĂ€ngeln sich zwischen Laden und Schreiben.return.â Klicke auf eine Antwort, um aufzulösen
counter++ zerfĂ€llt in drei Schritte (laden, +1, schreiben). Ohne Sperre kann ein zweiter Thread einen veralteten Wert lesen und das Update des ersten ĂŒberschreiben â ein Lost Update. Genau das verhindert ein Mutex.
Prof. Dr. Alexandra Mikityuk
HTW Berlin · BĂŒro Raum 308
© 2026 HTW Berlin · Verteilte Systeme