praca_magisterska/pytania/questions/pytanie_09.md

510 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## PYTANIE 9: Procesy i wątki (SOI)
**Budowa, szybkość, zastosowanie. Problemy komunikacji i synchronizacji.**
---
### Tło pojęciowe — słowniczek
**Proces (process)** — program w trakcie wykonania. Każdy proces ma własną, izolowaną przestrzeń adresową. System operacyjny zarządza procesami — tworzy, planuje (scheduling), kończy. Np. przeglądarka i edytor to osobne procesy.
**Wątek (thread)** — lekka jednostka wykonania wewnątrz procesu. Wątki jednego procesu współdzielą pamięć (kod, dane, heap), ale mają własny stos i rejestry CPU. Tworzenie wątku jest ~100× szybsze niż procesu.
![Proces vs Wątek — porównanie](img/q9_process_vs_thread.png)
---
**Przestrzeń adresowa (address space)** — zakres adresów pamięci wirtualnej dostępnych procesowi. Każdy proces widzi swoją „prywatną" pamięć, nawet jeśli fizycznie jest mapowana gdzieś indziej.
**Segmenty pamięci procesu:**
- **TEXT** — kod maszynowy (read-only)
- **DATA** — zainicjalizowane zmienne globalne/statyczne
- **BSS** — niezainicjalizowane zmienne globalne (zerowane)
- **HEAP** — pamięć alokowana dynamicznie (malloc/new), rośnie w górę
- **STACK** — zmienne lokalne, adresy powrotu, rośnie w dół
![Segmenty pamięci procesu](img/q9_memory_layout.png)
**PCB (Process Control Block)** — struktura danych w jądrze OS opisująca proces: PID, stan, rejestry CPU, tablice stron, otwarte pliki, priorytety. Przełączenie kontekstu = zapisanie PCB starego procesu i wczytanie nowego.
![PCB — Process Control Block](img/q9_pcb_structure.png)
**PID (Process ID)** — unikalny identyfikator procesu w systemie. Np. `PID 1` = init/systemd w Linux.
**TID (Thread ID)** — unikalny identyfikator wątku.
**Stany procesu:** NEW (tworzony) → READY (gotowy, czeka na CPU) ↔ RUNNING (wykonywany) → BLOCKED (czeka na I/O), TERMINATED (zakończony). Scheduler decyduje, który READY staje się RUNNING.
![Stany procesu — diagram przejść](img/q9_process_states.png)
---
**Przełączanie kontekstu (context switch)** — zapisanie stanu aktualnego procesu/wątku i wczytanie stanu następnego. Dla procesów kosztowne (wymaga TLB flush = unieważnienie cache translacji adresów). Dla wątków tańsze (ta sama przestrzeń adresowa = brak TLB flush).
**TLB (Translation Lookaside Buffer)** — sprzętowy cache translacji adres wirtualny → fizyczny. Przy zmianie procesu TLB trzeba wyczyścić (flush), bo nowy proces ma inne mapowania. Koszt: ~1000 ns. Przy zmianie wątku — TLB zostaje (ten sam proces).
---
**IPC (Inter-Process Communication)** — mechanizmy komunikacji między procesami. Konieczne, bo procesy mają izolowane przestrzenie adresowe i nie mogą czytać wzajemnej pamięci bezpośrednio.
- **Pipe** — jednokierunkowy strumień bajtów (ls | grep foo). Anonimowy, tylko między spokrewnionymi procesami.
- **Named Pipe (FIFO)** — pipe z nazwą w systemie plików, mogą go używać niespokrewnione procesy.
- **Message Queue** — kolejka wiadomości w jądrze; asynchroniczna komunikacja.
- **Shared Memory** — wspólny region pamięci; najszybszy IPC (brak kopiowania), ale wymaga synchronizacji.
- **Socket** — komunikacja sieciowa lub lokalna (Unix domain socket). Uniwersalny, działa między maszynami.
- **Signal** — asynchroniczne powiadomienie (np. SIGKILL, SIGTERM). Ograniczony — przesyła tylko numer sygnału.
---
**Wyścig (race condition)** — sytuacja, gdy wynik programu zależy od kolejności wykonania operacji przez wątki. Przykład: dwa wątki zwiększają x=0 o 1 → wynik może być 1 zamiast 2, bo oba czytają 0 zanim zapiszą.
![Wyścig — race condition](img/q9_race_condition.png)
**Sekcja krytyczna (critical section)** — fragment kodu, który może być wykonywany przez najwyżej jeden wątek naraz. Chroni współdzielone zasoby przed race condition.
**Zakleszczenie (deadlock)** — sytuacja, w której dwa lub więcej wątków czekają na siebie nawzajem i żaden nie może kontynuować. Jak dwa samochody na skrzyżowaniu — oba czekają, nikt nie jedzie.
![Zakleszczenie — deadlock](img/q9_deadlock_scenario.png)
**Warunki Coffmana** — 4 warunki konieczne deadlocka (wszystkie muszą zachodzić jednocześnie):
1. **Mutual exclusion** — zasób jest wyłączny (tylko jeden wątek)
2. **Hold and wait** — trzymaj zasób, czekaj na kolejny
3. **No preemption** — nie można zabrać zasobu siłą
4. **Circular wait** — cykliczne oczekiwanie (A→B→C→A)
Złam jeden = brak deadlocka.
**Zagłodzenie (starvation)** — wątek nigdy nie dostaje zasobu, bo inni ciągle go wyprzedzają (np. nisko priorytetowy wątek przy high-priority scheduling).
---
**Mutex (MUTual EXclusion)** — zamek na sekcję krytyczną. Tylko jeden wątek może go „zamknąć" (lock); reszta czeka (sleep). Tryb: lock → sekcja krytyczna → unlock.
**Semafor (semaphore)** — uogólniony mutex z licznikiem. Semafor binarny (0/1) = mutex. Semafor zliczający (n) — pozwala n wątkom jednocześnie. P() = probeer (zmniejsz), V() = verhoog (zwiększ).
![Semafor — koncepcja](img/q9_semaphore_concept.png)
**Monitor** — wysokopoziomowy mechanizm synchronizacji. Obiekt z mutexem wbudowanym — tylko jeden wątek może wykonywać metody monitora. Java: `synchronized`.
**Condition Variable** — pozwala wątkowi czekać (wait) na spełnienie warunku i być obudzonym (signal/notify) przez inny wątek. Używane z mutexem.
**Spinlock** — zamek, w którym wątek aktywnie czeka w pętli (busy-wait) zamiast zasypiać. Szybki dla bardzo krótkich sekcji krytycznych (~ns), marnotrawny dla dłuższych.
**Read-Write Lock** — pozwala wielu czytelnikom jednocześnie LUB jednemu pisarzowi. Optymalizacja dla scenariuszy z dużo odczytów i rzadkimi zapisami.
**Barrier** — punkt synchronizacji: wszystkie wątki muszą dotrzeć do bariery, zanim którykolwiek może kontynuować. Użyteczna w obliczeniach równoległych (np. po każdej iteracji).
---
### Budowa procesu
Proces = program w trakcie wykonania + cały jego kontekst. Składa się z **3 filarów**: pamięć, PCB, stany.
**Filar 1 — Pamięć (oddzielna przestrzeń adresowa):**
Każdy proces dostaje własną, izolowaną przestrzeń adresową. Inne procesy NIE widzą tej pamięci. Składa się z 5 segmentów (od niskich do wysokich adresów): TEXT → DATA → BSS → HEAP → STACK.
![Segmenty pamięci procesu — szczegóły](img/q9_memory_layout.png)
Konkretny przykład — mapowanie kodu C na segmenty pamięci:
![Mapowanie kodu C na segmenty pamięci](img/q9_memory_c_annotated.png)
**Filar 2 — PCB (Process Control Block):**
PCB to „dowód osobisty" procesu w jądrze OS. Zawiera WSZYSTKO co OS musi wiedzieć: PID, stan, rejestry CPU, tablice stron, otwarte pliki, priorytety, statystyki.
![PCB — struktura z konkretnymi wartościami](img/q9_pcb_detailed.png)
Przełączenie kontekstu = zapisanie PCB starego procesu → wczytanie PCB nowego.
**Filar 3 — Stany procesu:**
![Stany procesu — diagram przejść z opisami](img/q9_process_states_labeled.png)
Scheduler decyduje, który READY staje się RUNNING.
> **Mnemonik BUDOWY PROCESU — „3 filary: MPS":**
> **M**ieszkanie, **P**aszport, **S**tatus = **M**emory (5 segmentów), **P**CB (dowód), **S**tany (5 stanów).
>
> Segmenty od dołu — **„TDBHS"**: **T**ata **D**aje **B**abci **H**erbatę ze **S**mietanką = TEXT → DATA → BSS → HEAP → STACK.
>
> Stany — **„NRRBT"**: **N**igdy **R**ano **R**ybki **B**iegać nie **T**rafią = NEW → READY → RUNNING → BLOCKED → TERMINATED.
>
> PCB = paszport procesu — bez niego OS nie wie kim jest proces.
### Budowa wątku
Wątek = lekka jednostka wykonania WEWNĄTRZ procesu.
Kluczowa idea: wątek to NIE osobny byt — to dodatkowa „ścieżka wykonania" w istniejącym procesie. Proces z 3 wątkami ma 1 przestrzeń adresową, ale 3 niezależne ciągi instrukcji.
![Wątki wewnątrz procesu — współdzielone vs prywatne](img/q9_thread_shared_private.png)
**Co WSPÓŁDZIELONE**: TEXT, DATA, BSS, HEAP, otwarte pliki, PID — wszystkie wątki widzą to samo.
**Co PRYWATNE** (każdy wątek ma własne):
- **Stos (stack)** — zmienne lokalne TEGO wątku, ramki wywołań
- **Rejestry CPU** — stan procesora TEGO wątku (EAX, EBX, ESP...)
- **Program Counter (PC)** — KTÓRA instrukcja jest teraz wykonywana
- **TID** — unikalny identyfikator wątku
Konkretny przykład — dlaczego współdzielenie to siła i zagrożenie:
// Wątek 1 i Wątek 2 widzą TEN SAM obiekt:
int shared_counter = 0; // HEAP — współdzielone!
// Wątek 1: // Wątek 2:
shared_counter++; shared_counter++;
// Oba czytają 0, oba piszą 1 → wynik 1 zamiast 2!
// → race condition (potrzebny mutex)
Kluczowa różnica od procesu: proces ma CAŁĄ przestrzeń adresową (jak całe mieszkanie), wątek to tylko kontekst wykonania — stos + rejestry + PC (jak osoba w mieszkaniu ze swoim telefonem i pozycją w książce).
> **Mnemonik BUDOWY WĄTKU — „Wspólna kuchnia, własny pokój":**
> Współdzielone = KOD + DANE + HEAP + PLIKI (kuchnia, salon, łazienka — wszyscy korzystają).
> Prywatne = STOS + REJESTRY + PC (twój pokój, twój telefon, twoja pozycja w książce).
>
> Skrót: **„SRP"** — **S**tos, **R**ejestry, **P**C = to co PRYWATNE.
> Skrót odwrotny: **„KDHP"** — **K**od, **D**ane, **H**eap, **P**liki = to co WSPÓŁDZIELONE.
>
> Test: „Czy dwa wątki widzą tę samą zmienną globalną?" → TAK (współdzielone DATA).
> „Czy dwa wątki widzą tę samą zmienną lokalną?" → NIE (osobne stosy).
### Szybkość — porównanie ilościowe
| Operacja | Proces | Wątek | Dlaczego różnica? |
|-------------------|------------------|---------------------|--------------------------------------|
| Tworzenie | ~110 ms | ~10100 μs (100×) | Proces: nowa przestrzeń adresowa, tablice stron, kopiowanie struktur jądra. Wątek: tylko nowy stos + wpis w schedulerze. |
| Przełączanie | ~10005000 ns | ~100500 ns (10×) | Proces: TLB flush (unieważnienie cache adresów). Wątek: te same tablice stron, TLB zostaje. |
| Komunikacja | ~μs (IPC) | ~ns (pamięć) | Proces: kopiowanie przez jądro (pipe/socket). Wątek: bezpośredni zapis/odczyt heapu. |
| Zakończenie | wolne | szybkie | Proces: zwolnienie przestrzeni adresowej, zamknięcie plików. Wątek: zwolnienie stosu. |
![Szybkość — benchmarki Linux](img/q9_speed_comparison.png)
> **Mnemonik SZYBKOŚCI — „100, 10, 1000":**
> Tworzenie wątku **100×** szybsze, przełączanie **10×** szybsze, komunikacja **1000×** szybsza.
> Dlaczego? **„TLB zostaje"** — wątek nie zmienia mieszkania (przestrzeni adresowej), więc cache adresów (TLB) nie trzeba czyścić.
> Zapamiętaj: **fork() = przeprowadzka**, **pthread_create() = nowy pokój w tym samym mieszkaniu**.
### Zastosowanie — kiedy proces, kiedy wątek?
**Procesy — stosuj gdy:**
- **Izolacja jest krytyczna** — awaria jednego nie zabija reszty
- Przeglądarka Chrome: każda karta = osobny proces. Crash Flash w jednej karcie nie zabija reszty.
- Serwer: każde połączenie = fork() → klient nie może uszkodzić serwera (Apache pre-fork MPM)
- **Bezpieczeństwo** — procesy nie widzą nawzajem pamięci
- Sandboxing: proces renderujący PDFa nie ma dostępu do pamięci procesu z hasłami
- **Wieloprogramowość** — różne programy (edytor + kompilator + przeglądarka)
- **Fork-exec** — klasyczny model Unix: fork() + exec() → nowy program
**Wątki — stosuj gdy:**
- **Współdzielenie danych** — wątki czytają/piszą ten sam heap bez kopiowania
- Serwer WWW: wątki obsługujące requesty współdzielą cache w pamięci (nginx worker threads)
- Gra: wątek renderujący i wątek fizyki czytają ten sam świat gry
- **Szybkość tworzenia/przełączania** — potrzeba wielu lekkich zadań
- Thread pool: 8 wątków obsługuje tysiące zadań (zamiast tysiąca procesów)
- **Obliczenia równoległe** — podział pracy na rdzenie CPU
- Mnożenie macierzy: każdy wątek liczy fragment wyniku
- Rendering: każdy wątek renderuje część klatki
- **Responsywność UI** — wątek główny obsługuje interfejs, wątek tła liczy/pobiera dane
![Kiedy proces, kiedy wątek? — scenariusze](img/q9_scenario_table.png)
> **Mnemonik ZASTOSOWANIA — „MURY vs OKNA":**
> **Proces jak MURY** = izolacja, bezpieczeństwo, crash isolation (Chrome karty, Apache, sandboxing).
> **Wątek jak OKNA** = szybki dostęp do wspólnych danych, lekki, wydajny (gry, thread pool, UI).
> Pytanie-test: „Czy awaria jednego ma zabić resztę?" → TAK = wątek OK, NIE = proces.
> Regułka: **„IBW → P, WSO → W"** = **I**zolacja/**B**ezpieczeństwo/**W**ieloprogramowość → **P**roces. **W**spółdzielenie/**S**zybkość/**O**bliczenia → **W**ątek.
### Porównanie zbiorcze
| Cecha | Proces | Wątek |
|-----------------|------------------|----------------------|
| Przestrzeń addr | Własna, izolowana| Współdzielona |
| Tworzenie | ~1-10 ms | ~10-100 μs |
| Przełączanie | Wolne (TLB flush)| Szybkie (rejestry) |
| Komunikacja | IPC (pipe, shm) | Współdzielona pamięć |
| Izolacja | Pełna | Brak |
| Awaria | Nie zabija innych | Może zabić cały proces|
| Zastosowanie | Izolacja, bezpieczeństwo | Wydajność, współdzielenie |
### Problemy komunikacji
Procesy mają **izolowane** przestrzenie adresowe — nie mogą bezpośrednio czytać/pisać wzajemnej pamięci. Komunikacja wymaga pośrednictwa jądra OS (IPC). Wątki mają odwrotny problem: współdzielą pamięć, więc komunikacja jest trywialna, ale wymaga synchronizacji.
![Mechanizmy IPC — porównanie wizualny](img/ipc_mechanisms.png)
**Problem 1 — Overhead kopiowania (procesy).** Większość mechanizmów IPC wymaga kopiowania danych: proces A → jądro → proces B (2 kopie!). Przy dużych danych (np. klatki wideo 4K = 24 MB) to kosztowne.
**Problem 2 — Synchronizacja dostępu (wątki).** Wątki komunikują się przez wspólny heap, ale muszą pilnować, by nie pisać jednocześnie w to samo miejsce (race condition).
**Problem 3 — Blokowanie.** IPC może być synchroniczne (blokujące — nadawca czeka aż odbiorca przeczyta) lub asynchroniczne (nieblokujące — nadawca idzie dalej). Wybór wpływa na wydajność i złożoność kodu.
#### Mechanizmy IPC z przykładami
![Mechanizmy IPC — szczegóły: Pipe, Shared Memory, Socket](img/q9_ipc_details.png)
**Pipe (potok anonimowy)** — jednokierunkowy strumień bajtów w pamięci jądra. Tylko między procesem-rodzicem a potomkiem (fork). Klasyczny przykład Unix:
$ ls -la | grep ".txt" | wc -l
Kod C:
int fd[2];
pipe(fd); // tworzy potok: fd[0]=read, fd[1]=write
if (fork() == 0) { // potomek
close(fd[1]); // zamknij pisanie
read(fd[0], buf, n); // czytaj od rodzica
} else { // rodzic
close(fd[0]); // zamknij czytanie
write(fd[1], "hello", 5); // pisz do potomka
}
**Named Pipe (FIFO)** — jak pipe, ale ma nazwę w systemie plików. Niespokrewnione procesy mogą go używać:
$ mkfifo /tmp/moj_potok # stwórz FIFO
$ echo "dane" > /tmp/moj_potok & # proces A pisze
$ cat /tmp/moj_potok # proces B czyta → "dane"
**Shared Memory (pamięć współdzielona)** — najszybszy IPC. OS mapuje ten sam region pamięci fizycznej do obu procesów. Zero kopiowania — oba procesy czytają/piszą bezpośrednio. ALE: wymaga synchronizacji (semafor/mutex).
Kod C (POSIX):
int fd = shm_open("/my_shm", O_CREAT|O_RDWR, 0666);
ftruncate(fd, 4096);
char *ptr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
sprintf(ptr, "dane z procesu A"); // Proces A pisze
// Proces B: shm_open + mmap → czyta ptr → "dane z procesu A"
**Message Queue (kolejka wiadomości)** — strukturalne wiadomości w jądrze. Asynchroniczna: nadawca wrzuca, odbiorca pobiera z kolejki kiedy chce. Typ wiadomości pozwala filtrować.
Kod C:
Proces A: msgsnd(qid, &msg, size, 0) // wyślij wiadomość
Proces B: msgrcv(qid, &msg, size, typ, 0) // odbierz (filtruj typ)
Przewaga nad pipe: wiele nadawców/odbiorców, filtrowanie typów,
wiadomość ma granice (pipe to surowy strumień bajtów).
**Socket** — dwukierunkowa komunikacja, działa lokalnie (Unix domain) i przez sieć (TCP/UDP). Najbardziej uniwersalny mechanizm IPC.
**Signal (sygnał)** — asynchroniczne powiadomienie od jądra/procesu. Przesyła TYLKO numer (nie dane). Użycie: obsługa Ctrl+C (SIGINT), zabijanie procesów (SIGKILL), powiadomienie o zdarzeniu.
kill(pid, SIGUSR1); // wyślij sygnał SIGUSR1 do procesu
$ kill -9 1234 // wyślij SIGKILL (nie do przechwycenia)
signal(SIGINT, handler); // zarejestruj handler dla Ctrl+C
#### Porównanie mechanizmów IPC
![Porównanie mechanizmów IPC — tabela](img/q9_ipc_table.png)
> **Mnemonik KOMUNIKACJI — „PNMSSS" (6 mechanizmów IPC):**
> **P**iotrek **N**ie **M**a **S**iedmiu **S**tarych **S**karpet = **P**ipe, **N**amed pipe, **M**essage queue, **S**hared memory, **S**ocket, **S**ignal.
> Szybkość: **Shared Memory > Pipe ≈ MsgQueue > Socket** (sieciowy najwolniejszy).
> Zapamiętaj: **„Shared = zero kopii"** — najszybszy bo oba procesy piszą do tej samej ramki RAM.
> **Pipe = rura z wodą** (jednokierunkowa), **Socket = telefon** (dwukierunkowy, też przez sieć).
---
### Problemy synchronizacji
Gdy wątki (lub procesy z shared memory) współdzielą dane, pojawiają się 4 fundamentalne problemy:
![Ilustracja zakleszczenia — cykl oczekiwania](img/deadlock_illustration.png)
#### Problem 1 — Wyścig (Race Condition)
Wynik programu zależy od losowej kolejności operacji wątków. Źródło: operacja „czytaj-modyfikuj-zapisz" nie jest atomowa.
![Wyścig — dwa przykłady](img/q9_race_condition.png)
Poprawka — mutex:
lock(mutex);
saldo = saldo + kwota; // sekcja krytyczna
unlock(mutex);
#### Problem 2 — Zakleszczenie (Deadlock)
Dwa lub więcej wątków czekają na siebie nawzajem — żaden nie może kontynuować. System „zamiera".
![Zakleszczenie — scenariusz i cykl oczekiwania](img/q9_deadlock_scenario.png)
**Warunki Coffmana** — 4 warunki konieczne deadlocka (WSZYSTKIE muszą zachodzić):
![Warunki Coffmana — strategie zapobiegania](img/q9_coffman_strategies.png)
#### Problem 3 — Zagłodzenie (Starvation)
Wątek nigdy nie dostaje zasobu, bo inni ciągle go wyprzedzają. Nie jest deadlockiem (inni się wykonują). Przykład: 10 wątków, wątek niskopriorytetowy nigdy nie dostaje CPU bo wysoko priorytetowe ciągle dominują.
#### Problem 4 — Inwersja priorytetów (Priority Inversion)
Wątek wysokopriorytetowy (H) czeka na mutex trzymany przez niskopriorytetowy (L), a średniopriorytetowy (M) blokuje L. Efekt: H czeka na M (mimo wyższego priorytetu!).
![Zagłodzenie i inwersja priorytetów](img/q9_starvation_priority.png)
> **Mnemonik SYNCHRONIZACJI — „WZZI" (4 problemy):**
> **W**szystkie **Z**egarki **Z**atrzymały się **I**naczej = **W**yścig, **Z**akleszczenie, **Z**agłodzenie, **I**nwersja priorytetów.
> Coffman — **„MHNC"**: **M**uszę **H**amować, **N**ie **C**ofam = **M**utual exclusion, **H**old-and-wait, **N**o preemption, **C**ircular wait. Złam JEDEN = brak deadlocka.
> Najłatwiej złamać **Circular Wait** → numeruj mutexy, blokuj ROSNĄCO.
> Wyścig → **mutex**. Zakleszczenie → **porządek zamków**. Zagłodzenie → **aging**. Inwersja → **priority inheritance**.
---
### Klasyczne problemy synchronizacji
![Klasyczne problemy synchronizacji — 3 panele](img/q9_classic_problems.png)
#### Producent-Konsument (Bounded Buffer)
n producentów wrzuca elementy do bufora o ograniczonej pojemności, m konsumentów pobiera. Bufor pełny → producent czeka. Bufor pusty → konsument czeka.
![Producent-konsument z buforem cyklicznym](img/producer_consumer.png)
Rozwiązanie z semaforami:
semaphore mutex = 1; // wyłączny dostęp do bufora
semaphore empty = N; // ile wolnych slotów (początkowo N)
semaphore full = 0; // ile pełnych slotów (początkowo 0)
Producent: Konsument:
P(empty) P(full)
P(mutex) P(mutex)
wstaw(elem) elem = pobierz()
V(mutex) V(mutex)
V(full) V(empty)
UWAGA: kolejność P(empty/full) PRZED P(mutex)!
Odwrotnie (P(mutex) → P(empty)) = DEADLOCK!
#### Czytelnicy-Pisarze (Readers-Writers)
Wielu czytelników może czytać jednocześnie. Pisarz wymaga wyłącznego dostępu (ani czytelnicy, ani inni pisarze).
Rozwiązanie (first readers-writers):
int readers = 0;
mutex rw_mutex; // pisarz LUB pierwszy/ostatni czytelnik
mutex count_mutex; // ochrona zmiennej readers
Czytelnik: Pisarz:
lock(count_mutex) lock(rw_mutex)
readers++ // PISZ (wyłączny)
if (readers == 1) unlock(rw_mutex)
lock(rw_mutex)
unlock(count_mutex)
// CZYTAJ (wielu jednocześnie!)
lock(count_mutex)
readers--
if (readers == 0)
unlock(rw_mutex)
unlock(count_mutex)
Problem: pisarze mogą głodować (readers=0 nigdy nie zachodzi).
Rozwiązanie: fairness — kolejka FIFO czytelników i pisarzy.
#### Ucztujący filozofowie (Dining Philosophers)
5 filozofów siedzi przy okrągłym stole, między każdą parą 1 widelec. Filozofowie myślą lub jedzą. Jedzenie wymaga 2 widelców (lewego i prawego). Naiwne rozwiązanie: każdy bierze lewy → prawy → deadlock (wszyscy trzymają lewy, czekają na prawy).
Rozwiązanie — złamanie cyklu:
Filozofowie 0-3: bierz lewy, potem prawy.
Filozof 4: bierz PRAWY, potem lewy. ← łamie circular wait!
Alternatywa: semafor(4) — max 4 filozofów próbuje jeść naraz
→ jeden widelec zawsze wolny → brak deadlocka.
> **Mnemonik KLASYCZNYCH PROBLEMÓW — „PCF":**
> **P**yszne **C**iastka **F**ilozofów = **P**roducent-konsument, **C**zytelnicy-pisarze, **F**ilozofowie.
> Producent-konsument: „P(empty) PRZED P(mutex)" — inaczej deadlock!
> Czytelnicy-pisarze: „wielu czyta, jeden pisze" — pisarze mogą głodować.
> Filozofowie: „jeden bierze odwrotnie" — łamie circular wait.
---
### Mechanizmy synchronizacji — porównanie
![Mechanizmy synchronizacji — porównanie i mutex vs semafor vs spinlock](img/q9_sync_comparison.png)
> **Mnemonik MECHANIZMÓW — „MSMCSBR":**
> **M**ała **S**owa **M**oże **C**zasem **S**pinać na **B**ardzo **R**ówno = **M**utex, **S**emafor, **M**onitor, **C**ond.Variable, **S**pinlock, **B**arrier, **R**W Lock.
> Reguła kciuka: sekcja **> 1 μs → MUTEX** (wątek zasypia). Sekcja **< 1 μs → SPINLOCK** (kręci się). **n wątków naraz → SEMAFOR(n)**.
> Mutex = klucz do łazienki (1 osoba). Semafor(3) = parking na 3 miejsca. Spinlock = obrotowe drzwi.
---
### 🎮 Mostek do pracy magisterskiej — wątki w silnikach gier
> Praca magisterska: „Porównanie wydajności i możliwości współczesnych silników gier komputerowych" — Unity vs Unreal Engine. Temat wątków jest **kluczowy** dla game engine performance.
![Wątki w silniku gier — Game Loop Architecture](img/q9_game_engine_threads.png)
#### Game Loop = główny wątek silnika
Każdy silnik gier to **pętla główna (game loop)** działająca w jednym procesie z wieloma wątkami:
while (gameIsRunning) {
ProcessInput(); // ← Main Thread (sekwencyjne)
UpdateGameLogic(); // ← Main Thread
PhysicsStep(); // ← Physics Thread (równoległe)
RenderFrame(); // ← Render Thread (równoległe)
PresentFrame(); // ← GPU sync (blokujące!)
}
#### Unity vs Unreal — porównanie wielowątkowości
| Aspekt | Unity | Unreal Engine |
|--------|-------|---------------|
| Język | C# (managed) | C++ (native) |
| Main thread | MonoBehaviour.Update() | AActor::Tick() |
| Physics thread | PhysX (osobny) | PhysX (osobny) |
| Render thread | Ukryty, submit GPU cmd | RenderThread (jawny) |
| Worker pool | Job System (DOTS/Burst) | TaskGraph |
| Problem | **GC pauses** zamrażają main thread! | Brak GC — deterministyczny timing |
| Object pooling | BulletPool.cs eliminuje GC spikes | FPooledObject |
#### Przykład z pracy — BulletPool (Object Pooling)
W grze bullet-hell (1000+ pocisków) **bez poolingu**:
- `Instantiate()` → alokacja na managed heap → GC musi zbierać → **spike w frame time**
- Nsight Graphics pokazuje: frame time skacze z 8ms na 25ms podczas GC sweep
**Z poolingiem** (`BulletPool.cs`):
- Pre-alokacja 2000 obiektów na starcie
- `pool.Get()` / `pool.Return()`**zero alokacji w runtime**
- Frame time stabilny: 8ms ± 0.5ms
#### Problemy synchronizacji w silnikach gier — konkretne przykłady
| Problem (z pytania) | Jak występuje w game engine |
|---------------------|------------------------------|
| **Race condition** | Dwa wątki modyfikują pozycję tego samego obiektu (AI + Physics) |
| **Deadlock** | Render Thread czeka na dane z Main Thread, który czeka na GPU fence |
| **Zagłodzenie** | Render Thread ma wyższy priorytet → Audio Thread nie dostaje CPU |
| **Inwersja priorytetów** | Low-pri asset loading blokuje mutex potrzebny High-pri render |
#### Mnemonik — „GRAJ WĄTKAMI"
**G**ame Loop (main thread) → **R**ender Thread → **A**udio Thread → **J**ob System (workers)
Każda litera = osobny wątek. Game Loop orkiestruje resztę.
Kiedy na obronie padnie pytanie o wątki → od razu powiedz:
*„W mojej pracy porównuję Unity Job System (C#/Burst) z Unreal TaskGraph (C++). Kluczowa różnica: Unity GC może zatrzymać main thread, Unreal ma deterministyczne czasy."*
---
### Etymologia
**Proces** — łac. „processus" = posuwanie się naprzód. **Wątek (Thread)** — metafora nitki wykonania (jak nić Ariadny). **Mutex** — portmanteau MUTual EXclusion. **Semafor** — Dijkstra (1965); od semaforów kolejowych; P() = hol. „proberen" (próbować), V() = hol. „verhogen" (podnosić). **Coffman** — Edward Coffman Jr. et al. (1971): 4 warunki konieczne deadlocka. **Deadlock (zakleszczenie)** — jak zablokowane koła zębate. **IPC** — Inter-Process Communication.
### Jak zapamiętać
- **„Proces = mieszkanie, Wątek = pokój"** — każde mieszkanie ma adres (przestrzeń), pokoje dzielą kuchnię (heap)
- Wątki szybsze bo nie trzeba zmieniać „mieszkania" (TLB flush)
- **4 warunki Coffmana** zakleszczenia: złam jeden → brak deadlocka
- **„100, 10, 1000"** — tworzenie 100× szybsze, przełączanie 10×, komunikacja 1000×
- **PNMSSS** — 6 mechanizmów IPC (Pipe, Named pipe, Msg queue, Shared mem, Socket, Signal)
- **WZZI** — 4 problemy synchronizacji (Wyścig, Zakleszczenie, Zagłodzenie, Inwersja)
- **MHNC** — 4 warunki Coffmana (Mutual excl., Hold&wait, No preemption, Circular wait)
- **PCF** — 3 klasyczne problemy (Producent-konsument, Czytelnicy-pisarze, Filozofowie)
- **SRP** — prywatne części wątku (Stos, Rejestry, PC)
- **IBW → Proces, WSO → Wątek** — kiedy co stosować
\newpage