← Startseite
đŸ§”

Vorlesung 8

Prozesse, Threads & Synchronisation
Wenn viele Dinge gleichzeitig laufen — und sich nicht in die Quere kommen

Verteilte Systeme
Prof. Dr. Alexandra Mikityuk
HTW Berlin

Prozesse Threads Race Conditions

Lernziele fĂŒr heute

  • Den Unterschied zwischen Prozess und Thread verstehen
  • NebenlĂ€ufig vs. parallel sauber auseinanderhalten
  • Eine Race Condition in einem Code erkennen
  • Kritischer Abschnitt & wechselseitiger Ausschluss begreifen
  • Mutex / Semaphor in C anwenden, um das Problem zu lösen

RĂŒckblick & Einordnung

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.

📡 Bisher: zwischen Knoten

Kommunikation ĂŒber Netzwerk — HTTP-Request/Response, MQTT-Publish/Subscribe. Mehrere Rechner tauschen Nachrichten aus.

đŸ§” Heute: im Knoten

NebenlĂ€ufigkeit innerhalb eines Rechners — mehrere Prozesse/Threads laufen gleichzeitig und teilen sich Ressourcen.

Warum jetzt? NebenlĂ€ufigkeit ist die Grundlage jeder verteilten Verarbeitung — bevor wir Knoten koordinieren, mĂŒssen wir verstehen, wie ein einzelner Knoten parallele Aufgaben sauber abwickelt.
đŸ€”

Denkt mal kurz mit


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.

Teil A · Was ist ein Prozess?

🏱 Erst die Alltags-Analogie

Ein Prozess ist wie ein eigenes, abgeschlossenes BĂŒro — eigene Möbel, eigener Aktenschrank, und die Illusion, allein in der Firma zu sein.

📘 Dann die Definition

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.

Nachteil: Prozesse können nicht ohne Weiteres miteinander kommunizieren — sie haben getrennte AdressrĂ€ume. Datenaustausch braucht eigene Mechanismen (IPC).

Prozesstabelle & Prozessleitblock (PCB)

Das Betriebssystem verwaltet alle Prozesse in einer Prozesstabelle. Jeder Eintrag ist ein Prozessleitblock (Process Control Block, PCB).

đŸȘȘ IdentitĂ€t & Zustand

  • PID (Prozess-Identifikator)
  • Prozesszustand
  • ProgrammzĂ€hler
  • CPU-Registerinhalte
  • PrioritĂ€ten & Statistiken

📂 Ressourcen

  • Zeiger auf Text-, Daten- und Stacksegment
  • Offene Dateien
  • Netzwerkverbindungen
  • Zugriffsrechte
Der PCB ist das „GedĂ€chtnis" eines Prozesses: beim Kontextwechsel sichert das BS hier alles, um den Prozess spĂ€ter exakt fortzusetzen.

ProzesszustÀnde

Ein Prozess durchlÀuft im Laufe seines Lebens drei HauptzustÀnde:

⚙ rechnend

Die CPU ist diesem Prozess zugeteilt — seine Befehle laufen gerade.

⏳ rechenbereit

AusfĂŒhrbar, aber wartet auf die CPU — ein anderer Prozess ist gerade dran.

đŸš« blockiert

Wartet auf ein Ereignis — z.B. Tastatur, Festplatte oder Netzwerk.

ÜbergĂ€nge: Der Scheduler teilt die CPU zu (rechenbereit → rechnend). Wartet ein Prozess auf I/O, wird er blockiert (rechnend → blockiert). Kommt das Ereignis, wird er wieder rechenbereit.

Prozesse erzeugen — fork()

POSIX-Systeme erzeugen neue Prozesse mit dem Systemaufruf fork().

👹‍👩 Eltern & Kind

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).

🌳 Prozess-Hierarchie

So entsteht ein Baum: jeder Prozess hat einen Eltern-Prozess, kann selbst Kinder erzeugen — eine ganze Familie von Prozessen.

Woher kommt der erste Prozess? Beim Systemstart erzeugt das Betriebssystem den allerersten Prozess — von ihm stammen per fork() alle weiteren ab.

Sequentiell · nebenlÀufig · parallel

🍳 KĂŒchen-Analogie

