Tvůj backend skoro nikdy nedělá jen jednu věc — obsluhuje spoustu uživatelů naráz. A jakmile běží víc věcí „současně" a sahají na stejná data, začnou se dít zvláštní věci: výsledky, které nesedí, zaseknutí, chyby, co se objeví jen pod zátěží a v testech nikdy. Tahle kapitola tě naučí, proč se to děje a jak tomu předejít. Neboj, půjdeme pomalu.
Pár pojmů na úvod
- Souběžně (concurrently) = víc úloh „probíhá" v překrývajícím se čase. Server vyřizuje request od Aleny, Bórise i Cyrila zhruba ve stejnou chvíli.
- Vlákno (thread) = „pracovní linka" uvnitř programu, která vykonává kód. Program může mít jedno vlákno, nebo víc běžících paralelně.
- Proces = celá běžící instance programu (má vlastní paměť). Víc procesů = víc nezávislých kopií.
- Sdílený stav = data, ke kterým má přístup víc úloh naráz (společná proměnná, čítač, cache). Tady vzniká většina problémů.
Concurrency vs Parallelism (nepleť si)
- Concurrency (souběžnost) = zvládat víc úloh najednou tím, že mezi nimi rychle přepínáš. Zvládneš i na jednom procesoru.
- Parallelism (paralelismus) = vykonávat víc úloh opravdu naráz na víc procesorech zároveň.
Analogie: jeden kuchař, který žongluje třemi jídly a střídá je, = concurrency. Tři kuchaři, každý u svého jídla, = parallelism.
I/O-bound vs CPU-bound — podle tohohle volíš strategii
Než cokoli zrychluješ, zjisti, čím tvůj kód tráví čas:
- I/O-bound = většinu času čekáš na něco vnějšího (odpověď z databáze, ze sítě, z disku). Procesor zatím nedělá nic. Řešení: async — místo čekání mezitím obsluž jinou úlohu. Sem patří většina běžného backendu. (I/O = input/output, tedy vstup/výstup — komunikace s okolím.)
- CPU-bound = většinu času počítáš (šifrování, zpracování obrázků, komprese). Tady async nepomůže — práci musíš rozdělit na víc procesorů (parallelism).
Častý omyl: hodit async na výpočetně náročnou práci. Nezrychlí to nic — jen zablokuješ to jediné vlákno (viz event loop níže).
Dva způsoby, jak zvládat souběžnost
Vlákna (threads) — používá Java, Go, Python…
Operační systém spouští víc vláken, která můžou běžet paralelně na víc jádrech. Sdílejí ale paměť, takže když dvě vlákna sahají na stejná data, musíš je synchronizovat (viz zámky níže). Vlákno něco stojí (paměť, přepínání). Go to řeší levnými „mini-vlákny" zvanými goroutines.
⚠️ Python má výjimku — GIL (Global Interpreter Lock): standardní Python (CPython) pouští v jednu chvíli jen jedno vlákno s Python kódem, takže vlákna ti CPU-bound práci nezparalelizují (na to potřebuješ víc procesů). Na čekání (I/O) vlákna v Pythonu pomůžou, na počítání ne. (V Javě a Go vlákna paralelně počítají normálně. Pozn. k 2026: Python 3.13+ má experimentální free-threaded build bez GILu — tohle omezení se začíná měnit, ale defaultně pořád platí.)
Event loop (asynchronní model) — používá Node.js, Python asyncio
Tady běží kód na jednom vlákně, ale chytře. Když narazí na čekání (třeba dotaz do databáze), neztuhne — poznamená si „až přijde odpověď, vrať se sem" a mezitím obslouží další úlohu. Je to jako jeden velmi rychlý číšník, který místo postávání u kuchyně bere zatím další objednávky a vrací se pro hotové jídlo, až je nachystané. Tímhle zvládne tisíce souběžných spojení levně.
⚠️ V event loopu nikdy „neztuhni" dlouhým výpočtem (těžká matematika, nekonečný
while). Máš jen jedno vlákno — když ho zaměstnáš na 3 vteřiny, všichni ostatní uživatelé ty 3 vteřiny čekají. Náročné výpočty dej do samostatného vlákna nebo procesu.
async/await, na které narazíš v kódu, je jen čitelnější zápis téhle asynchronní práce.
⚠️ Event loop ≠ paralelismus. Jedna instance Node jede na jednom jádře — async ti pomůže zvládat spoustu čekání naráz, ale nevyužije 8 jader CPU. Na to potřebuješ víc procesů (
cluster/ víc instancí za load balancerem), nebo na CPU práci worker threads. Async je o I/O, ne o jádrech.
Když dvě souběžné operace čtou a zapisují stejná data a výsledek závisí na tom, kdo to stihl dřív, vzniká race condition („závod"). Příklad se sdíleným čítačem:
Oba přečetli 10 dřív, než kterýkoli stihl zapsat, takže oba zapsali 11 a jedno zvýšení se „ztratilo". Je to stejný problém jako u databáze (Databáze), jen v paměti aplikace. Objevuje se přes sdílený stav: globální proměnná, cache, čítač.
Jak se bránit:
- Atomické operace — operace, kterou nelze přerušit v půlce (databázové/Redis
INCRzvýší čítač v jednom kroku, takže se dva nemůžou „protnout"). - Zámky (locks) — kus kódu, kde se sahá na sdílená data, smí provádět vždy jen jeden naráz.
- Nesdílet měnitelný stav — nejlepší řešení. Drž stav venku (databáze, Redis) a měň ho atomicky, ne ve sdílené proměnné v paměti.
Zámky a další synchronizační nástroje
- Mutex / Lock — pustí do „chráněné zóny" jen jednoho naráz.
- Semaphore — pustí nejvýš N naráz (např. „max 5 souběžných volání cizího API").
- Read-Write lock — buď víc čtenářů zároveň, NEBO jeden zapisovatel.
Každý zámek znamená čekání a riziko deadlocku (viz níže). Proto platí: čím míň sdíleného stavu, tím míň zámků, tím míň problémů.
Deadlock — vzájemné zaseknutí
Deadlock nastane, když dvě úlohy čekají každá na to, co drží ta druhá — a nikdo se nehne navždy:
Úloha 1 drží zámek A, chce B
Úloha 2 drží zámek B, chce A → nikdo se nepohne
Existuje teorie (Coffmanovy podmínky), ale pro tebe stačí praktická obrana: zabírej zámky vždy ve stejném pořadí (pak nemůže vzniknout to vzájemné čekání) a dávej jim timeout, ať se případně samy uvolní místo věčného zaseknutí.
Příbuzný jev: starvation (vyhladovění) — úloha se nikdy nedostane ke zdroji, protože ji pořád předbíhají ostatní.
Pooly — nevyráběj nové vlákno/spojení na každou úlohu
Vytvořit vlákno nebo spojení něco stojí, a kdybys je dělal donekonečna, vyčerpáš zdroje a spadneš. Řešení je pool: pevná sada „pracantů", kteří se recyklují, a přebytek úloh počká ve frontě. Je to stejný princip jako connection pooling u databáze. Navíc tě omezený pool chrání před přetížením (jednomu úkolu nedovolí spotřebovat všechno).
Backpressure — tlak zpět
Když něco vyrábí práci rychleji, než ji stíháš zpracovávat, musí existovat způsob, jak říct „zpomal" (nebo úkoly odmítat, nebo mít frontu s limitem). Tomu se říká backpressure. Bez něj fronta nebo paměť roste, až dojde paměť a program spadne (OOM = out of memory). Souvisí s asynchronními frontami.
Failure modes — jak to v praxi praská
- Race condition → občas špatný výsledek pod zátěží, v testech se nikdy neprojeví (proto se těmhle bugům říká „heisenbug" — zmizí, jakmile se na ně díváš).
- Deadlock → zaseknuté requesty, vyčerpaná vlákna.
- Zablokovaný event loop → jeden těžký výpočet položí celou instanci.
- Vyčerpaný pool → když se použité vlákno/spojení „neuklidí", dojdou a nové requesty čekají věčně.
- Neomezená souběžnost → tisíce paralelních volání naráz zahltí cizí službu (chybí semaphore/pool).
🛠️ Cvičení
- Zablokuj event loop. V Node.js napiš endpoint s těžkým synchronním
whilecyklem na pár vteřin. Během něj pošli druhý request a sleduj, že stojí celá instance. Pak výpočet přesuň do samostatného vlákna. - Race na čítači. Spusť 1000× paralelně „zvyš čítač o 1" nad sdílenou proměnnou bez atomicity a
sleduj, že výsledek není 1000. Pak použij atomický
INCRa porovnej. - Vyrob deadlock. Dvě úlohy, dva zámky, opačné pořadí zabírání → zaseknutí. Pak to oprav sjednocením pořadí zámků.
- Omez souběžnost. Máš 500 URL ke stažení. Napiš to tak, aby běželo nejvýš 10 naráz (semaphore / pool), ne všech 500 najednou (zahltil bys cíl i sebe).
- I/O vs CPU. U každé úlohy rozhodni, jestli pomůže async, nebo víc procesů: zavolat 3 API, zmenšit 1000 obrázků, přečíst velký soubor, vypočítat hashe hesel.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
- Zablokuj event loop — těžký synchronní
whilezabere jediné vlákno Node a druhý request stojí celou dobu; přesuň výpočet do worker threadu nebo procesu. Pozor: async by tu nepomohl, problém je CPU-bound, ne čekání na I/O. - Race na čítači — bez atomicity oba requesty přečtou starou hodnotu a přepíšou se navzájem, takže výsledek je míň než 1000 (ztracená zvýšení). Pozor: nestačí „rychlejší kód" — řešení je atomický
INCRnebo držet stav venku (Redis/DB), ne ve sdílené proměnné. - Vyrob deadlock — dvě úlohy zaberou zámky v opačném pořadí (A→B vs B→A) a každá čeká na ten druhý navždy; oprava je sjednotit pořadí zabírání zámků. Pozor: navíc dej zámkům timeout, ať se v nouzi samy uvolní místo věčného zaseknutí.
- Omez souběžnost — pusť nejvýš 10 stahování naráz přes semaphore nebo pool, přebytek nech čekat ve frontě. Pozor: všech 500 najednou zahltí cíl i tebe (neomezená souběžnost je failure mode) — limit chrání obě strany.
- I/O vs CPU — 3 API a čtení velkého souboru jsou I/O-bound (pomůže async), zmenšení obrázků a hashování hesel jsou CPU-bound (potřebuješ víc procesů/jader). Pozor: hodit async na CPU práci nic nezrychlí, jen zablokuje vlákno.
🧠 Otázky & odpovědi
Proč se v event loopu nesmí ztuhnout dlouhým výpočtem?
Node.js běží tvůj kód na jednom vlákně. Když ho zaměstnáš těžkým synchronním výpočtem, po celou tu dobu se neobslouží žádný jiný request — latence vyletí úplně všem uživatelům. Proto náročné výpočty dej do samostatného vlákna nebo procesu a vstupně-výstupní operace (databáze, síť) nechej asynchronní.
Jaký je rozdíl mezi concurrency a parallelism?
Concurrency = zvládat víc úloh „najednou" rychlým přepínáním mezi nimi (jde i na jednom jádře). Parallelism = opravdu vykonávat víc úloh ve stejný okamžik na víc jádrech. Jeden kuchař žonglující třemi jídly = concurrency; tři kuchaři = parallelism. I/O-bound práci řeší concurrency (async), CPU-bound potřebuje parallelism (víc jader).
Jak vzniká deadlock a jak ho nejjednodušeji rozbít?
Dvě úlohy drží každá jeden zámek a čekají na ten druhý → cyklické čekání, nikdo se nehne. Nejjednodušší obrana: zabírej zámky vždy ve stejném pořadí (pak nemůže vzniknout cyklus) a dej zámkům timeout, ať se případně samy uvolní místo věčného zaseknutí.
Proč pool, a ne nové vlákno/spojení na každou úlohu?
Vlákna i spojení něco stojí (paměť, navázání) a kdyby jich vznikalo neomezeně, vyčerpáš zdroje a
spadneš (nebo dostaneš too many connections). Pool drží pevnou sadu recyklovaných pracantů a
přebytek úloh nechá počkat ve frontě. Navíc tě omezený pool chrání před přetížením, protože nepustí
souběžně víc, než uneseš.
Co je backpressure a proč na něm záleží?
Je to „tlak zpět" od pomalejšího zpracování k rychlejšímu zdroji práce: zpomal, odmítni, nebo měj frontu s limitem. Bez něj práce přibývá rychleji, než ji stíháš, fronta nebo paměť roste, až dojde paměť a program spadne (OOM). Backpressure drží systém pod kontrolou, když přijde nápor.
