# 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:

```bash
ps -eLf                         # sloupec LWP je TID, NLWP je počet vláken skupiny
ls /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č.

```bash
ulimit -s                          # velikost zásobníku v KiB, na Debianu 8192
cat /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

```bash
top -H -p 1234                     # vlákna procesu na samostatných řádcích
ps -eLf                            # všechna vlákna v systému
grep 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