Zwei Köche, eine Herdplatte → sie wechseln sich ab (nebenlĂ€ufig). Zwei Köche, zwei Herdplatten → wirklich gleichzeitig (parallel).

1ïžâƒŁ sequentiell

Eine Aufgabe nach der anderen — erst fertig, dann die nĂ€chste.

🔀 nebenlĂ€ufig

Zeitlich ĂŒberlappend — auf 1 CPU durch schnelles Umschalten zwischen Aufgaben.

⏞ parallel

Echt gleichzeitig — braucht mehrere CPU-Kerne.

Faustregel: Single-Core → nur nebenlĂ€ufig. Multi-Core → auch echt parallel. Zum Schutz trennt das BS außerdem User-Mode (unprivilegiert) von Kernel-Mode (voller Hardwarezugriff).

Teil B · Threads = Leichtgewichtprozesse

đŸ§‘â€đŸ’Œ Erst die Analogie

Wenn der Prozess das BĂŒro ist, sind Threads die Mitarbeiter im selben BĂŒro — sie teilen sich denselben Schreibtisch und Aktenschrank (= gemeinsamer Speicher).

📘 Dann die Definition

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.

Vorteil: blitzschnelle Kommunikation ĂŒber gemeinsamen Speicher.
Gefahr: genau dieser gemeinsame Speicher → sie kommen sich in die Quere (Teil C!).

Prozess vs. Thread im Vergleich

ProzessThread
Speichergetrennt (eigener Adressraum)gemeinsam (im selben Prozess)
Erzeugungteuer (eigener Adressraum)leicht (nur Stack + Register)
KommunikationĂŒber IPC (umstĂ€ndlich)ĂŒber gemeinsamen Speicher (direkt)
Absturzisoliert — betrifft nur sichreißt den ganzen Prozess mit
KontextwechselteuergĂŒnstig
Merksatz: Threads sind schnell und sparsam — aber weil sie sich Speicher teilen, sind sie auch gefĂ€hrlicher. Genau das ist das Thema von Teil C.

POSIX-Threads (pthreads) in C

Header #include <pthread.h> · kompilieren mit gcc prog.c -lpthread

🧰 Die wichtigsten Aufrufe

pthread_createThread erzeugen
pthread_joinauf Thread warten
pthread_exitThread beenden
pthread_mutex_lockSperre nehmen
pthread_mutex_unlockSperre freigeben

👋 Hello-World-Thread

#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;
}

Parallelisierungsmodelle

Wie verteilt man Arbeit auf mehrere Threads? Drei klassische Muster:

đŸ‘· Master-Worker

Ein Master verteilt Arbeitspakete an freie Worker. Klassisch fĂŒr „viele gleichartige Aufgaben".

đŸ—‚ïž Thread-Pool / Work-Queue

Eine feste Anzahl Threads holt sich Jobs aus einer gemeinsamen Queue. Spart stÀndiges Neu-Erzeugen.

🏭 Pipeline

Threads bearbeiten am Fließband nacheinander Teilaufgaben — z.B. Intrusion-Detection: Pakete gegen verschiedene Angriffssignaturen.

Allen gemeinsam: die Threads teilen Daten (Queue, Ergebnisse, ZĂ€hler) → wir brauchen Synchronisation. Warum, kommt jetzt.

Teil C · Auflösung: counter++ ist NICHT atomar

Es sieht aus wie ein einziger Schritt. In Wirklichkeit sind es drei:

1ïžâƒŁ Laden

counter aus dem Speicher in ein Register laden.

2ïžâƒŁ Rechnen

Im Register +1 rechnen.

3ïžâƒŁ Schreiben

Das Ergebnis zurĂŒck in den Speicher schreiben.

int counter = 0;        // gemeinsame Variable
...
counter++;             // NICHT atomar: laden, +1, schreiben
Zwei Threads können sich zwischen diesen Schritten dazwischendrÀngeln. Genau dann geht etwas schief.

Interleaving — das „Lost Update"

Beide Threads wollen counter (Startwert 5) um 1 erhöhen — erwartet wĂ€ren 7.

