Vícevláknové programování na CPU: POSIX vlákna a OpenMP I. Šimeček xsimecek@fit.cvut.cz Katedra počítačových systémů FIT České vysoké učení technické v Praze Ivan Šimeček, 2011 MI-PRC, LS2010/11, Predn.2 Příprava studijního programu Informatika je podporována projektem financovaným z Evropského sociálního fondu a rozpočtu hlavního města Prahy. Praha & EU: Investujeme do vaší budoucnosti
Vlákna na CPU Vlákna v systému se nejčastěji vytváří pomocí: POSIX API OpenMP Výkonnostní nevýhoda: O vzniku/yániku vlákna musí být informován OS (hlavně plánovač)
POSIX vlákna POSIX threads (pthreads) je knihovna funkcí v jazyce C pro psaní vícevláknových aplikací. POSIX API obsahuje funkce pro: správu vláken (vytvoření, zrušení, ) synchronizaci a spolupráci mezi vlákny (uzamykání sdílených prostředků) plánování a spouštění vláken Vhodné spíše pro MIMD aplikace: Aplikace typu server-klient (WWW server, tiskový server) Pro I/O nebo GUI Pro SIMD aplikace zbytečně složité
OpenMP základy Zpracováno dle Blaise Barney, Lawrence Livermore National Laboratory Co je to OpenMP? sjednocené API pro programování vícevláknových aplikací. Jak OpenMP funguje? Ve vybraných částech kódu (dále označovaných jako paralelní bloky = parallel regions) je pomocí fork-join mechanismu vytvořen daný počet vláken a tak je tento paralelní blok řešen vícevláknově. Mimo tyto paralelní bloky existuje pouze jedno vlákno hlavního procesu. Pozn. Nedoporučuje se míchat tento způsob vytváření a zániku threadů POSIX thready.
Jak udělat program vícevláknový? Stanovit si, které části kódu (nejčastěji cykly) mají běžet vícevláknově pomocí OpenMP direktiv = vytvoříme paralelní bloky Stanovit pro jednotlivé proměnné jejich OpenMP vlastnosti. V rámci paralelních bloků se vyhnout "thead-unsafe" operacím. Zkompilovat program pomocí "gcc -fopenmp -O3", první volba je pro zpracování OpenMP, druhá zapíná optimalizace kompilátoru. Je zakázáno vyskočit ven nebo vskočit zvenčí dovnitř paralelního bloku.
Efektivita Kvůli Amdahlovu zákonu, je třeba co největší (časovou) část programu napsat paralelně Ale díky tomu, že vytvoření a zánik vláken má jistou režii, je snaha vytvořit spíše menší počet paralelních bloků. Je snaha co nejvíce se vyhýbat zápisu do sdílených proměnných. Kód v kritických sekcích by měl být co nejkratší.
Ukázka OpenMP #include <omp.h> int main () { int var1, var2, var3; //zde pracuje jen hlavni vlakno procesu (serial code) //nasleduje paralelni blok #pragma omp parallel private(var1, var2) shared(var3) { Uvnitr paralelniho bloku pracuje vice vlaken... Promenne var1, var2 jsou privatni = kazde vlakno v tomto bloku ma vlastni Promenna var3 je sdilena vsemi vlakny } //Na konci bloku se nove vznikla vlakna zrusi} //opet pracuje jen hlavni vlakno procesu (serial code)
Počet vláken Počet vzniklých vláken je řízen těmito výrazy (v tomto pořadí, důležitý je první platný výraz) 1. pokud je uvedena, vyhodnotí se if. Pokud není splněna, bude provádět jako sekvenční kód. 2. pokud je uvedena, provede se dle nastavení num_threads 3. pokud bylo uvedeno, provede se dle nastavení při posledním volání fci omp_set_num_threads() 4. pokud je uvedena, provede se dle nastavení proměnné prostředí OMP_NUM_THREADS 5. v závislosti na implementaci nebo dynamicky.
Direktiva "parallel" Možné parametry jsou: if (podmínka) num_threads (výraz) vlastnosti proměnných (seznam proměnných) = v tomto paralelním bloku budou mít proměnné tyto OpenMP vlastnosti. Přesné chování Je vytvořen daný počet vláken, vlákna jsou očíslována, původní (hlavní vlákno procesu) vlákno má číslo 0. Kód paralelního bloku je zduplikován na všechny vlákna a začne se provádět. Na konci paralelního bloku je implicitní bariéra, nově vzniklá vlákna jsou ukončena, dále pokračuje zase jen původní (hlavní vlákno procesu) vlákno. Pokud je z nějakého důvodu jedno z vláken ukončeno během paralelního bloku, jsou ukončeny všchny vlákna a tím i program.
Omezení direktivy "parallel" Paralelního blok nesmí překročit rámec jedné procedury a musí být v jednom programovém souboru. Je zakázáno vyskočit ven nebo vskočit zvenčí dovnitř paralelního bloku. Je dovolena maximálně jedna if (podmínka) před každým paralelním blokem. Je dovolen maximálně jeden num_threads (výraz) před každým paralelním blokem.
Direktiva "for" Konstrukce pro paralelizaci for-cyklu uvniř paralelního bloku, možné parametry jsou: schedule(viz dále) nowait = vlákna na konci paralelního bloku neprovádí bariéru. ordered = pořádí iterací je stejné jako při sekvenčním provádění collapse = slouží pro upřesnění u vnořených cyklů Pozn: Direktivu "parallel" a "for" je mozno spojit do "parallel for
Direktiva "for schedule schedule(typ,velikost) static = Každému vláknu je staticky přiděleno velikost iterací (jdoucích po sobě) cyklu. Pokud není velikost uvedena, jsou iterace rovnoměrně rozděleny mezi vlákna. dynamic = Každému vláknu je dynamicky přiděleno velikost iterací (jdoucích po sobě) cyklu. Když vlákno dokončí, je mu přidělena další část stejné velikosti. Pokud není velikost uvedena, předpokládá se 1. guided = Každému vláknu je dynamicky přiděleno x iterací ( x je počet neprovedených iterací děleno počtem vláken, x je větší nez velikost kromě posledního kousku) cyklu. Když vlákno dokončí,je mu přidělena další část stejné velikosti. Pokud není velikost uvedena, předpokládá se 1. runtime = Rozhodnutí o plánování je odloženo až do okamžiku provedení dle proměnné prostředí OMP_SCHEDULE. auto = Plánování je necháno na kompilátoru a OS.
Kritické sekce Direktiva "single = specifikuje část paralelního bloku, kterou provádí pouze jedno vlákno Možné parametry jsou: nowait = vlákna na konci paralelního bloku neprovádí bariéru. Direktiva "master = specifikuje část paralelního bloku, kterou provádí pouze master vlákno (to s číslem vlákna rovným 0), ostatní vlákna zpracovávají kód následující po této části bloku. Direktiva "critical = specifikuje část paralelního bloku, kterou může současně provádět pouze jedno vlákno (kritická sekce). Možné parametry jsou: jméno = části se stejným názvem jsou brány jako jedna kritická sekce. Všechny části bez jména jsou také brány jako jedna kritická sekce. Direktiva "atomic = specifikuje paměťové operace, které mají být atomické.
Vlastnosti proměnných Určují vlastnosti proměnných vzhledem k paměťovým operacím. Pozor!!! Pokud budou vlastnosti použity na pointer, aplikují se pouze na tento pointer nikoliv na data na která ukazuje.
Vlastnosti proměnných vlastnost shared = určuje, že daná proměnná bude sdílena všemi vlákny. vlastnost private = určuje, že daná proměnná bude lokální ve vlákně, tozn. že každé vlákno bude mít nezávislou instanci této proměnné. Proměnná je vytvořena jako neinicializovaná.
Vlastnosti proměnných vlastnost firstprivate = určuje, že daná proměnná bude lokální ve vlákně, tozn. každé vlákno bude mít nezávislou instanci této proměnné. Proměnná je vytvořena s původní hodnotou, kterou měla před vstupem do paralelního bloku. vlastnost lastprivate = určuje, že daná proměnná bude lokální ve vlákně, tozn. že každé vlákno bude mít nezávislou instanci této proměnné. Hodnota proměnná z poslední iterace bude překopírována do proměnné hlavního vlákna procesu (bude platná po skončení paralelního bloku).
Vlastnosti proměnných vlastnost default = určuje, jakou vlastnost budou mít defaultně všechny proměnné použité v paralelním bloku (pokud nebude uvedeno jinak). vlastnost reduction = určuje, že daná proměnná bude lokální ve vlákně, tozn. že každé vlákno bude mít nezávislou instanci této proměnné. Po skončení bloku se na všechny instance použije redukční operace a výsledek bude zapsán do proměnné hlavního vlákna procesu (bude platný po skončení paralelního bloku). Možné operace jsou: +, *, -, &, ^, Omezení: Musí se jednat o proměnné jednoduchého typu. V nadřízeném bloku musí být deklarovány jako SHARED. Operace nesmí být přetížena pro daný datový typ. Pro reálné datové typy operace nemusí být asociativní. Každý blok může obsahovat i více proměnných pro redukci ale každou maximálně jednou.
Příklad redukce #include <omp.h> int main () { int i, n, chunk; float a[100], b[100], result; /* Some initializations */ n = 100; chunk = 10; result = 0.0; for (i=0; i < n; i++) { a[i] = i * 1.0; b[i] = i * 2.0; } #pragma omp parallel for \ default(shared) private(i) \ schedule(static,chunk) \ reduction(+:result) for (i=0; i < n; i++) result = result + (a[i] * b[i]); printf("final result= %f\n",result); }
OpenMP operace Operace "barrier = specifikuje bariéru (část kódu, kde na sebe všechny vlákna "počkají" a dále jdou zase všechny) Operace "flush = násilně vyvolá čtení a zápis daných proměných do/z paměti. Možné parametry jsou: seznam proměnných. Operace "omp_get_wtime = vrátí číslo (typu double), které udává uběhnutý čas od nějakém (implementačně závislého) okamžiku v minulosti. Nejčastěji se používá jako párové volání pro zjištění např. doby trvání cyklu, doby trvání programu apod.
OpenMP operace Operace "omp_set_num_threads(int i) = programově změní počet vytvořených vláken v následujících paralelních blocích. Musí být volán ze sekvenční části předcházející paralelnímu bloku. Efekt zůstává v platnosti do dalšího volání. Parametrem je požadovaný počet vláken. Operace "omp_get_num_threads = vrátí počet vláken v aktuálním paralelním bloku. V sekvenční části vrací 1.
Operace s OpenMP zámky Operace omp_init_lock(omp_lock_t *x) = inicializuje OpenMP zámek (mutex) s počáteční hodnotou odemčeno. Operace omp_destroy_lock(omp_lock_t *x) = zruší (inicializovaný) OpenMP zámek (mutex). Operace omp_set_lock(omp_lock_t *x) = pokusí se zamknout OpenMP zámek (mutex). V případě neúspěchu se vlákno uspí. Operace omp_test_lock(omp_lock_t *x) = pokusí se zamknout OpenMP zámek (mutex). V případě neúspěchu vrací 0. Operace omp_unset_lock(omp_lock_t *x) = odemkne OpenMP zámek (mutex).