27 KiB
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.
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ół
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.
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.
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ą.
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.
Warunki Coffmana — 4 warunki konieczne deadlocka (wszystkie muszą zachodzić jednocześnie):
- Mutual exclusion — zasób jest wyłączny (tylko jeden wątek)
- Hold and wait — trzymaj zasób, czekaj na kolejny
- No preemption — nie można zabrać zasobu siłą
- 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).
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.
Konkretny przykład — 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.
Przełączenie kontekstu = zapisanie PCB starego procesu → wczytanie PCB nowego.
Filar 3 — Stany procesu:
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.
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 | ~1–10 ms | ~10–100 μs (100×) | Proces: nowa przestrzeń adresowa, tablice stron, kopiowanie struktur jądra. Wątek: tylko nowy stos + wpis w schedulerze. |
| Przełączanie | ~1000–5000 ns | ~100–500 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. |
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
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.
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
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
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:
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.
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".
Warunki Coffmana — 4 warunki konieczne deadlocka (WSZYSTKIE muszą zachodzić):
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!).
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
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.
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
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.
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






