ZeitThread AThread Bcounter
t1liest counter (5)—5
t2—liest counter (5)5
t3rechnet 5 + 1 = 6—5
t4schreibt 6—6
t5—rechnet 5 + 1 = 66
t6—schreibt 66
Ergebnis 6 statt 7 — ein Update ging verloren („Lost Update"). Genau deshalb kommt bei 1000 + 1000 oft weniger als 2000 raus.

Race Condition — Definition

HĂ€ngt das Ergebnis nebenlĂ€ufiger Prozesse von der zeitlichen Reihenfolge ihrer AusfĂŒhrung ab, spricht man von einer Race Condition (kritischer Wettlauf).

đŸ–šïž Klassisches Beispiel: Druckerspooler

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.

TĂŒckisch: Solche Bugs treten nur bei bestimmtem Timing auf, sind schwer reproduzierbar („Heisenbugs") → unbedingt durch Design vermeiden.

Kritischer Abschnitt

Der Programmteil, in dem mehrere Prozesse verĂ€ndernd auf gemeinsam genutzte Ressourcen zugreifen, heißt kritischer Abschnitt (kritische Region).

☎ Analogie: die Telefonzelle

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.

Ziel: Im kritischen Abschnitt darf zu jedem Zeitpunkt höchstens ein Prozess/Thread sein.

Die 4 Regeln fĂŒr eine korrekte Lösung

1ïžâƒŁ Wechselseitiger Ausschluss

Nie zwei gleichzeitig im kritischen Abschnitt.

2ïžâƒŁ Keine Annahmen

Keine Annahmen ĂŒber Geschwindigkeit oder Anzahl der CPUs.

3ïžâƒŁ Keine Blockade von außen

Kein Prozess außerhalb des kritischen Abschnitts darf andere blockieren.

4ïžâƒŁ Kein ewiges Warten

Kein Prozess wartet ewig (Starvation-frei).

Kernziel = Mutual Exclusion (wechselseitiger Ausschluss): zu jedem Zeitpunkt höchstens einer im kritischen Abschnitt.

Teil D · Naive Versuche (und warum sie scheitern)

Wir kennen jetzt die 4 Regeln. Was wĂ€re die naheliegende erste Idee, um zwei Threads auseinanderzuhalten? Zwei Versuche — beide kippen.

đŸš© Versuch 1: Sperrvariable flag

Idee: 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.

🔁 Versuch 2: Strikter Wechsel turn

Idee: 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:

  • Striktes Alternieren: A darf nie zweimal hintereinander — selbst wenn B gar nicht rein will.
  • Ist B außerhalb des Abschnitts langsam/untĂ€tig, blockiert es A → verletzt Regel 3.
  • Aktives Warten (Spinnen im while) verbrennt CPU-Zeit.
Lehre: reine Software-Tricks reichen nicht — „abfragen + setzen" muss untrennbar (atomar) sein. Hilfe kommt von der Hardware: moderne CPUs bieten atomare Test-and-Set-Befehle. Darauf bauen Semaphor & Mutex (nĂ€chste Slides) — die blockieren den Thread, statt ihn busy-warten zu lassen.

Semaphor (Dijkstra, 1965)

đŸ…żïž Analogie: Parkhaus

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".

đŸ‘€ Wer war Dijkstra?

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".

📘 Definition

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

🔄 Aktives Warten (Busy-Waiting)

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.)

😮 Blockieren (Semaphor)

Der Thread wird schlafen gelegt, gibt die CPU frei und braucht 0 % Rechenzeit. Wird ein Platz frei, weckt ihn UP wieder. Effizient statt verschwenderisch.

Warum „atomar"? atomar = unteilbar. 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.

BinÀrer Semaphor = Mutex

Setzt man den Anfangswert auf 1, darf nur einer zugleich rein — das ist ein Mutex (mutual exclusion).

🔑 Analogie

Ein SchlĂŒssel fĂŒr einen Raum — wer ihn hat, darf rein; alle anderen warten, bis der SchlĂŒssel zurĂŒckkommt.

Ablauf: vor dem kritischen Abschnitt DOWN / lock (SchlĂŒssel nehmen), danach UP / unlock (SchlĂŒssel zurĂŒckgeben).

Mutex in C — die Denkfrage gelöst

🔒 pthread-Mutex behebt counter

#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-API

pthread_mutex_initanlegen
pthread_mutex_locksperren
pthread_mutex_trylockversuchen
pthread_mutex_unlockfreigeben
pthread_mutex_destroyaufrÀumen
Jetzt kommt zuverlĂ€ssig 2000 raus — immer nur ein Thread ist im kritischen Abschnitt. Die Denkfrage von Slide 4 ist gelöst.

Deadlock — wenn sich zwei Threads gegenseitig einsperren

Es gibt zwei Sperren, A und B. Jeder Thread schnappt sich zuerst eine — und will dann die andere.

ZeitThread 1Thread 2Lage
t1nimmt Sperre A ✅nimmt Sperre B ✅jeder hat eine
t2will B → wartet (T2 hat B)will A → wartet (T1 hat A)beide warten
t3hÀlt A weiter fest beim WartenhÀlt B weiter fest beim Wartenniemand gibt ab
t4gibt A erst frei, wenn es B hatgibt B erst frei, wenn es A hatfĂŒr immer ⛔

đŸ„ą Analogie: ein StĂ€bchen jeder

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.

💡 Der Denkfehler

„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.

Die Faustregel, die es verhindert: alle Threads nehmen Sperren immer in derselben Reihenfolge (erst A, dann B). Dann kann der Kreis „T1 wartet auf T2 wartet auf T1" gar nicht entstehen. Alternativ: trylock mit Timeout — ein Thread gibt auf und lĂ€sst los.

Producer-Consumer-Problem

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.

Warum gerade jetzt? Producer-Consumer ist die Standard-Anwendung von Semaphoren: Es braucht beides gleichzeitig — einen Semaphor zum ZĂ€hlen freier/belegter PlĂ€tze (genau der Parkhaus-ZĂ€hler von vorhin) und einen Mutex zum SchĂŒtzen des Puffers. Und das Muster ist 1:1 das, was im verteilten System eine Message Queue macht (nĂ€chste Slide).
🏭
Produzent
schreibt
→
📩
Puffer (N PlÀtze)
→
🛒
Konsument
liest

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").

Bezug zu verteilten Systemen

Mutex & Semaphor brauchen gemeinsamen Speicher — den gibt es in einem verteilten System (mehrere Rechner) oft nicht!

đŸ§” Ein Rechner

Threads teilen Speicher → Synchronisation ĂŒber Mutex / Semaphor.

🌐 Mehrere Rechner

Kein gemeinsamer Speicher → Synchronisation ĂŒber Nachrichtenaustausch (Message Passing: send() / receive()).

RĂŒckbezug zur Blockchain: genau dieses Problem — wie einigen sich viele Rechner ohne gemeinsamen Speicher auf eine Reihenfolge von Ereignissen? — kennt ihr schon von der Blockchain (VS-Vorlesungen 4 & 5). Dort legt ein Konsens-Verfahren fest, in welcher Reihenfolge Transaktionen gelten — ganz ohne zentralen Koordinator und ohne gemeinsamen Speicher.

🧠 Quiz

Was ist die Ursache, dass counter++ aus zwei Threads ein falsches Ergebnis liefert?

ADer Compiler hat einen Bug.
Bcounter++ ist nicht atomar — die Threads drĂ€ngeln sich zwischen Laden und Schreiben.
CThreads sind langsamer als Prozesse.
DEs fehlt ein return.

→ Klicke auf eine Antwort, um aufzulösen

Zusammenfassung

  • Prozess = eigener Speicher · Thread = gemeinsamer Speicher im selben Prozess
  • nebenlĂ€ufig (ĂŒberlappend) ≠ parallel (echt gleichzeitig, mehrere Kerne)
  • Race Condition = das Ergebnis hĂ€ngt vom Timing ab
  • Kritischer Abschnitt + 4 Regeln, Ziel: wechselseitiger Ausschluss
  • Mutex / Semaphor lösen es — ohne Busy-Waiting
  • In verteilten Systemen oft Message Passing statt gemeinsamem Speicher

Prof. Dr. Alexandra Mikityuk
HTW Berlin · BĂŒro Raum 308

© 2026 HTW Berlin · Verteilte Systeme

1 /