Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
statnice:bakalar:b4b36pdv [2025/06/04 21:06] – zapleka3 | statnice:bakalar:b4b36pdv [2025/06/04 22:52] (current) – [Logické hodiny a kauzalita] zapleka3 | ||
---|---|---|---|
Line 18: | Line 18: | ||
- **Vzájemné vyloučení procesů v DS** – algoritmy pro vyloučení procesů a jejich vlastnosti. | - **Vzájemné vyloučení procesů v DS** – algoritmy pro vyloučení procesů a jejich vlastnosti. | ||
- **Volba lídra v DS** – algoritmy pro volbu lídra a jejich vlastnosti. | - **Volba lídra v DS** – algoritmy pro volbu lídra a jejich vlastnosti. | ||
+ | |||
+ | ===== Paralelní systémy/ | ||
===== 1. Hardwarová podpora pro paralelní výpočty ===== | ===== 1. Hardwarová podpora pro paralelní výpočty ===== | ||
Line 78: | Line 80: | ||
==== GPGPU ==== | ==== GPGPU ==== | ||
- | * **General-Purpose computing on Graphics Processing Units** (GPGPU) využívá původně grafické karty pro obecné výpočty. | + | |
- | + | * GPU obsahují tisíce jednoduchých jader sdružených do **streaming multiprocessorů (SM)**. | |
- | * GPU obsahují tisíce jednoduchých jader sdružených do **streaming multiprocessorů (SM)**. | + | |
- | Ty spouštějí stovky až tisíce vláken najednou ve skupinách (warp – 32 vláken běžících lock-step). | + | |
=== Architektura a omezení === | === Architektura a omezení === | ||
- | |||
* **Výpočetní hierarchie: | * **Výpočetní hierarchie: | ||
* **Sdílené prostředky: | * **Sdílené prostředky: | ||
Line 90: | Line 90: | ||
* max 64 warpů / SM (až 2048 vláken) | * max 64 warpů / SM (až 2048 vláken) | ||
* ~255 registrů na vlákno – méně registrů = vyšší obsazenost | * ~255 registrů na vlákno – méně registrů = vyšší obsazenost | ||
- | |||
* **Paměťová hierarchie: | * **Paměťová hierarchie: | ||
* L1/shared: ~33 cyklů | * L1/shared: ~33 cyklů | ||
Line 128: | Line 127: | ||
==== Vlákna a procesy ==== | ==== Vlákna a procesy ==== | ||
- | * **Proces** je samostatná jednotka běhu programu, má vlastní adresní prostor (paměť), popisovače souborů atd. | + | |
- | * Spuštění nového procesu je relativně nákladné. | + | * Spuštění nového procesu je relativně nákladné. |
- | * Vhodné pro více zcela oddělených výpočetních úloh (např. mikroservisy, | + | * Vhodné pro více zcela oddělených výpočetních úloh (např. mikroservisy, |
- | + | * **Vlákno (thread)** je lehčí jednotka běhu uvnitř procesu. | |
- | * **Vlákno (thread)** je lehčí jednotka běhu uvnitř procesu. | + | * Sdílí paměť a další prostředky s ostatními vlákny procesu. |
- | * Sdílí paměť a další prostředky s ostatními vlákny procesu. | + | * Přepínání mezi vlákny je levnější než mezi procesy. |
- | * Přepínání mezi vlákny je levnější než mezi procesy. | + | * Vhodné pro paralelizaci uvnitř jedné aplikace – více úloh najednou (např. UI, I/O, výpočty). |
- | * Vhodné pro paralelizaci uvnitř jedné aplikace – více úloh najednou (např. UI, I/O, výpočty). | + | * Pozor na **synchronizaci a souběhy (race conditions)** – nutnost použití zámků, mutexů apod. |
- | * Pozor na **synchronizaci a souběhy (race conditions)** – nutnost použití zámků, mutexů apod. | + | |
==== Hierarchie cache pamětí ==== | ==== Hierarchie cache pamětí ==== | ||
Moderní CPU mají vícestupňovou **hierarchii cache**, která zajišťuje rychlý přístup k často používaným datům: | Moderní CPU mají vícestupňovou **hierarchii cache**, která zajišťuje rychlý přístup k často používaným datům: | ||
- | |||
* **L1 cache** – nejmenší (např. 32 KB), ale nejrychlejší (~4 cykly), zvlášť pro instrukce a data. | * **L1 cache** – nejmenší (např. 32 KB), ale nejrychlejší (~4 cykly), zvlášť pro instrukce a data. | ||
* **L2 cache** – větší (např. 256 KB), pomalejší (~12 cyklů), většinou sdílena mezi jádry. | * **L2 cache** – větší (např. 256 KB), pomalejší (~12 cyklů), většinou sdílena mezi jádry. | ||
Line 148: | Line 145: | ||
Efektivní využití cache (např. přístup po řádcích, nikoliv po sloupcích v 2D poli) může mít zásadní vliv na výkon. | Efektivní využití cache (např. přístup po řádcích, nikoliv po sloupcích v 2D poli) může mít zásadní vliv na výkon. | ||
- | |||
- | |||
===== 2. Komplikace v paralelním programování ===== | ===== 2. Komplikace v paralelním programování ===== | ||
Line 155: | Line 150: | ||
==== Race Condition (souběh) ==== | ==== Race Condition (souběh) ==== | ||
- | * **Souběh (race condition)** nastává, když dvě nebo více vláken přistupuje ke **stejným datům** a **alespoň jedno z nich je mění**, přičemž jejich pořadí není řízené (např. zámky, mutexy). | + | |
- | + | * To znamená, že výsledek programu **závisí na načasování** – někdy to „vyjde dobře“, jindy ne. | |
- | * To znamená, že výsledek programu **závisí na načasování** – někdy to „vyjde dobře“, jindy ne. | + | * Typickým příkladem je inkrementace sdílené proměnné – pokud není chráněná zámkem, může být výsledek menší, než kolik se očekává. |
- | * Typickým příkladem je inkrementace sdílené proměnné – pokud není chráněná zámkem, může být výsledek menší, než kolik se očekává. | + | |
Například: | Například: | ||
Line 168: | Line 162: | ||
</ | </ | ||
- | * Řešení: | + | |
- | * Použít **synchronizační primitiva** – mutexy, atomické operace, semafory apod. | + | * Použít **synchronizační primitiva** – mutexy, atomické operace, semafory apod. |
- | * Nebo použít **thread-local** kopie a sloučit je až nakonec. | + | * Nebo použít **thread-local** kopie a sloučit je až nakonec. |
==== Deadlock (uváznutí) ==== | ==== Deadlock (uváznutí) ==== | ||
- | * **Uváznutí (deadlock)** nastává, když dvě nebo více vláken čeká na zdroj, který drží to druhé – a žádné se nepohne dál. | + | |
- | + | * Jednoduše řečeno: vlákno A čeká na zámek, který drží vlákno B, a vlákno B čeká na zámek, který drží vlákno A. | |
- | * Jednoduše řečeno: vlákno A čeká na zámek, který drží vlákno B, a vlákno B čeká na zámek, který drží vlákno A. | + | |
Například: | Například: | ||
Line 189: | Line 182: | ||
</ | </ | ||
- | * Aby deadlock mohl nastat, musí být splněny tyto 4 podmínky (tzv. Coffmanovy podmínky): | + | |
- | * **Vzájemné vyloučení** – zdroje nejsou sdílené (jen jedno vlákno je může mít). | + | * **Vzájemné vyloučení** – zdroje nejsou sdílené (jen jedno vlákno je může mít). |
- | * **Zadržení a čekání** – vlákno drží jeden zámek a čeká na další. | + | * **Zadržení a čekání** – vlákno drží jeden zámek a čeká na další. |
- | * **Neodnímatelnost** – zdroj (zámek) nemůže být násilně odebrán. | + | * **Neodnímatelnost** – zdroj (zámek) nemůže být násilně odebrán. |
- | * **Cyklické čekání** – existuje cyklus závislostí mezi vlákny. | + | * **Cyklické čekání** – existuje cyklus závislostí mezi vlákny. |
- | + | * Prevence: | |
- | * Prevence: | + | * Dodržovat **pevné pořadí zamykání zdrojů**. |
- | * Dodržovat **pevné pořadí zamykání zdrojů**. | + | * Používat **timeouty** u zámků. |
- | * Používat **timeouty** u zámků. | + | * Využít algoritmy pro **deadlock detection** a recovery. |
- | * Využít algoritmy pro **deadlock detection** a recovery. | + | |
==== False Sharing ==== | ==== False Sharing ==== | ||
Nastává, když dvě vlákna přistupují k **různým** proměnným, | Nastává, když dvě vlákna přistupují k **různým** proměnným, | ||
- | + | | |
- | * Když jedno vlákno zapíše do své proměnné, CPU invaliduje celou cache line – i když druhé vlákno pracuje na jiné proměnné ve stejné linii. | + | * Pokud se to děje často, dochází k **velkému zpomalení**, |
- | * Pokud se to děje často, dochází k **velkému zpomalení**, | + | * Řešení: |
- | + | * **Zarovnat proměnné** tak, aby každá byla na vlastní cache line (např. pomocí `alignas(64)`). | |
- | * Řešení: | + | * Přidat „padding“ mezi proměnné, které používají různá vlákna. |
- | * **Zarovnat proměnné** tak, aby každá byla na vlastní cache line (např. pomocí `alignas(64)`). | + | |
- | * Přidat „padding“ mezi proměnné, které používají různá vlákna. | + | |
==== True Sharing (není komplikace) ==== | ==== True Sharing (není komplikace) ==== | ||
Nastává, když dvě vlákna přistupují ke **stejné** proměnné. | Nastává, když dvě vlákna přistupují ke **stejné** proměnné. | ||
- | + | | |
- | * Narozdíl od false sharing je to **žádoucí chování**, | + | * Typicky jde o sdílené čítače, fronty, stavové proměnné atd. |
- | * Typicky jde o sdílené čítače, fronty, stavové proměnné atd. | + | |
===== 3. Podpora paralelního programování v C a C++ ===== | ===== 3. Podpora paralelního programování v C a C++ ===== | ||
- | Existuje více úrovní podpory – od nízkoúrovňových knihoven jako `pthreads` v C, až po elegantní moderní rozhraní ve stylu `std:: | + | Existuje více úrovní podpory – od nízkoúrovňových knihoven jako `pthreads` v C, až po elegantní moderní rozhraní ve stylu `std:: |
==== POSIX vlákna (pthreads) ==== | ==== POSIX vlákna (pthreads) ==== | ||
- | * `pthreads` (POSIX Threads) je **standardní rozhraní v jazyku C** pro práci s vlákny. | + | |
- | * Poskytuje funkce pro: | + | * Poskytuje funkce pro: |
- | * **vytváření vláken**: `pthread_create()` | + | * **vytváření vláken**: `pthread_create()` |
- | * **synchronizaci vláken**: | + | * **synchronizaci vláken**: |
- | * **mutexy**: `pthread_mutex_lock()`, | + | * **mutexy**: `pthread_mutex_lock()`, |
- | * **podmínkové proměnné**: | + | * **podmínkové proměnné**: |
- | * **semafory**: | + | * **semafory**: |
Je to výkonný, ale nízkoúrovňový nástroj – programátor musí vše spravovat ručně. Hodí se pro systémy, kde je důležitá kontrola nad výkonem a kompatibilita se standardem POSIX. | Je to výkonný, ale nízkoúrovňový nástroj – programátor musí vše spravovat ručně. Hodí se pro systémy, kde je důležitá kontrola nad výkonem a kompatibilita se standardem POSIX. | ||
==== std::thread (C++11) ==== | ==== std::thread (C++11) ==== | ||
- | * Od C++11 je k dispozici standardní knihovna pro vlákna – `std:: | + | |
- | * Umožňuje jednoduché vytvoření a správu vláken pomocí objektově orientovaného rozhraní. | + | * Umožňuje jednoduché vytvoření a správu vláken pomocí objektově orientovaného rozhraní. |
- | * Vlákno spustíme předáním funkce (nebo lambdy) do konstruktoru. | + | * Vlákno spustíme předáním funkce (nebo lambdy) do konstruktoru. |
- | * Synchronizace se řeší pomocí `std:: | + | * Synchronizace se řeší pomocí `std:: |
Výhodou oproti `pthreads` je **vyšší přehlednost, | Výhodou oproti `pthreads` je **vyšší přehlednost, | ||
==== std:: | ==== std:: | ||
- | * Od C++20 přibyla nová třída `std:: | + | |
- | * Při zničení objektu se vlákno **automaticky ukončí** (`join()` nebo `detach()`). | + | * Při zničení objektu se vlákno **automaticky ukončí** (`join()` nebo `detach()`). |
- | * Má vestavěnou podporu pro **zrušení vlákna** pomocí `stop_token`. | + | * Má vestavěnou podporu pro **zrušení vlákna** pomocí `stop_token`. |
- | * Výborně se hodí pro práci s **RAII přístupem** a bezpečnější práci s vláknem. | + | * Výborně se hodí pro práci s **RAII přístupem** a bezpečnější práci s vláknem. |
To znamená, že se snižuje riziko zapomenutého `join()` a tím i potencionálních chyb. | To znamená, že se snižuje riziko zapomenutého `join()` a tím i potencionálních chyb. | ||
Line 250: | Line 238: | ||
==== Mutex a Lock Guard ==== | ==== Mutex a Lock Guard ==== | ||
**Mutex** (*mutual exclusion*) je synchronizační nástroj, který **zajišťuje, | **Mutex** (*mutual exclusion*) je synchronizační nástroj, který **zajišťuje, | ||
- | + | | |
- | * V C++ se používá `std:: | + | * `std:: |
- | * `std:: | + | |
Ukázka (ruční zamykání/ | Ukázka (ruční zamykání/ | ||
Line 308: | Line 295: | ||
==== Atomic ==== | ==== Atomic ==== | ||
**Atomic** proměnné umožňují provádět operace, které jsou **nedělitelné (atomické)** – tím pádem **bezpečné vůči souběhu**, aniž by bylo potřeba používat mutex. | **Atomic** proměnné umožňují provádět operace, které jsou **nedělitelné (atomické)** – tím pádem **bezpečné vůči souběhu**, aniž by bylo potřeba používat mutex. | ||
- | + | | |
- | * V C++ se používá `std:: | + | * V pozadí to využívá **atomické instrukce CPU** (pokud jsou dostupné), takže jsou **rychlejší než mutexy**. |
- | + | * Pokud HW atomické instrukce neumí, fallback je přes mutex. | |
- | | + | |
- | * Pokud HW atomické instrukce neumí, fallback je přes mutex. | + | |
V praxi jsou tedy `std:: | V praxi jsou tedy `std:: | ||
Line 342: | Line 327: | ||
</ | </ | ||
- | * Jinými slovy – `std:: | + | |
===== 4. Podpora paralelního programování v OpenMP ===== | ===== 4. Podpora paralelního programování v OpenMP ===== | ||
Line 350: | Line 334: | ||
==== Sériově-paralelní model uspořádání vláken (fork-join) ==== | ==== Sériově-paralelní model uspořádání vláken (fork-join) ==== | ||
Fork-join model je základní exekuční schéma OpenMP. Program začíná jedním *master* vláknem, které po vstupu do direktivy **`parallel`** vytvoří tým dalších vláken (*fork*). Všechna vlákna spolupracují v daném paralelním regionu a na jeho konci se implicitní bariérou zase spojí do jediného vlákna (*join*). | Fork-join model je základní exekuční schéma OpenMP. Program začíná jedním *master* vláknem, které po vstupu do direktivy **`parallel`** vytvoří tým dalších vláken (*fork*). Všechna vlákna spolupracují v daném paralelním regionu a na jeho konci se implicitní bariérou zase spojí do jediného vlákna (*join*). | ||
- | + | | |
- | * Výhody: | + | * jednoduchá správa vláken |
- | * jednoduchá správa vláken | + | * sdílená paměť |
- | * sdílená paměť | + | * předvídatelné chování a synchronizace |
- | * předvídatelné chování a synchronizace | + | |
Ukázkový kód: | Ukázkový kód: | ||
Line 372: | Line 355: | ||
</ | </ | ||
- | * Poznámka: OpenMP vytváří **vlákna**, | + | |
Další možnosti a doplňky modelu: | Další možnosti a doplňky modelu: | ||
Line 385: | Line 368: | ||
==== Paralelizovatelná úloha (task region) ==== | ==== Paralelizovatelná úloha (task region) ==== | ||
Pomocí direktivy `task` lze definovat úlohy, které mohou být nezávisle spouštěny paralelně. Oproti `parallel for`, které rozděluje smyčku, `task` umožňuje paralelizovat **libovolné části kódu**, včetně rekurze a nepravidelné struktury výpočtu. | Pomocí direktivy `task` lze definovat úlohy, které mohou být nezávisle spouštěny paralelně. Oproti `parallel for`, které rozděluje smyčku, `task` umožňuje paralelizovat **libovolné části kódu**, včetně rekurze a nepravidelné struktury výpočtu. | ||
- | + | | |
- | * Tasky lze organizovat do `taskgroup`. | + | * Pomocí `depend(in|out|inout: |
- | * Pomocí `depend(in|out|inout: | + | * `detach`, `priority` – umožňují pokročilou kontrolu provádění. |
- | * `detach`, `priority` – umožňují pokročilou kontrolu provádění. | + | |
Příklad: | Příklad: | ||
Line 404: | Line 386: | ||
OpenMP je pouze specifikace – jednotlivé kompilátory ji implementují v různé míře. | OpenMP je pouze specifikace – jednotlivé kompilátory ji implementují v různé míře. | ||
- | *Přehled nejznámějších implementací (stav 2025):* | + | Přehled nejznámějších implementací (stav 2025):* |
* **GCC 14+ (libgomp)** – `-fopenmp`, verze 5.2 (téměř úplná), open-source klasika. | * **GCC 14+ (libgomp)** – `-fopenmp`, verze 5.2 (téměř úplná), open-source klasika. | ||
* **Clang 18+ (libomp)** – `-fopenmp`, verze 5.2 (většina), | * **Clang 18+ (libomp)** – `-fopenmp`, verze 5.2 (většina), | ||
Line 414: | Line 395: | ||
==== Přehled hlavních direktiv OpenMP ==== | ==== Přehled hlavních direktiv OpenMP ==== | ||
- | * **`#pragma omp parallel`** | + | **`#pragma omp parallel`** |
* Vytvoří tým vláken. | * Vytvoří tým vláken. | ||
* Podporuje: `num_threads`, | * Podporuje: `num_threads`, | ||
- | + | **`for` / `do`** | |
- | * **`for` / `do`** | + | |
* Paralelizace smyčky. | * Paralelizace smyčky. | ||
* Možnosti: `schedule(static|dynamic|guided)`, | * Možnosti: `schedule(static|dynamic|guided)`, | ||
- | + | **`sections` / `section`** | |
- | * **`sections` / `section`** | + | |
* Spustí nezávislé bloky kódu paralelně (např. různé funkce najednou). | * Spustí nezávislé bloky kódu paralelně (např. různé funkce najednou). | ||
- | + | **`task`** | |
- | * **`task`** | + | |
* Vytvoří úlohu; možnost specifikace závislostí přes `depend(...)`. | * Vytvoří úlohu; možnost specifikace závislostí přes `depend(...)`. | ||
* Další klauzule: `priority`, `detach`. | * Další klauzule: `priority`, `detach`. | ||
- | + | **`barrier`** | |
- | * **`barrier`** | + | |
* Explicitní synchronizační bod – všechna vlákna musí dojít na toto místo. | * Explicitní synchronizační bod – všechna vlákna musí dojít na toto místo. | ||
- | + | **`critical [(name)]`** | |
- | * **`critical [(name)]`** | + | |
* Zamezí více vláknům vstup do stejné sekce kódu najednou. | * Zamezí více vláknům vstup do stejné sekce kódu najednou. | ||
* Jméno odděluje různé nezávislé sekce. | * Jméno odděluje různé nezávislé sekce. | ||
- | + | **`atomic`** | |
- | * **`atomic`** | + | |
* Odlehčená synchronizace pro jednoduché operace s proměnnou (inkrementace, | * Odlehčená synchronizace pro jednoduché operace s proměnnou (inkrementace, | ||
Line 475: | Line 450: | ||
==== Statické × dynamické rozdělení práce ==== | ==== Statické × dynamické rozdělení práce ==== | ||
- | * **Statické rozdělení** – práce (např. iterace smyčky) se rozdělí předem mezi všechna vlákna. | + | |
- | * Např. pomocí `schedule(static[, | + | * Např. pomocí `schedule(static[, |
- | * Výhodou je **nízký overhead** (prakticky žádná synchronizace). | + | * Výhodou je **nízký overhead** (prakticky žádná synchronizace). |
- | * Nevýhodou je, že **u nepravidelných úloh může být některé vlákno přetížené**, | + | * Nevýhodou je, že **u nepravidelných úloh může být některé vlákno přetížené**, |
- | + | * **Dynamické rozdělení** – práce se přiděluje **za běhu** podle toho, které vlákno je volné. | |
- | * **Dynamické rozdělení** – práce se přiděluje **za běhu** podle toho, které vlákno je volné. | + | * Používá se `schedule(dynamic[, |
- | * Používá se `schedule(dynamic[, | + | * Výhodou je lepší **vyvážení zátěže**. |
- | * Výhodou je lepší **vyvážení zátěže**. | + | * Nevýhodou je **vyšší režie kvůli synchronizaci**. |
- | * Nevýhodou je **vyšší režie kvůli synchronizaci**. | + | * **Guided plánování** – kompromis mezi statickým a dynamickým: |
- | + | * `schedule(guided[, | |
- | * **Guided plánování** – kompromis mezi statickým a dynamickým: | + | * Vhodné pro případy, kdy práce trvá různě dlouho a časem ubývá. |
- | * `schedule(guided[, | + | * Další možnosti: |
- | * Vhodné pro případy, kdy práce trvá různě dlouho a časem ubývá. | + | * `schedule(runtime)` – výběr plánování je ponechán na hodnotě proměnné `OMP_SCHEDULE`. |
- | + | * `schedule(auto)` – nechá výběr plánu na implementaci OpenMP. | |
- | * Další možnosti: | + | |
- | * `schedule(runtime)` – výběr plánování je ponechán na hodnotě proměnné `OMP_SCHEDULE`. | + | |
- | * `schedule(auto)` – nechá výběr plánu na implementaci OpenMP. | + | |
==== Thread-pool a fronta úkolů (work-stealing) ==== | ==== Thread-pool a fronta úkolů (work-stealing) ==== | ||
- | * OpenMP (od verze 3.0) podporuje **tasky**, které jsou plánovány na vlákna z **persistentního thread-poolu**. | + | |
- | * Vlákna jsou vytvořena v prvním paralelním regionu a **zůstávají aktivní** i pro další úseky programu. | + | * Vlákna jsou vytvořena v prvním paralelním regionu a **zůstávají aktivní** i pro další úseky programu. |
- | + | * Každé vlákno má vlastní **lokální deque** (obousměrnou frontu), do které si vkládá nové úkoly. | |
- | * Každé vlákno má vlastní **lokální deque** (obousměrnou frontu), do které si vkládá nové úkoly. | + | * **Work-stealing**: |
- | + | * Tento přístup pomáhá **automaticky vyvažovat zátěž**. | |
- | * **Work-stealing**: | + | * Push/pop na vlastní frontě jsou většinou **lock-free**; |
- | * Tento přístup pomáhá **automaticky vyvažovat zátěž**. | + | |
- | * Push/pop na vlastní frontě jsou většinou **lock-free**; | + | |
==== Balancování zátěže ==== | ==== Balancování zátěže ==== | ||
Vyrovnané rozdělení práce je klíčem k výkonu paralelního programu. | Vyrovnané rozdělení práce je klíčem k výkonu paralelního programu. | ||
- | + | | |
- | * U **smyček**: | + | * Volba správného `schedule(...)` plánu. |
- | * Volba správného `schedule(...)` plánu. | + | * Velikost `chunku` ovlivňuje granularitu přidělené práce. |
- | * Velikost `chunku` ovlivňuje granularitu přidělené práce. | + | * `collapse(n)` – slučuje více vnořených smyček do jedné, čímž zvyšuje počet iterací pro paralelizaci. |
- | * `collapse(n)` – slučuje více vnořených smyček do jedné, čímž zvyšuje počet iterací pro paralelizaci. | + | * U **tasků**: |
- | + | * Work-stealing pomáhá s dynamickým plánováním. | |
- | * U **tasků**: | + | * Pomocí `priority(expr)` lze zvýhodnit důležité úkoly. |
- | * Work-stealing pomáhá s dynamickým plánováním. | + | * Heuristiky runtime mohou **omezit vznik malých tasků** (např. jejich inline-ing) → lepší škálování. |
- | * Pomocí `priority(expr)` lze zvýhodnit důležité úkoly. | + | |
- | * Heuristiky runtime mohou **omezit vznik malých tasků** (např. jejich inline-ing) → lepší škálování. | + | |
==== Závislosti (dependencies) ==== | ==== Závislosti (dependencies) ==== | ||
Tasky mohou mít mezi sebou **závislosti**, | Tasky mohou mít mezi sebou **závislosti**, | ||
- | + | | |
- | * Typy závislostí: | + | * `depend(in: X)` – task **potřebuje** data `X`. |
- | * `depend(in: X)` – task **potřebuje** data `X`. | + | * `depend(out: |
- | * `depend(out: | + | * `depend(inout: |
- | * `depend(inout: | + | * Další pokročilé typy: `mutexinoutset`, |
- | * Další pokročilé typy: `mutexinoutset`, | + | |
Ukázkový kód: | Ukázkový kód: | ||
Line 537: | Line 504: | ||
</ | </ | ||
- | * OpenMP runtime si z těchto závislostí **sestaví DAG (acyklický graf)**. | + | |
- | * Jakmile jsou všechny vstupní závislosti splněny, task je označen jako **„ready“** a zařadí se do fronty. | + | * Jakmile jsou všechny vstupní závislosti splněny, task je označen jako **„ready“** a zařadí se do fronty. |
Tato metoda umožňuje velmi jemné řízení výpočtu a paralelizace i **nepravidelných nebo stromových struktur** (např. kompilátory, | Tato metoda umožňuje velmi jemné řízení výpočtu a paralelizace i **nepravidelných nebo stromových struktur** (např. kompilátory, | ||
- | |||
- | |||
===== 6. Techniky dekompozice programu na příkladech ===== | ===== 6. Techniky dekompozice programu na příkladech ===== | ||
Line 551: | Line 516: | ||
=== Quick sort === | === Quick sort === | ||
- | * **Proč „rozděl a panuj“? | + | |
- | * *Divide*: vybereme pivot a jedním průchodem rozdělíme pole na část < pivot a část ≥ pivot. | + | * *Divide*: vybereme pivot a jedním průchodem rozdělíme pole na část < pivot a část ≥ pivot. |
- | * *Conquer*: obě části rekurzivně řadíme – ideální místo pro vytvoření dvou paralelních úloh (*tasků*). | + | * *Conquer*: obě části rekurzivně řadíme – ideální místo pro vytvoření dvou paralelních úloh (*tasků*). |
- | * *Combine*: není třeba – dělení samo zajistí správné pořadí. | + | * *Combine*: není třeba – dělení samo zajistí správné pořadí. |
- | + | * Paralelizace: | |
- | * Paralelizace: | + | |
<code cpp> | <code cpp> | ||
Line 579: | Line 543: | ||
=== Merge sort === | === Merge sort === | ||
- | * **Proč „rozděl a panuj“? | + | |
- | * *Divide*: rekurzivní dělení pole na půlky. | + | * *Divide*: rekurzivní dělení pole na půlky. |
- | * *Conquer*: každou půlku řadíme paralelně. | + | * *Conquer*: každou půlku řadíme paralelně. |
- | * *Combine*: dvě seřazené části spojíme lineárním `merge`. | + | * *Combine*: dvě seřazené části spojíme lineárním `merge`. |
- | + | * Paralelizace: | |
- | * Paralelizace: | + | |
<code cpp> | <code cpp> | ||
Line 609: | Line 572: | ||
=== Paralelní násobení matice × vektor (SpMV/Dense MV) === | === Paralelní násobení matice × vektor (SpMV/Dense MV) === | ||
- | * **Princip: | + | |
- | * *Divide*: rozdělíme po řádcích. | + | * *Divide*: rozdělíme po řádcích. |
- | * *Conquer*: každý řádek počítá vlákno nezávisle. | + | * *Conquer*: každý řádek počítá vlákno nezávisle. |
- | * *Combine*: nepotřebujeme – každý výstupní `y[i]` je unikátní. | + | * *Combine*: nepotřebujeme – každý výstupní `y[i]` je unikátní. |
- | + | * U husté matice: nejlepší přístup po řádcích (row-major). | |
- | * U husté matice: nejlepší přístup po řádcích (row-major). | + | * U řídké: paralelizace přes nenulové řádky (např. CSR formát). |
- | * U řídké: paralelizace přes nenulové řádky (např. CSR formát). | + | |
<code cpp> | <code cpp> | ||
Line 632: | Line 594: | ||
=== Paralelní násobení dvou matic === | === Paralelní násobení dvou matic === | ||
- | * **Naivní přístup**: | + | |
- | + | ||
- | * **Blokové (tiling) násobení**: | + | * **Blokové (tiling) násobení**: |
- | * *Divide*: rozdělíme výstupní matici $C$ na bloky $B_{pq}$. | + | * *Divide*: rozdělíme výstupní matici $C$ na bloky $B_{pq}$. |
- | * *Conquer*: každý blok počítá vlákno/ | + | * *Conquer*: každý blok počítá vlákno/ |
- | * *Combine*: není třeba – bloky jsou oddělené. | + | * *Combine*: není třeba – bloky jsou oddělené. |
<code cpp> | <code cpp> | ||
Line 647: | Line 609: | ||
</ | </ | ||
- | * Pro vysoký výkon: používat BLAS knihovny (OpenBLAS, MKL…) se SIMD a tilingem. | + | |
=== Paralelní řešení systému lineárních rovnic (Gaussova eliminace) === | === Paralelní řešení systému lineárních rovnic (Gaussova eliminace) === | ||
- | * **Gaussova eliminace (LU)**: v kroku $k$ odstraňujeme hodnoty pod pivotem $A_{kk}$. | + | |
- | * Paralelizujeme řádky $i = k+1..n-1$. | + | * Paralelizujeme řádky $i = k+1..n-1$. |
- | * Nutná synchronizace mezi kroky – `taskwait` nebo sekvenční závislosti. | + | * Nutná synchronizace mezi kroky – `taskwait` nebo sekvenční závislosti. |
<code cpp> | <code cpp> | ||
Line 667: | Line 629: | ||
</ | </ | ||
- | * **Iterativní metody (Jacobi, CG, GMRES)**: | + | |
- | * Lépe paralelizovatelné než Gauss – hlavně `matvec`, vektorové operace, redukce. | + | * Lépe paralelizovatelné než Gauss – hlavně `matvec`, vektorové operace, redukce. |
- | * OpenMP poskytuje `reduction(+: | + | * OpenMP poskytuje `reduction(+: |
- | * **Off-load na GPU (OpenMP 5.x)**: | + | |
- | * Pomocí `target teams distribute parallel for` lze výpočet přenést na GPU. | + | * Pomocí `target teams distribute parallel for` lze výpočet přenést na GPU. |
- | * Host kontroluje synchronizaci, | + | * Host kontroluje synchronizaci, |
- | ===== Úvod do distribuovaných systémů (DS) ==== | ||
- | **charakteristiky DS, čas a typy selhání v DS** | ||
- | DS je soubor nezávislých, | ||
- | Výpočetní jednotky nemají sdílenou paměť, nesdílejí globální hodiny a selhávají na sobě nezávisle. | + | ===== Distribuované výpočty/systémy ==== |
- | Kadý proces má své lokální hodiny, které | + | ===== 1. Úvod do distribuovaných systémů (DS) ===== |
+ | Distribuované systémy (DS) jsou o tom, jak spolupracují **nezávislé výpočetní jednotky**, které | ||
- | Je obtížné uvažovat o čase nejen kvůli absenci globálních hodin ale obecně nelze dát limit na komunikaci a délku výpočtu. | + | *Distribuovaný systém je*: |
+ | * soustava **autonomních procesů** spojených sítí, | ||
+ | * komunikace probíhá **pouze zprávami** (message passing), | ||
+ | * **žádná sdílená paměť**, **žádné globální hodiny**, | ||
+ | * procesy mohou **selhávat nezávisle**, | ||
+ | * každý proces má **lokální hodiny**, které mohou běžet různou rychlostí. | ||
+ | |||
+ | Kvůli těmto vlastnostem je velmi těžké se v systému spolehlivě orientovat v čase – nemůžeme | ||
+ | |||
+ | ==== Asynchronní × Synchronní DS ==== | ||
+ | Tyto modely popisují „jaký svět“ předpokládáme při návrhu distribuovaného algoritmu – tedy **jaké vlastnosti sítí, hodin a chování procesů** budeme brát jako dané. | ||
- | ==== Asynchronní x Synchronní DS ==== | ||
- | Popisují model světa ve kterém DS existuje. rozdělení synchroní a asynchroní popisuje množinu předpokladů, | ||
=== Synchronní model === | === Synchronní model === | ||
- | * **Známe | + | * Známe |
- | * maximální | + | * zpoždění |
- | * maximální | + | * rychlost výpočtu, |
- | * maximální drift lokálních hodin | + | * odchylku mezi hodinami. |
- | * Algoritmy můžeme | + | * Můžeme |
- | * V praxi čistě synchroní sétě skoro neexistují | + | * Takto předvídatelné sítě se v praxi téměř **nevyskytují** – je to spíš teoretický model. |
=== Asynchronní model === | === Asynchronní model === | ||
- | * **Žádné | + | * **Žádné |
- | běžet | + | * Timeouty |
- | * Timeout tedy **nemůže spolehlivě rozlišit** „zpomalení“ od „pádu“. | + | * Například slavný důkaz **FLP** ukazuje, že **v asynchronním modelu není možný deterministický konsensus**, pokud může |
- | * V tomto modelu je např. prokázáno (FLP), že deterministický konsensus | + | |
- | s možností jediného pádu procesu | + | |
- | === Částečně synchronní model (real-world kompromis) === | + | |
- | * Systém se může | + | |
- | * Většina produkčních protokolů (Raft, PBFT …) předpokládá právě tenhle model – jsou robustní vůči výkyvům, ale nakonec se „chytí“. | + | |
- | | + | |
+ | === Částečně synchronní model === | ||
+ | * Realistický kompromis: | ||
+ | * Systém se může nějakou dobu chovat asynchronně, | ||
+ | * Většina reálných protokolů (např. **Raft**, **PBFT**) počítá právě s tímto modelem. | ||
+ | * Výhoda: **odolnost vůči výpadkům i výkyvům**, | ||
- | - Selhání procesu | + | ==== Typy selhání v distribuovaných systémech ==== |
- | * crash / fail-stop - proces přestane vykonávatz algoritmus | + | V DS musíme počítat s tím, že **něco selže** – a často i nepozorovaně. Rozlišujeme: |
- | | + | |
- | - Selhání kanálu | + | |
- | | + | |
- | * partitioning - procesy jsou rozdělené do disjunktních množin, komunikace v nich je možná, ale mezi nimi ne | + | |
- | - Selhání časování - doba odezvy procesu nebo přenosu zprávy vybočila z dohodnutého časového rozmezí | + | |
- | Předpoklady na komunikační kanál: | + | * **Selhání procesu**: |
- | Spolehlivé doručování, | + | * *crash / fail-stop* – proces přestane fungovat (nic už neposílá). |
+ | * *byzantské (libovolné) selhání* – proces dál funguje, ale **chybně** (např. posílá nesmysly, nebo se chová škodlivě). | ||
+ | * **Selhání kanálu**: | ||
+ | * *ztráta zprávy* – zpráva se ztratí v síti. | ||
+ | * *partitioning* – síť se rozdělí na **oddělené oblasti**, které spolu **nemohou komunikovat**. | ||
+ | * **Selhání časování**: | ||
+ | * odezva procesu nebo zprávy překročí **očekávané časové limity** – může jít o zpoždění i výpadek. | ||
- | ===== Detekce selhání v DS ===== | + | ==== Předpoklady na komunikační kanál |
- | **detektory selhání a jejich | + | Distribuované algoritmy často |
- | **úplnost** - každé selhání je časem detekováno alespoň jedním bezvadným procesem | + | |
+ | * **žádné duplikace** – zprávy se nedoručují víckrát, | ||
+ | * **žádné falešné zprávy** – proces nemůže dostat zprávu, kterou nikdo neposlal, | ||
+ | * **zachování pořadí** – zprávy mezi dvěma procesy dorazí ve stejném pořadí, v jakém byly odeslány. | ||
- | **přesnost** - nedochází k mylné detekci | + | Tato vlastnosti se často |
- | nelze dosáhnout obou vlastností současně, je vyžadována | + | ===== 2. Detekce selhání v DS ===== |
+ | V distribuovaných systémech je běžné, že některé procesy selžou – a aby systém mohl dál správně fungovat, ostatní uzly se to musí nějak **včas a spolehlivě dozvědět**. K tomu slouží tzv. **detektory selhání**, | ||
+ | Jenže v distribuovaném prostředí nemáme jistotu, jestli uzel selhal, nebo je jen dočasně pomalý (např. síťová prodleva) – a proto je detekce selhání **zásadně nejistá**. Tato kapitola rozebírá vlastnosti detektorů selhání, různé typy heartbeat protokolů a praktický algoritmus SWIM. | ||
+ | ==== Vlastnosti detektorů selhání ==== | ||
+ | * **Úplnost (completeness)** | ||
+ | * Každé skutečné selhání je **časem detekováno** alespoň jedním bezchybným uzlem. | ||
+ | * **Přesnost (accuracy)** | ||
+ | * Detektor **nemá falešné poplachy** – neoznačí proces za havarovaný, | ||
- | Frekvence selhání roste lineárně s počtem procesů ve skupině (selhání jsou běžná) | + | Nelze zaručit obě vlastnosti zároveň |
- | Dva podprotokoly - detekce selhání a šíření informace o selhání | + | Jinými slovy: **lepší je omylem někoho považovat za mrtvého, než si nevšimnout, |
- | Průběh detekce: | + | * Frekvence selhání v praxi roste **lineárně s počtem uzlů** – tj. v systému se stovkami uzlů je běžné, že některé z nich selžou v každém okamžiku. |
- | - havárie procesu $p_j$ | + | |
- | - detekce selhání procesu $p_j$ nějakým procesem $p_k$ | + | |
- | - proces $p_k$ rozšiřuje informaci o selhání procesu $p_j$ do ostatních procesů | + | |
- | Základní protokoly pro detekci | + | ==== Průběh detekce ==== |
+ | - Proces $p_j$ selže. | ||
+ | - Jiný proces $p_k$ jeho selhání | ||
+ | - Proces $p_k$ **šíří informaci** o selhání $p_j$ dalším uzlům – podle typu protokolu. | ||
+ | |||
+ | ==== Typy detekčních protokolů ==== | ||
=== Centralizovaný heartbeat === | === Centralizovaný heartbeat === | ||
- | Heartbeat jsou odesílány | + | * Každý uzel **periodicky |
+ | * $p_j$ si udržuje čítač a čas posledního přijetí od každého uzlu. | ||
+ | * Pokud nedorazí heartbeat během $\tau$ → $p_i$ je považován za selhaný. | ||
- | Heartbeat má pořadové číslo, to odeslání heartbeatu je inkrementován lokální čítač pro každý proces | + | **Vlastnosti: |
+ | * Jednoduchá implementace. | ||
+ | * **Nezjistí selhání $p_j$ samotného** – není nikdo, kdo by ho hlídal. | ||
+ | * $p_j$ může být **přetížen**, | ||
- | Pokud není heartbeat | + | === Kruhový |
+ | * Každý proces periodicky posílá heartbeat **sousedovi** | ||
+ | * Lze použít **jednosměrný** nebo **obousměrný** kruh. | ||
- | Je úplný pro všechny uzly kromě $p_j$, selhání $p_j$ není detekováno a při velkém počtu uzlů může být $p_j$ přetížen. | + | **Vlastnosti: |
+ | * Není žádný centrální bod → lepší škálování. | ||
+ | * **Není úplný**, pokud selže jeden (v jednosměrném) nebo dva (v obousměrném) uzly. | ||
+ | * Nutno **udržovat kruhovou topologii** – složitější údržba. | ||
+ | === All-to-all heartbeat === | ||
+ | Každý uzel **periodicky odesílá heartbeat všem ostatním**. | ||
- | === Kruhový heartbeat === | + | **Vlastnosti: |
- | Heartbeats jsou odesílány periodicky sousedům | + | * **Vysoká úplnost** – každý je hlídán všemi. |
+ | * Rovnoměrná zátěž mezi uzly. | ||
+ | * **Nízká přesnost** – síťové zpoždění může vést k falešnému označení za mrtvého. | ||
+ | * **Velké zatížení sítě** – škáluje špatně pro stovky uzlů. | ||
- | * není centrální uzel | + | === SWIM protokol === |
- | * není úplné při selhání více procesů (stačí jeden u jednosměrného, | + | **Scalable Weakly-consistent Infection-style Membership** |
- | | + | |
- | === All to all heartbeat === | + | Moderní přístup k detekci, který |
- | každý proces periodicky odesílá heartbeat všem ostatním procesům | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | === SWIM === | + | Každý proces $p_i$ periodicky: |
- | **Scalable weakly consistent infection-style proces group membership protocol** | + | - Posílá `ping` náhodnému uzlu $p_j$ a čeká na `ack`. |
+ | - Pokud žádná odpověď nepřijde, požádá $\mathcal{K}$ jiných uzlů, aby se zeptaly místo něj (`ping_req`). | ||
+ | - Tyto uzly pingnou $p_j$, a pokud odpověď dostanou, pošlou ji zpět $p_i` jako `ping_ack`. | ||
+ | - Pokud nikdo z $\mathcal{K}$ nedostane odpověď → $p_j$ je označen za mrtvého. | ||
- | | + | **Vlastnosti: |
- | * pokud ji nedostance, odešle zprávu | + | * **Přesnost** lze nastavit volbou |
- | * tyto uzly se zeptají $p_j$ pomocí zprávy | + | * **Úplnost** je zaručena. |
- | * pokud ani jeden z $\mathcal{K}$ uzlů nedostane odpověď, označí $p_i$ $p_j$ jako havarovaný | + | * **Průměrný čas detekce**: |
+ | * $\frac{e}{e - 1} \mathcal{T}$ | ||
+ | * průměrná délka detekčního cyklu v SWIM protokolu | ||
+ | * Dobře funguje i ve velkých systémech (1000+ uzlů). | ||
- | přesnost lze nastavit volbou $\mathcal{K}$, | ||
- | úplnost je zaručena, průměrný čas detekce je $\frac{e}{e - 1}\mathcal{T}$ | + | ===== 3. Čas a kauzalita v DS ===== |
+ | V běžném programu máme přesné hodiny, které říkají, kdy se co stalo. V distribuovaném systému ale žádné „společné“ hodiny nemáme – každý uzel má své vlastní a běží jinak rychle. Proto je v DS mnohem důležitější **pořadí a závislosti událostí** než konkrétní | ||
- | ===== Čas a kauzalita | + | ==== Problémy s časem |
- | **uspořádání událostí | + | * **Clock slew** – rozdíl ve skutečném čase mezi dvěma procesy. |
+ | * **Clock drift** – rozdíl | ||
+ | * To znamená, že hodiny **nelze úplně sladit**, jen přiblížit. | ||
- | clock slew - rozdíl v času hodin dvou procesů, mají li dvoje hodiny nenulovou mimoběžnost, jsou nesynchronizované | + | ==== Synchronizace |
+ | Synchronizace může být: | ||
- | clock drift - rozdíl | + | === Externí synchronizace === |
+ | * Každý proces $p_i$ má své hodiny $\mathcal{C}_i$ udržované v rozmezí $\delta$ od nějakého **externího referenčního času** $\mathcal{S}$. | ||
+ | * Např. atomové hodiny, UTC, NTP servery. | ||
+ | * $|\mathcal{C}_i | ||
+ | * Každý uzel se snaží držet | ||
+ | * Typický příklad: **NTP (Network Time Protocol)**. | ||
- | synchronizace | + | === Interní |
- | * externí | + | * Zajišťuje, |
- | | + | * Formálně: |
- | * externí hodiny mohou být napojeny na UTC nebo atomové hodiny | + | * Rozdíl mezi hodinami dvou uzlů je nejvýše $\delta$ |
- | * například NTP | + | * Není vázaná na žádný „reálný |
- | * interní | + | * Např. **Berkeley |
- | * každý pár procesů $(p_i, p_j)$ má hodnoty | + | |
- | * například | + | |
- | Externí synchronizace typu Kolik je hodin? -> odešle odpověď -> nastaví čas, má chybu nastavení špatného času kvůli komunikační prodlevě (latenci) | + | ==== Praktické algoritmy pro synchronizaci ==== |
- | Cristianův algoritmus | + | === Cristianův algoritmus |
- | $$ \mathcal{C}_i \coloneq t + \frac{\mathcal{T}_\text{RT}-l_{\text{min}+l' | + | * Klient se zeptá serveru: *„Kolik |
- | chyba je omezena, tj maximálně $\frac{\mathcal{T}_\text{RT} - l_{\text{min}} - l' | + | * Server odpoví, a klient si upraví svůj čas podle: |
- | lokální čas lze posunou libovolně dopředu, ale ne dozadu, je možné zvýšit nebo snížit rychlost hodin | + | $\mathcal{C}_i := t + \frac{\mathcal{T}_{RT} - l_{\text{min}} - l' |
+ | * RTT mínus odhadnutá minimální latence tam i zpět, děleno dvěma | ||
- | NTP | + | Očekávaná |
- | * servery uspořádány do hierarchie stromu, uzly synchronizují čas se svými rodiči a někdy s dalšími servery na stejné úrovni | + | |
- | | + | |
- | | + | |
- | Logické hodiny | ||
- | nepoužívají | + | Lokální čas lze zvyšovat okamžitě, ale **nelze ho vracet zpět** – místo toho se mění rychlost |
- | kauzální vztah - první událost může ovlivnit druhou | + | === NTP === |
+ | * Servery tvoří **stromovou hierarchii** (stratum 0, 1, 2, …). | ||
+ | * Uzly synchronizují čas s rodiči i některými sousedy. | ||
+ | * Latence se odhaduje pomocí offsetu: | ||
- | Relace stalo se před | + | $o = \frac{(t_1^{r} - t_2^{r} + t_2^{s} - t_1^{s})}{2}$ |
- | - Jsou-li | + | |
- | | + | |
- | | + | |
- | Kauzální nezávislost | + | ==== Logické hodiny a kauzalita ==== |
- | * relace stalo se před zavádí | + | Logické hodiny nejsou o „skutečném čase“, ale o **pořadí událostí**. Nepotřebují žádnou synchronizaci – pouze konzistentní značkování událostí podle „co mohlo ovlivnit co“. |
- | | + | |
- | | + | |
- | Lamportovy logické hodiny - každý proces má své logické hodiny, které | + | === Kauzální vztah – relace „stalo |
+ | Událost $\mathcal{A}$ **mohla ovlivnit** událost $\mathcal{B}$, | ||
- | Synchronizace logických hodin | + | * $\mathcal{A} \rightarrow \mathcal{B}$ – pokud obě nastaly ve stejném procesu a $\mathcal{A}$ byla dřív, |
- | - po každé události která se odehraje v $p_i$ se hodiny | + | |
- | | + | * nebo (tranzitivně): |
- | - kdykoliv proces $p_j$ přijme zprávu $m$, tak: | + | |
- | - upraví své lokální hodiny | + | |
- | - provede krok 1 předtím než předá $m$ aplikaci (<= přijetí zprávy je událost) | + | |
- | Lamportovy hodiny neimplikují kauzalitu! | + | === Kauzální nezávislost === |
- | * Platí: jestliže | + | * Události |
- | * Neplatí: jestliže $\mathcal{C}(e_1) \lt \mathcal{C}(e_2)$, | + | * Jinými slovy: žádná z nich nemohla ovlivnit druhou. |
- | Vektorové | + | === Lamportovy logické |
+ | Každý proces si udržuje | ||
+ | * Při každé lokální události: $\mathcal{C}_i := \mathcal{C}_i + 1$ | ||
+ | * Při odeslání zprávy $m$: zpráva dostane časovou značku $ts(m) := \mathcal{C}_i$ | ||
+ | * Při přijetí zprávy $m$ procesem $p_j$: | ||
- | ===== Globální stav v DS a jeho výpočet ===== | + | Důležité: |
- | **řez distribuovaného výpočtu, algoritmus pro distribuovaný globální snapshot, stabilní vlastnosti DS** | + | * Jestliže $e_1 \rightarrow e_2$, pak $\mathcal{C}(e_1) < \mathcal{C}(e_2)$ |
+ | | ||
- | globální stav je množina lokálních stavů procesů v DS a stavu všech komunikačních kanálů v jednom okamžiku | + | ⇒ Lamportovy hodiny **respektují kauzalitu**, |
- | globální snapshot je záznam globálního stavu | + | === Vektorové hodiny === |
+ | Abychom **přesně zachytili kauzalitu**, | ||
- | například | + | |
- | | + | * Při každé lokální události: $V_i[i] += 1$ |
- | * deadlock detection | + | * Při odeslání zprávy se posílá kopie $V_i$ |
- | * detekce ukončení výpočtu | + | * Při přijetí zprávy $m$ se provede prvek po prvku: $$ V_i[j] := \max(V_i[j], |
- | * checkpointing | + | |
- | Řez - časová hranice v každém procesu a v každém komunikačním kanále | + | Událost $e_1$ předchází $e_2$ **ve vektorovém čase**, pokud: $$ V(e_1)[k] \leq V(e_2)[k] \ \forall k \text{ a alespoň jedna nerovnost je ostrá} $$ |
- | * události které nastanou | + | |
- | | + | |
- | Konzistentní | + | To už umí odhalit **kauzální závislost i nezávislost**. |
- | $$ f \in \mathcal{Ř} \land e \rightarrow f \implies e \in \mathcal{Ř}$$ | + | |
- | tj. pokud řez obsahuje | + | |
+ | ===== 4. Globální stav v DS a jeho výpočet ===== | ||
+ | V distribuovaném systému neexistuje globální hodina ani centrální pohled na „okamžitý stav celého systému“. Přesto někdy potřebujeme **získat konzistentní snímek stavu** všech procesů a kanálů – třeba pro detekci uváznutí, garbage collection nebo checkpointing. Tato kapitola vysvětluje, | ||
+ | |||
+ | ==== Co je globální stav a snapshot ==== | ||
+ | |||
+ | **Globální stav** je: | ||
+ | * množina **lokálních stavů všech procesů** + | ||
+ | * stav **všech komunikačních kanálů** | ||
+ | * … ve stejném (logickém) okamžiku | ||
+ | |||
+ | **Globální snapshot** je **záznam** takového stavu, který může být analyzován mimo běžící systém (např. logicky, offline). | ||
+ | |||
+ | Použití: | ||
+ | * **garbage collection** – rozhodnutí, | ||
+ | * **detekce deadlocku** | ||
+ | * **ukončení výpočtu** | ||
+ | * **checkpointing** – ukládání stavu pro případ obnovení | ||
+ | |||
+ | ==== Řez distribuovaného výpočtu ==== | ||
+ | |||
+ | **Řez (cut)** definuje: | ||
+ | * **časový okamžik** v každém procesu a | ||
+ | * **hranicí** mezi tím, co se „už stalo“, a tím, co „teprve nastane“ | ||
+ | |||
+ | Události, které nastanou **před řezem**, do něj patří; zbytek je mimo. | ||
+ | |||
+ | === Konzistentní řez === | ||
+ | Řez $\mathcal{Ř}$ je **konzistentní**, pokud: $$ f \in \mathcal{Ř} \land e \rightarrow f \implies e \in \mathcal{Ř} $$ | ||
+ | |||
+ | Jinými slovy: **jestliže | ||
+ | | ||
+ | |||
+ | Konzistentní řez je tedy **logický okamžik**, který by mohl být zpozorován „zvenčí“. | ||
- | konzistentní řez = logický okamžik | ||
{{statnice: | {{statnice: | ||
Line 868: | Line 899: | ||
{{statnice: | {{statnice: | ||
- | Chamdy-Lamport algoritmus | + | ==== Algoritmus pro distribuovaný snapshot (Chandy-Lamport) ==== |
- | | + | Chandy-Lamport algoritmus |
- | | + | |
- | - Jeden (libovolný) z procesů iniciuje | + | **Základní princip:** |
- | - Procesy reagují | + | |
- | - výsledkem | + | |
+ | * Algoritmus je **asynchronní** a **neblokující** – každý proces koná na základě přijatých značek. | ||
+ | |||
+ | **Průběh: | ||
+ | - Jeden proces iniciuje snapshot | ||
+ | - Když jiný proces obdrží `ZNAČKA ▪` poprvé: | ||
+ | * uloží svůj stav, | ||
+ | * pošle `ZNAČKA ▪` dál, | ||
+ | * vše, co do té doby přišlo | ||
+ | - Pokud přijde `ZNAČKA ▪` na kanál, u kterého už máme stav procesu, ale ne kanálu → zprávy mezi tím patří do snapshotu. | ||
+ | Výsledkem | ||
{{statnice: | {{statnice: | ||
- | stabilní vlastnost je taková vlastnost, že jakmile je ve výpočtu jednou splněna, zůtává splněna navždy | + | ==== Stabilní vlastnosti ==== |
- | * například výpočet skončil, nastalo uváznutí, objekt je sirotek | + | |
- | | + | **Stabilní |
+ | * Příklady: | ||
+ | * výpočet skončil | ||
+ | * došlo k uváznutí | ||
+ | * objekt je sirotek (už nikdy nebude dosažen) | ||
- | Chandy-Lamport | + | **Nestabilní vlastnost**: |
- | * Je daná stabilní vlastnost | + | * např. „žádný proces neselhal“ – může přestat platit. |
- | - ANO - Vlastnost | + | |
- | | + | === Kontrola stabilní vlastnosti pomocí snapshotu === |
+ | Chandy-Lamport algoritmus lze využít pro **detekci stabilních vlastností**: | ||
+ | |||
+ | Chceme vědět, jestli nějaká | ||
+ | * Pokud **platí v globálním snapshotu**, | ||
+ | * Platila už i ve skutečném systému v okamžiku | ||
+ | * Pokud **neplatí v snapshotu**, | ||
+ | | ||
+ | |||
+ | Snapshot tedy vytváří **spolehlivý časový úsek**, během kterého můžeme ověřit trvalost vybraných vlastností. | ||
===== Vzájemné vyloučení procesů v DS ===== | ===== Vzájemné vyloučení procesů v DS ===== | ||
**algoritmy pro vyloučení procesů a jejich vlastnosti** | **algoritmy pro vyloučení procesů a jejich vlastnosti** | ||
+ | |||
+ | V distribuovaných systémech nemáme sdílenou paměť ani centrální plánovač. Pokud chceme zajistit, že do tzv. **kritické sekce** vstupuje vždy jen jeden proces, potřebujeme speciální algoritmy pro **vzájemné vyloučení**. Tato kapitola popisuje základní problém, požadavky na jeho řešení a několik klíčových algoritmů včetně jejich výhod a nevýhod. | ||
==== Problém vzájemného vyloučení ==== | ==== Problém vzájemného vyloučení ==== | ||
- | < | + | |
- | - *Kritická sekce* je část | + | * V distribuovaném prostředí je nutné |
- | - Definujeme funkce | + | * Algoritmus poskytuje dvě základní operace: |
- | - Na úrovni operačního systému umíme problém řešit pomocí synchronizačních nástrojů, v distribuovaném systému potřebuje algoritmus | + | * `enter()` – požádá o vstup do kritické sekce |
- | </ | + | * `exit()` – opustí kritickou sekci |
==== Požadavky na algoritmus pro vyloučení procesů ==== | ==== Požadavky na algoritmus pro vyloučení procesů ==== | ||
- | < | + | * **Bezpečnost**: |
+ | * **Živost**: | ||
+ | * **Uspořádání (volitelné)**: | ||
+ | * Předpoklady: | ||
+ | * procesy neselhávají | ||
+ | * komunikace je spolehlivá (FIFO, bez ztrát, bez duplicit) | ||
+ | * systém je **asynchronní** s konečnou latencí | ||
- | - *Bezpečnost* - Nejvýše jeden proces v kritické sekci | + | ==== Analýza výkonnosti algoritmů ==== |
- | - *Živost* - Každý požadavek na vstup kritické sekce je časem uspokojen | + | * **Komunikační zátěž** – kolik zpráv |
- | - *Uspořádání* (není nutné) - Předchází-li žádost jednoho procesu do kritické sekce kauzálně žádosti jiného procesu, tento proces by se měl do kritické sekce dostat dříve | + | * **Zpoždění klienta** – čas od požadavku po vstup, pokud nikdo jiný nečeká |
- | - Uvažujeme navíc, že procesy neselhávají a komunikační kanály jsou perfektní (FIFO, zprávy se neduplikují, | + | * **Synchronizační zpoždění** – čas mezi výstupem |
- | </ | + | |
- | + | ||
- | ==== Analýza výkonnosti algoritmů | + | |
- | < | + | |
- | + | ||
- | - *Komunikační zátěž* | + | |
- | - *Zpoždění klienta* | + | |
- | - *Synchronizační zpoždění* | + | |
- | </ | + | |
==== Centralizovaný algoritmus ==== | ==== Centralizovaný algoritmus ==== | ||
- | < | + | * Jeden proces slouží jako **koordinátor** a spravuje **token**. |
+ | * Proces žádá koordinátora o token; po jeho získání vstoupí do kritické sekce. | ||
+ | * Po výstupu token vrací. | ||
+ | * Token je předáván podle fronty požadavků. | ||
- | - Je zvolen jeden koordinátor | + | **Vlastnosti: |
- | - Ten spravuje speciální token, který držiteli umožní vstup do KS a frontu požadavků na vstup do KS | + | |
- | - Před vstupem do KS je poslán požadavek na token koordinátorovi, | + | |
- | - Po výstupu je token vrácen koordinátorovi | + | |
- | - Koordinátor po přijetí požadavku na token předá token pokud jej drží, pokud jej nedrží, tak tento požadavek přidá do fronty | + | |
- | - Ve chvíli kdy koordinátor obdrží token tak zkontroluje frontu zda tam není žádný požadavek a případně jej obslouží | + | * Nevýhoda: **koordinátor je single point of failure**. |
- | - Bezpečnost | + | |
- | - Komunikační zátěž | + | |
- | - Zpoždění klienta | + | |
- | - Synchronizační zpoždění | + | |
- | </ | + | |
==== Kruhový algoritmus ==== | ==== Kruhový algoritmus ==== | ||
- | < | + | * Procesy jsou uspořádány v logickém kruhu. |
+ | * Jeden **token** obíhá dokola. | ||
+ | * Proces vstupuje do kritické sekce až po přijetí tokenu. | ||
+ | * Pokud token nepotřebuje, | ||
- | - N procesů v kruhu | + | **Vlastnosti: |
- | - Proces může poslat zprávu následníkovi | + | |
- | - Mezi procesy koluje jeden token | + | |
- | - Před vstupem proces čeká dokud neobdrží token | + | |
- | - Po vstupu proces pošle token následníkovi | + | * Výhoda: žádný centrální uzel. |
- | - Pokud proces obdrží token a nečeká na vstup, tak jej hned předá dále | + | * Nevýhoda: token může „zabloudit“ nebo se ztratit. |
- | - Komunikační zátěž | + | |
- | - Zpoždění klienta | + | |
- | - Synchronizační zpoždění | + | |
- | </ | + | |
==== Ricart-Agrawalův algoritmus ==== | ==== Ricart-Agrawalův algoritmus ==== | ||
- | < | + | * Nepoužívá token – pracuje pomocí **multicastu |
- | - Nepoužívá token, ale kauzalitu | + | |
- | - Nižší synchronizační zpoždění než kruhový algoritmus a nepoužívá centrální proces | + | |
- | - Každý proces si udržuje | + | |
- | 1. identifikátor kritické sekce, kterou zamyká, | + | * Pro vstup do kritické sekce: |
- | 1. stav: *RELEASED*, *HELD* nebo *WANTED*, a | + | * proces |
- | 1. frontu odložených požadavků. | + | * čeká na odpovědi `OK` od všech |
+ | * Ostatní procesy odpoví podle kauzálního (Lamportova) času | ||
- | Na začátku je stav každého zámku každého procesu nastaven na *RELEASED*. V systému kolují pro každou kritickou sekci dva typy zpráv: `REQUEST` a `OK` | + | <code cpp> |
+ | // schéma odpovědi na REQUEST(K) | ||
+ | if (stav == HELD || (stav == WANTED && (můj_čas < jejich_čas || (můj_čas == jejich_čas && můj_id < jejich_id)))) { | ||
+ | odlož požadavek; | ||
+ | } else { | ||
+ | pošli | ||
+ | } | ||
+ | </ | ||
- | 1. Pokud chce proces `P[i]` požádat o vstup do kritické sekce `K`, zaznamená | + | * Po ukončení práce |
- | 1. Zámek `K` procesu je ve stavu *WANTED* dokud neobdrží zprávu `OK(K)` od každého dalšího přistupujícího procesu. Poté se nastaví | + | * přepne stav na RELEASED |
- | 1. Pokud procesu `P[j]` přijde zpráva `REQUEST(K)` od procesu `P[i]` s časem `T[i]`, tak | + | * pošle |
- | - pokud je zámek `K` ve stavu *HELD*, pak zprávu `REQUEST(K)` zařadí mezi odložené požadavky a neodpoví | + | |
- | - pokud je ve stavu *WANTED* a o vstup do kritické sekce žádal | + | |
- | - jinak pošle zprávu `OK(K)` procesu `P[i]`. | + | |
- | 1. Pokud proces `P[i]` dokončí práci v kritické sekci `K`, nastaví stav zámku `K` na *RELEASED*, odpoví na všechny zprávy ve frontě zámku a frontu vyprázdní. | + | |
- | - V nejhorším případě je nutno čekat než všech $(N-1)$ pošle OK | + | **Vlastnosti: |
- | - Je zachováno pořadí, požadavky s nižší lamportovou značkou mají přednost | + | |
- | - Komunikační zátěž | + | * $2(N-1)$ |
- | - Zpoždění klienta | + | * až $(N-1)$ |
- | - Synchronizační zpoždění | + | |
- | </ | + | |
+ | * Výhoda: **zcela decentralizovaný**, | ||
+ | * Nevýhoda: **vysoký počet zpráv** | ||
===== Volba lídra v DS ===== | ===== Volba lídra v DS ===== | ||
**algoritmy pro volbu lídra a jejich vlastnosti** | **algoritmy pro volbu lídra a jejich vlastnosti** | ||
- | < | + | |
- | - Cílem je, aby se všechny procesy shodly na jednom | + | V distribuovaném systému často potřebujeme zvolit jeden proces, který bude působit jako koordinátor – například pro synchronizaci, |
- | - Předpokládáme možnost selhání (crash) | + | |
- | - Výsledek nesmí záviset na tom, kdo volbu odstartoval, | + | Cílem |
- | </ | + | * všechny |
+ | * nezáleželo na tom, kdo volbu zahájil, | ||
+ | * více paralelních voleb nakonec **konvergovalo k jedné**. | ||
+ | |||
+ | Předpokládáme: | ||
+ | * spolehlivé doručování zpráv (žádné ztráty ani duplicity), | ||
+ | * možnost selhání | ||
+ | * procesy mají unikátní ID (číselné) a dokážou je mezi sebou porovnat. | ||
==== Kruhový algoritmus (Ring) ==== | ==== Kruhový algoritmus (Ring) ==== | ||
- | < | + | * Procesy |
- | - Procesy | + | |
- | - **Start**: | + | * pošle `ELECTION(i)` svému následníkovi. |
- | - **Přijetí | + | * pokud příjemce dostane |
- | - pokud $j > i$: předá | + | |
- | | + | |
- | | + | |
- | - $P_i$ vyšle `ELECTED(i)` kolem kruhu; všichni si uloží, že koordinátor je $i$ a předají dál, dokud zpráva neoběhne kruh. | + | * Nový lídr pak vyšle `ELECTED(i)` kolem kruhu → všichni si uloží |
- | - **Komplexita**: $\mathcal{O}(n)$ | + | |
- | </ | + | **Vlastnosti:** |
+ | * Komunikační složitost: $\mathcal{O}(n)$ | ||
+ | * Funguje | ||
+ | * Zvítězí proces s nejvyšším ID | ||
+ | * Nevyžaduje globální znalost všech procesů | ||
==== Algoritmus Bully ==== | ==== Algoritmus Bully ==== | ||
- | < | + | |
- | - **Assumpce**: úplná síť (každý zná všechny), procesy se dají porovnat dle ID; detekce pádu je „rychlá“ (time-outs). | + | |
- | - **Kroky (z pohledu procesu $P_i$)** | + | |
- | 1. $P_i$ zjistí, že koordinátor | + | |
- | | + | - Pošle `COORDINATOR(i)` všem **nižším ID** |
- | | + | |
- | | + | - Pokud neobdrží oznámení včas, znovu zahájí |
- | | + | |
+ | **Zprávy: | ||
+ | * `ELECTION` – „ozvi se, pokud žiješ a jsi silnější“ | ||
+ | | ||
+ | * `COORDINATOR(k)` | ||
- | - **Typy zpráv** | + | **Vlastnosti:** |
- | | + | |
- | | + | * Horší složitost: $\mathcal{O}(n^2)$ zpráv v nejhorším případě |
- | | + | |
+ | | ||
- | - **Vlastnosti** | ||
- | - Garantuje, že zvítězí **nejvyšší živé ID** (“bully” přetlačí ostatní). | ||
- | - Nejhorší případ $\mathcal{O}(n^2)$ zpráv (kaskáda voleb, když padá více procesů). | ||
- | - Rychlejší konvergence než kruh, pokud selže jen koordinátor a vyšší ID je blízko. | ||
- | - Netoleruje síťové rozdělení (“split brain”) – vyžaduje spolehlivé detekce crashů. | ||
- | </ | ||