praca_magisterska/pytania/questions/pytanie_09.md

27 KiB
Raw Blame History

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


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

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

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ść


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

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

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

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

Konkretny przykład — mapowanie kodu C na segmenty pamięci:

Mapowanie kodu C na segmenty pamięci

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

Przełączenie kontekstu = zapisanie PCB starego procesu → wczytanie PCB nowego.

Filar 3 — Stany procesu:

Stany procesu — diagram przejść z opisami

Scheduler decyduje, który READY staje się RUNNING.

Mnemonik BUDOWY PROCESU — „3 filary: MPS": Mieszkanie, Paszport, Status = Memory (5 segmentów), PCB (dowód), Stany (5 stanów).

Segmenty od dołu — „TDBHS": Tata Daje Babci Herbatę ze Smietanką = TEXT → DATA → BSS → HEAP → STACK.

Stany — „NRRBT": Nigdy Rano Rybki Biegać nie Trafią = 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

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"Stos, Rejestry, PC = to co PRYWATNE. Skrót odwrotny: „KDHP"Kod, Dane, Heap, Pliki = 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

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

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" = Izolacja/Bezpieczeństwo/Wieloprogramowość → Proces. Współdzielenie/Szybkość/Obliczenia → 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

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

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

Mnemonik KOMUNIKACJI — „PNMSSS" (6 mechanizmów IPC): Piotrek Nie Ma Siedmiu Starych Skarpet = Pipe, Named pipe, Message queue, Shared memory, Socket, Signal. 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

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

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

Warunki Coffmana — 4 warunki konieczne deadlocka (WSZYSTKIE muszą zachodzić):

Warunki Coffmana — strategie zapobiegania

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

Mnemonik SYNCHRONIZACJI — „WZZI" (4 problemy): Wszystkie Zegarki Zatrzymały się Inaczej = Wyścig, Zakleszczenie, Zagłodzenie, Inwersja priorytetów. Coffman — „MHNC": Muszę Hamować, Nie Cofam = Mutual exclusion, Hold-and-wait, No preemption, Circular 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

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

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": Pyszne Ciastka Filozofów = Producent-konsument, Czytelnicy-pisarze, Filozofowie. 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

Mnemonik MECHANIZMÓW — „MSMCSBR": Mała Sowa Może Czasem Spinać na Bardzo Równo = Mutex, Semafor, Monitor, Cond.Variable, Spinlock, Barrier, RW 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

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"

Game Loop (main thread) → Render Thread → Audio Thread → Job 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