Operační systémy
Obsah Soubory
markdown

Vlakna.md

11 kB 158 řádků Změněno Zobrazit na GitHubu Stáhnout
markdown
# Vlákna a kdy se vyplatí „Přidám vlákna a bude to rychlejší." Tuhle větu najdeš ve stovkách návodů a platí jen za dvou podmínek, které si skoro nikdo neověří: úloha se dá rozdělit na části, které na sebe nečekají, a je omezená procesorem, ne něčím jiným. Osm vláken na úloze, která čeká na disk, není osmkrát rychlejší. Je stejně rychlé a hůř laditelné. Tahle stránka říká, co vlákno je, jak ho vidí Linux, kdy si vybrat vlákno a kdy proces a kdy vlákna nepomůžou. Neřeší, jak se sdílený stav zamyká - to je na [kritické sekci a zámcích](Kriticka-sekce-a-zamky) a na [semaforech a monitorech](Semafory-a-monitory). Předpokládá, že víš, co je [proces](Procesy) a co stojí [přepnutí kontextu](Prepinani-kontextu). ## Jediný rozdíl proti procesu Tohle je nejdůležitější věc na celé stránce: **vlákna jednoho procesu sdílejí adresní prostor a tabulku otevřených souborů. Vlastní mají jen zásobník, registry a chybové číslo.** Zbytek stránky je rozvedení téhle věty. Výhoda plyne přímo: dvě vlákna si předají ukazatel na strukturu v haldě, žádné kopírování, žádná [meziprocesová komunikace](Meziprocesova-komunikace). Problémy taky: dvě vlákna si tu strukturu můžou přepsat pod rukama. | Sdílené celou skupinou vláken | Vlastní každému vláknu ||---|---|| kód programu | zásobník, implicitně 8 MiB || halda a všechno, co v ní je | registry včetně čítače instrukcí || globální a statické proměnné | `errno` || tabulka otevřených souborů | signálová maska || obsluhy signálů | ID vlákna (TID) a priorita || pracovní adresář, `umask`, UID a GID | úložiště vlákna (TLS) | Signál doručený procesu proto dostane **libovolné** vlákno, které ho nemá zamaskovaný. ### Jak je to v Linuxu doopravdy Tvrzení, že „Linux vlákna nemá" nebo že „vlákno je něco úplně jiného než proces", je nesprávné. Vlákno i proces vznikají stejným [systémovým voláním](Systemova-volani) `clone` a liší se jen příznaky. `fork` je `clone` bez `CLONE_VM` - potomek dostane vlastní adresní prostor. Vlákno je `clone` s `CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD`: sdílená paměť, deskriptory, adresář i obsluhy signálů. Jádro neplánuje procesy. Plánuje **úlohy**, a úloha je přesně to, čemu se v uživatelském prostoru říká vlákno. Vlákna jednoho procesu tvoří *skupinu vláken* (thread group), jejíž ID je TGID - to číslo vrací `getpid()`. Vlákno má navíc vlastní TID z `gettid()`, u hlavního vlákna se obě rovnají. Proto `ps` ve výchozím stavu vlákna neukáže, vypisuje skupiny: ```bashps -eLf                         # sloupec LWP je TID, NLWP je počet vláken skupinyls /proc/1234/task/             # jeden adresář na vlákno, jméno adresáře je TID``` ## Kdo vlákna plánuje ### Tři modely **1:1, plánuje jádro.** Každé uživatelské vlákno je jedna úloha jádra. Standard na Linuxu i na Windows; v glibc ho zavedla knihovna NPTL v roce 2003 spolu s jádrem 2.6. Plánovač o vláknech ví, rozloží je na jádra a blokující volání zablokuje jen to jedno. **N:1, zelená vlákna v knihovně.** Přepínání řeší běhové prostředí, jádro vidí jediné vlákno. Přepnutí je levné, ale všechno běží na jednom jádře a **jedno blokující volání zablokuje všechna vlákna naráz**. **M:N.** N uživatelských vláken na M vláknech jádra. Zkoušel to Solaris před verzí 9, staré Javy a knihovna NGPT. Dnes je opuštěné: dva plánovače nad sebou dělají rozhodnutí, o kterých ten druhý neví, a výsledek byl pomalejší než prostý model 1:1. Solaris přešel na 1:1 ve verzi 9 (rok 2002). **Doporučení: neřeš to.** Na Linuxu dostaneš 1:1 a je to správná volba. ### Kam patří korutiny a goroutiny Goroutiny v Go, korutiny v Kotlinu a virtuální vlákna v Javě (od verze 21, rok 2023) jsou zelená vlákna nad modelem 1:1: runtime drží pár skutečných vláken jádra a na ně multiplexuje tisíce svých. Rozdíl proti starým zeleným vláknům je zásadní: runtime umí blokující volání obejít. Když goroutina sáhne na síť, runtime ji odloží, čekání vloží do `epoll` a na uvolněné vlákno pustí jinou. Kde ten trik nefunguje: volání do knihovny v C, na kterém vlákno jádra skutečně stojí. ## Kdy vlákno a kdy proces | Situace | Vezmi ||---|---|| sdílení velké datové struktury | **vlákna** || rozdělení jednoho výpočtu na jádra | **vlákna** || izolace proti pádu jedné části | **procesy** || nedůvěryhodný nebo cizí kód | **procesy** | **Když potřebuješ izolaci proti pádu, ber procesy.** Vlákno, které sáhne na neplatný ukazatel, dostane `SIGSEGV` a s ním padá celý proces včetně ostatních vláken. Plyne to přímo ze sdíleného adresního prostoru. Proto má prohlížeč záložku ve vlastním procesu, a ne ve vlákně - Chrome od vydání v roce 2008, Firefox od roku 2017. Cena je paměť, výnos je, že spadlá stránka nevezme s sebou zbylých třicet. **Když potřebuješ sdílet velkou datovou strukturu, ber vlákna.** Předání ukazatele nestojí nic; sdílená paměť přes [meziprocesovou komunikaci](Meziprocesova-komunikace) je práce navíc. ## Kdy vlákna nepomůžou vůbec **Úloha omezená diskem nebo sítí.** Osm vláken, která čekají na `read` z `/dev/nvme0n1`, čeká osmkrát vedle sebe. Když je úzké hrdlo ve [vstupu a výstupu](Vstup-a-vystup), vlákna přidají jen režii na přepínání. **GIL v CPythonu.** Globální zámek interpretu dovolí vykonávat bajtkód vždycky jen jednomu vláknu. Osm vláken na výpočtu v čistém Pythonu běží na jednom jádře, kvůli přepínání pomaleji než jedno. Na výpočet tedy vezmi procesy přes `multiprocessing`, na čekání vlákna nech - GIL se při blokujícím volání uvolňuje. Od Pythonu 3.13 (rok 2024) je volitelný i sestavovací režim bez GIL, ale je nový a část rozšíření v C s ním neběží. ## Co se s vlákny nedá dělat bezpečně **Nereentrantní funkce.** `strtok`, `gmtime` nebo `getpwnam` drží výsledek ve statické proměnné, kterou ti druhé vlákno přepíše mezi zavoláním a přečtením. Používej varianty s `_r`. **Sdílený stav bez zámku.** Ani `i++` není atomické, je to načtení, přičtení a zápis, a mezi tím se dá přepnout. Nedá se to obejít tím, že „ta operace je přece rychlá" - patří na to zámek z [kritické sekce](Kriticka-sekce-a-zamky). Ozařovač Therac-25 měl v letech 1985 až 1987 šest známých nehod a nejméně tři mrtvé pacienty. Příčinou byl souběh mezi obsluhou klávesnice a nastavovací úlohou: když operátor přepsal zadání dost rychle, stroj vystřelil plnou dávku bez rozptylovače. Poučení: **souběh se projeví jen při určitém časování a běžné testy ho nenajdou.** ### fork ve vícevláknovém programu `fork` zkopíruje adresní prostor, ale **potomek zdědí jen to vlákno, které `fork` zavolalo**. Ostatní vlákna v potomkovi neexistují. Zámky zdědí všechny, včetně zamčených. Když jiné vlákno zrovna drželo zámek uvnitř alokátoru, v potomkovi zůstane zamčený navždy - nikdo ho už neodemkne. První `malloc` pak zamrzne a nikdo neví proč. POSIX to řeší tvrdě: mezi `fork` a `exec` smíš volat jen funkce bezpečné vůči signálům, což vylučuje `malloc` i `printf`. **Doporučení: spouštěj potomka přes `posix_spawn`.** Udělá obojí naráz a nedá ti šanci mezi tím něco pokazit. ## Kolik vláken vlastně **Výpočet omezený procesorem: tolik vláken, kolik je jader.** Na osmijádrovém stroji osm. Víc jich znamená jen víc [přepínání kontextu](Prepinani-kontextu) a horší využití mezipaměti. **Čekání na vstup a výstup: víc vláken než jader, ale ne o řád.** Začni dvojnásobkem počtu jader a změř to. **Deset tisíc vláken není plán.** Každé dostane zásobník o implicitní velikosti 8 MiB - jen rezervace ve [virtuálním adresním prostoru](Sprava-pameti), fyzická paměť se přidělí podle dotčených stránek. Pořád je to ale deset tisíc úloh pro plánovač. ```bashulimit -s                          # velikost zásobníku v KiB, na Debianu 8192cat /proc/sys/kernel/threads-max   # strop pro celý systém``` Deset tisíc spojení se řeší asynchronním [vstupem a výstupem](Vstup-a-vystup) - `epoll` nebo `io_uring` a hrstka vláken obsluhujících hotové události. Nginx tak drží desítky tisíc spojení osmi procesy. ## Diagnostika ```bashtop -H -p 1234                     # vlákna procesu na samostatných řádcíchps -eLf                            # všechna vlákna v systémugrep Threads /proc/1234/status     # neroste jejich počet?gdb -p 1234                        # připojení k běžícímu procesu, běh se zastaví``` V `gdb` pak stačí `thread apply all bt` - vypíše zásobník všech vláken naráz. **Tohle je nejrychlejší cesta k uváznutí:** když dvě vlákna stojí v `pthread_mutex_lock`, máš odpověď a jdeš na [uváznutí](Uvaznuti). | Příznak | Kde je problém ||---|---|| Program stojí, procesor na nule | čeká se na zámek nebo na vstup a výstup, uváznutí || Procesor na 100 %, nic se neděje | aktivní čekání ve smyčce nebo živé uváznutí || Výkon klesá s přibývajícími vlákny | rvačka o jeden zámek nebo falešné sdílení || Jedno vlákno na 100 %, ostatní nic | práce se nerozdělila, nebo GIL || Chyba nastane jen pod zátěží | souběh nad sdíleným stavem bez zámku || Potomek po `fork` zamrzne | zděděný zamčený zámek || Paměť roste s počtem vláken | zásobníky, zkontroluj `ulimit -s` | **Falešné sdílení je z té tabulky nejméně čitelná položka.** Dvě vlákna zapisují do různých proměnných, které leží ve stejném řádku mezipaměti (na x86-64 64 bajtů), a hardware ten řádek posílá mezi jádry tam a zpět. Lék je odsadit proměnné na hranici řádku. ## Co si odnést **Vlákno je úloha, která sdílí adresní prostor.** Vlastní má jen zásobník, registry a `errno`. **V Linuxu je vlákno i proces `clone`, jen s jinými příznaky.** Jádro plánuje úlohy, ne procesy. **Vlákna zrychlí jen rozdělitelnou úlohu omezenou procesorem.** Na čekání na disk nebo síť nepomůžou. **Izolace proti pádu se dělá procesy.** Jeden `SIGSEGV` položí celou skupinu vláken. **Sdílený stav bez zámku je chyba, i když ti to zatím vychází.** Testy souběh nenajdou. **Osm jader znamená osm výpočetních vláken, ne osmdesát.** Na deset tisíc spojení patří asynchronní vstup a výstup. **`fork` ve vícevláknovém programu je past.** Použij `posix_spawn`. ## Kam dál - **[Kritická sekce a zámky](Kriticka-sekce-a-zamky)** - jak sdílený stav ochránit, aby to nebyla loterie- **[Přepínání kontextu](Prepinani-kontextu)** - co stojí přepnutí a proč je mezi vlákny levnější- **[Plánování procesů](Planovani-procesu)** - jak jádro rozhoduje, které vlákno poběží dál- **[Vstup a výstup](Vstup-a-vystup)** - model, který nahrazuje tisíce čekajících vláken- **[Nástroje a diagnostika](Nastroje-a-diagnostika)** - `perf`, `strace` a zbytek výbavy